first commit
This commit is contained in:
commit
5893b00dd2
1669 changed files with 1982740 additions and 0 deletions
|
|
@ -0,0 +1,677 @@
|
|||
|
||||
#include <M5UnitLCD.h>
|
||||
#include <M5UnitOLED.h>
|
||||
#include <M5Unified.h>
|
||||
|
||||
/// need ESP32-A2DP library. ( URL : https://github.com/pschatzmann/ESP32-A2DP/ )
|
||||
#include <BluetoothA2DPSink.h>
|
||||
|
||||
/// set M5Speaker virtual channel (0-7)
|
||||
static constexpr uint8_t m5spk_virtual_channel = 0;
|
||||
|
||||
/// set ESP32-A2DP device name
|
||||
static constexpr char bt_device_name[] = "ESP32";
|
||||
|
||||
|
||||
class BluetoothA2DPSink_M5Speaker : public BluetoothA2DPSink
|
||||
{
|
||||
public:
|
||||
BluetoothA2DPSink_M5Speaker(m5::Speaker_Class* m5sound, uint8_t virtual_channel = 0)
|
||||
: BluetoothA2DPSink()
|
||||
{
|
||||
is_i2s_output = false; // I2S control by BluetoothA2DPSink is not required.
|
||||
}
|
||||
|
||||
// get rawdata buffer for FFT.
|
||||
const int16_t* getBuffer(void) const { return _tri_buf[_export_index]; }
|
||||
|
||||
const char* getMetaData(size_t id, bool clear_flg = true) { if (clear_flg) { _meta_bits &= ~(1<<id); } return (id < metatext_num) ? _meta_text[id] : nullptr; }
|
||||
|
||||
uint8_t getMetaUpdateInfo(void) const { return _meta_bits; }
|
||||
void clearMetaUpdateInfo(void) { _meta_bits = 0; }
|
||||
|
||||
void clear(void)
|
||||
{
|
||||
for (int i = 0; i < 3; ++i)
|
||||
{
|
||||
if (_tri_buf[i]) { memset(_tri_buf[i], 0, _tri_buf_size[i]); }
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr size_t metatext_size = 128;
|
||||
static constexpr size_t metatext_num = 3;
|
||||
|
||||
protected:
|
||||
int16_t* _tri_buf[3] = { nullptr, nullptr, nullptr };
|
||||
size_t _tri_buf_size[3] = { 0, 0, 0 };
|
||||
size_t _tri_index = 0;
|
||||
size_t _export_index = 0;
|
||||
char _meta_text[metatext_num][metatext_size];
|
||||
uint8_t _meta_bits = 0;
|
||||
size_t _sample_rate = 48000;
|
||||
|
||||
void clearMetaData(void)
|
||||
{
|
||||
for (int i = 0; i < metatext_num; ++i)
|
||||
{
|
||||
_meta_text[i][0] = 0;
|
||||
}
|
||||
_meta_bits = (1<<metatext_num)-1;
|
||||
}
|
||||
|
||||
void av_hdl_a2d_evt(uint16_t event, void *p_param) override
|
||||
{
|
||||
esp_a2d_cb_param_t* a2d = (esp_a2d_cb_param_t *)(p_param);
|
||||
|
||||
switch (event) {
|
||||
case ESP_A2D_CONNECTION_STATE_EVT:
|
||||
if (ESP_A2D_CONNECTION_STATE_CONNECTED == a2d->conn_stat.state)
|
||||
{ // 接続
|
||||
|
||||
}
|
||||
else
|
||||
if (ESP_A2D_CONNECTION_STATE_DISCONNECTED == a2d->conn_stat.state)
|
||||
{ // 切断
|
||||
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_A2D_AUDIO_STATE_EVT:
|
||||
if (ESP_A2D_AUDIO_STATE_STARTED == a2d->audio_stat.state)
|
||||
{ // 再生
|
||||
|
||||
} else
|
||||
if ( ESP_A2D_AUDIO_STATE_REMOTE_SUSPEND == a2d->audio_stat.state
|
||||
|| ESP_A2D_AUDIO_STATE_STOPPED == a2d->audio_stat.state )
|
||||
{ // 停止
|
||||
clearMetaData();
|
||||
clear();
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_A2D_AUDIO_CFG_EVT:
|
||||
{
|
||||
esp_a2d_cb_param_t *a2d = (esp_a2d_cb_param_t *)(p_param);
|
||||
size_t tmp = a2d->audio_cfg.mcc.cie.sbc[0];
|
||||
size_t rate = 16000;
|
||||
if ( tmp & (1 << 6)) { rate = 32000; }
|
||||
else if (tmp & (1 << 5)) { rate = 44100; }
|
||||
else if (tmp & (1 << 4)) { rate = 48000; }
|
||||
_sample_rate = rate;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
BluetoothA2DPSink::av_hdl_a2d_evt(event, p_param);
|
||||
}
|
||||
|
||||
void av_hdl_avrc_evt(uint16_t event, void *p_param) override
|
||||
{
|
||||
esp_avrc_ct_cb_param_t *rc = (esp_avrc_ct_cb_param_t *)(p_param);
|
||||
|
||||
switch (event)
|
||||
{
|
||||
case ESP_AVRC_CT_METADATA_RSP_EVT:
|
||||
for (size_t i = 0; i < metatext_num; ++i)
|
||||
{
|
||||
if (0 == (rc->meta_rsp.attr_id & (1 << i))) { continue; }
|
||||
strncpy(_meta_text[i], (char*)(rc->meta_rsp.attr_text), metatext_size);
|
||||
_meta_bits |= rc->meta_rsp.attr_id;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ESP_AVRC_CT_CONNECTION_STATE_EVT:
|
||||
break;
|
||||
|
||||
case ESP_AVRC_CT_CHANGE_NOTIFY_EVT:
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
BluetoothA2DPSink::av_hdl_avrc_evt(event, p_param);
|
||||
}
|
||||
|
||||
int16_t* get_next_buf(const uint8_t* src_data, uint32_t len)
|
||||
{
|
||||
size_t tri = _tri_index < 2 ? _tri_index + 1 : 0;
|
||||
if (_tri_buf_size[tri] < len)
|
||||
{
|
||||
_tri_buf_size[tri] = len;
|
||||
if (_tri_buf[tri] != nullptr) { heap_caps_free(_tri_buf[tri]); }
|
||||
auto tmp = (int16_t*)heap_caps_malloc(len, MALLOC_CAP_8BIT);
|
||||
_tri_buf[tri] = tmp;
|
||||
if (tmp == nullptr)
|
||||
{
|
||||
_tri_buf_size[tri] = 0;
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
memcpy(_tri_buf[tri], src_data, len);
|
||||
_tri_index = tri;
|
||||
return _tri_buf[tri];
|
||||
}
|
||||
|
||||
void audio_data_callback(const uint8_t *data, uint32_t length) override
|
||||
{
|
||||
// Reduce memory requirements by dividing the received data into the first and second halves.
|
||||
length >>= 1;
|
||||
M5.Speaker.playRaw(get_next_buf( data , length), length >> 1, _sample_rate, true, 1, m5spk_virtual_channel);
|
||||
M5.Speaker.playRaw(get_next_buf(&data[length], length), length >> 1, _sample_rate, true, 1, m5spk_virtual_channel);
|
||||
_export_index = _tri_index;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
#define FFT_SIZE 256
|
||||
class fft_t
|
||||
{
|
||||
float _wr[FFT_SIZE + 1];
|
||||
float _wi[FFT_SIZE + 1];
|
||||
float _fr[FFT_SIZE + 1];
|
||||
float _fi[FFT_SIZE + 1];
|
||||
uint16_t _br[FFT_SIZE + 1];
|
||||
size_t _ie;
|
||||
|
||||
public:
|
||||
fft_t(void)
|
||||
{
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.141592653
|
||||
#endif
|
||||
_ie = logf( (float)FFT_SIZE ) / log(2.0) + 0.5;
|
||||
static constexpr float omega = 2.0f * M_PI / FFT_SIZE;
|
||||
static constexpr int s4 = FFT_SIZE / 4;
|
||||
static constexpr int s2 = FFT_SIZE / 2;
|
||||
for ( int i = 1 ; i < s4 ; ++i)
|
||||
{
|
||||
float f = cosf(omega * i);
|
||||
_wi[s4 + i] = f;
|
||||
_wi[s4 - i] = f;
|
||||
_wr[ i] = f;
|
||||
_wr[s2 - i] = -f;
|
||||
}
|
||||
_wi[s4] = _wr[0] = 1;
|
||||
|
||||
size_t je = 1;
|
||||
_br[0] = 0;
|
||||
_br[1] = FFT_SIZE / 2;
|
||||
for ( size_t i = 0 ; i < _ie - 1 ; ++i )
|
||||
{
|
||||
_br[ je << 1 ] = _br[ je ] >> 1;
|
||||
je = je << 1;
|
||||
for ( size_t j = 1 ; j < je ; ++j )
|
||||
{
|
||||
_br[je + j] = _br[je] + _br[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void exec(const int16_t* in)
|
||||
{
|
||||
memset(_fi, 0, sizeof(_fi));
|
||||
for ( size_t j = 0 ; j < FFT_SIZE / 2 ; ++j )
|
||||
{
|
||||
float basej = 0.25 * (1.0-_wr[j]);
|
||||
size_t r = FFT_SIZE - j - 1;
|
||||
|
||||
/// perform han window and stereo to mono convert.
|
||||
_fr[_br[j]] = basej * (in[j * 2] + in[j * 2 + 1]);
|
||||
_fr[_br[r]] = basej * (in[r * 2] + in[r * 2 + 1]);
|
||||
}
|
||||
|
||||
size_t s = 1;
|
||||
size_t i = 0;
|
||||
do
|
||||
{
|
||||
size_t ke = s;
|
||||
s <<= 1;
|
||||
size_t je = FFT_SIZE / s;
|
||||
size_t j = 0;
|
||||
do
|
||||
{
|
||||
size_t k = 0;
|
||||
do
|
||||
{
|
||||
size_t l = s * j + k;
|
||||
size_t m = ke * (2 * j + 1) + k;
|
||||
size_t p = je * k;
|
||||
float Wxmr = _fr[m] * _wr[p] + _fi[m] * _wi[p];
|
||||
float Wxmi = _fi[m] * _wr[p] - _fr[m] * _wi[p];
|
||||
_fr[m] = _fr[l] - Wxmr;
|
||||
_fi[m] = _fi[l] - Wxmi;
|
||||
_fr[l] += Wxmr;
|
||||
_fi[l] += Wxmi;
|
||||
} while ( ++k < ke) ;
|
||||
} while ( ++j < je );
|
||||
} while ( ++i < _ie );
|
||||
}
|
||||
|
||||
uint32_t get(size_t index)
|
||||
{
|
||||
return (index < FFT_SIZE / 2) ? (uint32_t)sqrtf(_fr[ index ] * _fr[ index ] + _fi[ index ] * _fi[ index ]) : 0u;
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr size_t WAVE_SIZE = 320;
|
||||
static BluetoothA2DPSink_M5Speaker a2dp_sink = { &M5.Speaker, m5spk_virtual_channel };
|
||||
static fft_t fft;
|
||||
static bool fft_enabled = false;
|
||||
static bool wave_enabled = false;
|
||||
static uint16_t prev_y[(FFT_SIZE / 2)+1];
|
||||
static uint16_t peak_y[(FFT_SIZE / 2)+1];
|
||||
static int16_t wave_y[WAVE_SIZE];
|
||||
static int16_t wave_h[WAVE_SIZE];
|
||||
static int16_t raw_data[WAVE_SIZE * 2];
|
||||
static int header_height = 0;
|
||||
|
||||
|
||||
uint32_t bgcolor(LGFX_Device* gfx, int y)
|
||||
{
|
||||
auto h = gfx->height();
|
||||
auto dh = h - header_height;
|
||||
int v = ((h - y)<<5) / dh;
|
||||
if (dh > 44)
|
||||
{
|
||||
int v2 = ((h - y - 1)<<5) / dh;
|
||||
if ((v >> 2) != (v2 >> 2))
|
||||
{
|
||||
return 0x666666u;
|
||||
}
|
||||
}
|
||||
return gfx->color888(v + 2, v, v + 6);
|
||||
}
|
||||
|
||||
void gfxSetup(LGFX_Device* gfx)
|
||||
{
|
||||
if (gfx == nullptr) { return; }
|
||||
if (gfx->width() < gfx->height())
|
||||
{
|
||||
gfx->setRotation(gfx->getRotation()^1);
|
||||
}
|
||||
gfx->setFont(&fonts::lgfxJapanGothic_12);
|
||||
gfx->setEpdMode(epd_mode_t::epd_fastest);
|
||||
gfx->setCursor(0, 8);
|
||||
gfx->print("BT A2DP : ");
|
||||
gfx->println(bt_device_name);
|
||||
gfx->setTextWrap(false);
|
||||
gfx->fillRect(0, 6, gfx->width(), 2, TFT_BLACK);
|
||||
|
||||
header_height = (gfx->height() > 80) ? 45 : 21;
|
||||
fft_enabled = !gfx->isEPD();
|
||||
if (fft_enabled)
|
||||
{
|
||||
wave_enabled = (gfx->getBoard() != m5gfx::board_M5UnitLCD);
|
||||
|
||||
for (int y = header_height; y < gfx->height(); ++y)
|
||||
{
|
||||
gfx->drawFastHLine(0, y, gfx->width(), bgcolor(gfx, y));
|
||||
}
|
||||
}
|
||||
|
||||
for (int x = 0; x < (FFT_SIZE/2)+1; ++x)
|
||||
{
|
||||
prev_y[x] = INT16_MAX;
|
||||
peak_y[x] = INT16_MAX;
|
||||
}
|
||||
for (int x = 0; x < WAVE_SIZE; ++x)
|
||||
{
|
||||
wave_y[x] = gfx->height();
|
||||
wave_h[x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void gfxLoop(LGFX_Device* gfx)
|
||||
{
|
||||
if (gfx == nullptr) { return; }
|
||||
if (header_height > 32)
|
||||
{
|
||||
auto bits = a2dp_sink.getMetaUpdateInfo();
|
||||
if (bits)
|
||||
{
|
||||
gfx->startWrite();
|
||||
for (int id = 0; id < a2dp_sink.metatext_num; ++id)
|
||||
{
|
||||
if (0 == (bits & (1<<id))) { continue; }
|
||||
size_t y = id * 12;
|
||||
if (y+12 >= header_height) { continue; }
|
||||
gfx->setCursor(4, 8 + y);
|
||||
gfx->fillRect(0, 8 + y, gfx->width(), 12, gfx->getBaseColor());
|
||||
gfx->print(a2dp_sink.getMetaData(id));
|
||||
gfx->print(" "); // Garbage data removal when UTF8 characters are broken in the middle.
|
||||
}
|
||||
gfx->display();
|
||||
gfx->endWrite();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
static int title_x;
|
||||
static int title_id;
|
||||
static int wait = INT16_MAX;
|
||||
|
||||
if (a2dp_sink.getMetaUpdateInfo())
|
||||
{
|
||||
gfx->fillRect(0, 8, gfx->width(), 12, TFT_BLACK);
|
||||
a2dp_sink.clearMetaUpdateInfo();
|
||||
title_x = 4;
|
||||
title_id = 0;
|
||||
wait = 0;
|
||||
}
|
||||
|
||||
if (--wait < 0)
|
||||
{
|
||||
int tx = title_x;
|
||||
int tid = title_id;
|
||||
wait = 3;
|
||||
gfx->startWrite();
|
||||
uint_fast8_t no_data_bits = 0;
|
||||
do
|
||||
{
|
||||
if (tx == 4) { wait = 255; }
|
||||
gfx->setCursor(tx, 8);
|
||||
const char* meta = a2dp_sink.getMetaData(tid, false);
|
||||
if (meta[0] != 0)
|
||||
{
|
||||
gfx->print(meta);
|
||||
gfx->print(" / ");
|
||||
tx = gfx->getCursorX();
|
||||
if (++tid == a2dp_sink.metatext_num) { tid = 0; }
|
||||
if (tx <= 4)
|
||||
{
|
||||
title_x = tx;
|
||||
title_id = tid;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((no_data_bits |= 1 << tid) == ((1 << a2dp_sink.metatext_num) - 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (++tid == a2dp_sink.metatext_num) { tid = 0; }
|
||||
}
|
||||
} while (tx < gfx->width());
|
||||
--title_x;
|
||||
gfx->display();
|
||||
gfx->endWrite();
|
||||
}
|
||||
}
|
||||
if (!gfx->displayBusy())
|
||||
{ // draw volume bar
|
||||
static int px;
|
||||
uint8_t v = M5.Speaker.getVolume();
|
||||
int x = v * (gfx->width()) >> 8;
|
||||
if (px != x)
|
||||
{
|
||||
gfx->fillRect(x, 6, px - x, 2, px < x ? 0xAAFFAAu : 0u);
|
||||
gfx->display();
|
||||
px = x;
|
||||
}
|
||||
}
|
||||
|
||||
if (fft_enabled && !gfx->displayBusy())
|
||||
{
|
||||
static int prev_x[2];
|
||||
static int peak_x[2];
|
||||
static bool prev_conn;
|
||||
bool connected = a2dp_sink.is_connected();
|
||||
if (prev_conn != connected)
|
||||
{
|
||||
prev_conn = connected;
|
||||
if (!connected)
|
||||
{
|
||||
a2dp_sink.clear();
|
||||
}
|
||||
}
|
||||
|
||||
auto buf = a2dp_sink.getBuffer();
|
||||
if (buf)
|
||||
{
|
||||
memcpy(raw_data, buf, WAVE_SIZE * 2 * sizeof(int16_t)); // stereo data copy
|
||||
gfx->startWrite();
|
||||
|
||||
// draw stereo level meter
|
||||
for (size_t i = 0; i < 2; ++i)
|
||||
{
|
||||
int32_t level = 0;
|
||||
for (size_t j = i; j < 640; j += 32)
|
||||
{
|
||||
uint32_t lv = abs(raw_data[j]);
|
||||
if (level < lv) { level = lv; }
|
||||
}
|
||||
|
||||
int32_t x = (level * gfx->width()) / INT16_MAX;
|
||||
int32_t px = prev_x[i];
|
||||
if (px != x)
|
||||
{
|
||||
gfx->fillRect(x, i * 3, px - x, 2, px < x ? 0xFF9900u : 0x330000u);
|
||||
prev_x[i] = x;
|
||||
}
|
||||
px = peak_x[i];
|
||||
if (px > x)
|
||||
{
|
||||
gfx->writeFastVLine(px, i * 3, 2, TFT_BLACK);
|
||||
px--;
|
||||
}
|
||||
else
|
||||
{
|
||||
px = x;
|
||||
}
|
||||
if (peak_x[i] != px)
|
||||
{
|
||||
peak_x[i] = px;
|
||||
gfx->writeFastVLine(px, i * 3, 2, TFT_WHITE);
|
||||
}
|
||||
}
|
||||
gfx->display();
|
||||
|
||||
// draw FFT level meter
|
||||
fft.exec(raw_data);
|
||||
size_t bw = gfx->width() / 60;
|
||||
if (bw < 3) { bw = 3; }
|
||||
int32_t dsp_height = gfx->height();
|
||||
int32_t fft_height = dsp_height - header_height - 1;
|
||||
size_t xe = gfx->width() / bw;
|
||||
if (xe > (FFT_SIZE/2)) { xe = (FFT_SIZE/2); }
|
||||
int32_t wave_next = ((header_height + dsp_height) >> 1) + (((256 - (raw_data[0] + raw_data[1])) * fft_height) >> 17);
|
||||
|
||||
uint32_t bar_color[2] = { 0x000033u, 0x99AAFFu };
|
||||
|
||||
for (size_t bx = 0; bx <= xe; ++bx)
|
||||
{
|
||||
size_t x = bx * bw;
|
||||
if ((x & 7) == 0) { gfx->display(); taskYIELD(); }
|
||||
int32_t f = fft.get(bx);
|
||||
int32_t y = (f * fft_height) >> 18;
|
||||
if (y > fft_height) { y = fft_height; }
|
||||
y = dsp_height - y;
|
||||
int32_t py = prev_y[bx];
|
||||
if (y != py)
|
||||
{
|
||||
gfx->fillRect(x, y, bw - 1, py - y, bar_color[(y < py)]);
|
||||
prev_y[bx] = y;
|
||||
}
|
||||
py = peak_y[bx] + 1;
|
||||
if (py < y)
|
||||
{
|
||||
gfx->writeFastHLine(x, py - 1, bw - 1, bgcolor(gfx, py - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
py = y - 1;
|
||||
}
|
||||
if (peak_y[bx] != py)
|
||||
{
|
||||
peak_y[bx] = py;
|
||||
gfx->writeFastHLine(x, py, bw - 1, TFT_WHITE);
|
||||
}
|
||||
|
||||
|
||||
if (wave_enabled)
|
||||
{
|
||||
for (size_t bi = 0; bi < bw; ++bi)
|
||||
{
|
||||
size_t i = x + bi;
|
||||
if (i >= gfx->width() || i >= WAVE_SIZE) { break; }
|
||||
y = wave_y[i];
|
||||
int32_t h = wave_h[i];
|
||||
bool use_bg = (bi+1 == bw);
|
||||
if (h>0)
|
||||
{ /// erase previous wave.
|
||||
gfx->setAddrWindow(i, y, 1, h);
|
||||
h += y;
|
||||
do
|
||||
{
|
||||
uint32_t bg = (use_bg || y < peak_y[bx]) ? bgcolor(gfx, y)
|
||||
: (y == peak_y[bx]) ? 0xFFFFFFu
|
||||
: bar_color[(y >= prev_y[bx])];
|
||||
gfx->writeColor(bg, 1);
|
||||
} while (++y < h);
|
||||
}
|
||||
size_t i2 = i << 1;
|
||||
int32_t y1 = wave_next;
|
||||
wave_next = ((header_height + dsp_height) >> 1) + (((256 - (raw_data[i2] + raw_data[i2 + 1])) * fft_height) >> 17);
|
||||
int32_t y2 = wave_next;
|
||||
if (y1 > y2)
|
||||
{
|
||||
int32_t tmp = y1;
|
||||
y1 = y2;
|
||||
y2 = tmp;
|
||||
}
|
||||
y = y1;
|
||||
h = y2 + 1 - y;
|
||||
wave_y[i] = y;
|
||||
wave_h[i] = h;
|
||||
if (h>0)
|
||||
{ /// draw new wave.
|
||||
gfx->setAddrWindow(i, y, 1, h);
|
||||
h += y;
|
||||
do
|
||||
{
|
||||
uint32_t bg = (y < prev_y[bx]) ? 0xFFCC33u : 0xFFFFFFu;
|
||||
gfx->writeColor(bg, 1);
|
||||
} while (++y < h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx->display();
|
||||
gfx->endWrite();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup(void)
|
||||
{
|
||||
auto cfg = M5.config();
|
||||
|
||||
// If you want to play sound from ModuleDisplay, write this
|
||||
// cfg.external_speaker.module_display = true;
|
||||
|
||||
// If you want to play sound from ModuleRCA, write this
|
||||
// cfg.external_speaker.module_rca = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker, write this
|
||||
cfg.external_speaker.hat_spk = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker2, write this
|
||||
// cfg.external_speaker.hat_spk2 = true;
|
||||
|
||||
// If you want to play sound from ATOMIC Speaker, write this
|
||||
cfg.external_speaker.atomic_spk = true;
|
||||
|
||||
M5.begin(cfg);
|
||||
|
||||
|
||||
{ /// custom setting
|
||||
auto spk_cfg = M5.Speaker.config();
|
||||
/// Increasing the sample_rate will improve the sound quality instead of increasing the CPU load.
|
||||
spk_cfg.sample_rate = 96000; // default:64000 (64kHz) e.g. 48000 , 50000 , 80000 , 96000 , 100000 , 128000 , 144000 , 192000 , 200000
|
||||
spk_cfg.task_pinned_core = APP_CPU_NUM;
|
||||
// spk_cfg.task_priority = configMAX_PRIORITIES - 2;
|
||||
spk_cfg.dma_buf_count = 20;
|
||||
// spk_cfg.dma_buf_len = 512;
|
||||
M5.Speaker.config(spk_cfg);
|
||||
}
|
||||
|
||||
|
||||
M5.Speaker.begin();
|
||||
|
||||
a2dp_sink.start(bt_device_name, false);
|
||||
|
||||
gfxSetup(&M5.Display);
|
||||
}
|
||||
|
||||
void loop(void)
|
||||
{
|
||||
gfxLoop(&M5.Display);
|
||||
|
||||
{
|
||||
static int prev_frame;
|
||||
int frame;
|
||||
do
|
||||
{
|
||||
vTaskDelay(1);
|
||||
} while (prev_frame == (frame = millis() >> 3)); /// 8 msec cycle wait
|
||||
prev_frame = frame;
|
||||
}
|
||||
|
||||
M5.update();
|
||||
if (M5.BtnA.wasPressed())
|
||||
{
|
||||
M5.Speaker.tone(440, 50);
|
||||
}
|
||||
if (M5.BtnA.wasDecideClickCount())
|
||||
{
|
||||
switch (M5.BtnA.getClickCount())
|
||||
{
|
||||
case 1:
|
||||
M5.Speaker.tone(1000, 100);
|
||||
a2dp_sink.next();
|
||||
break;
|
||||
|
||||
case 2:
|
||||
M5.Speaker.tone(800, 100);
|
||||
a2dp_sink.previous();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (M5.BtnA.isHolding() || M5.BtnB.isPressed() || M5.BtnC.isPressed())
|
||||
{
|
||||
size_t v = M5.Speaker.getVolume();
|
||||
int add = (M5.BtnB.isPressed()) ? -1 : 1;
|
||||
if (M5.BtnA.isHolding())
|
||||
{
|
||||
add = M5.BtnA.getClickCount() ? -1 : 1;
|
||||
}
|
||||
v += add;
|
||||
if (v <= 255)
|
||||
{
|
||||
M5.Speaker.setVolume(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !defined ( ARDUINO )
|
||||
extern "C" {
|
||||
void loopTask(void*)
|
||||
{
|
||||
setup();
|
||||
for (;;) {
|
||||
loop();
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main()
|
||||
{
|
||||
xTaskCreatePinnedToCore(loopTask, "loopTask", 8192, NULL, 1, NULL, 1);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,511 @@
|
|||
#include <SD.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <math.h>
|
||||
|
||||
/// need ESP8266Audio library. ( URL : https://github.com/earlephilhower/ESP8266Audio/ )
|
||||
#include <AudioOutput.h>
|
||||
#include <AudioFileSourceSD.h>
|
||||
#include <AudioFileSourceID3.h>
|
||||
#include <AudioGeneratorMP3.h>
|
||||
|
||||
#include <M5UnitLCD.h>
|
||||
#include <M5UnitOLED.h>
|
||||
#include <M5Unified.h>
|
||||
|
||||
/// set M5Speaker virtual channel (0-7)
|
||||
static constexpr uint8_t m5spk_virtual_channel = 0;
|
||||
|
||||
/// set your mp3 filename
|
||||
static constexpr const char* filename[] =
|
||||
{
|
||||
"/mp3/file01.mp3",
|
||||
"/mp3/file02.mp3",
|
||||
"/mp3/file03.mp3",
|
||||
"/mp3/file04.mp3",
|
||||
};
|
||||
static constexpr const size_t filecount = sizeof(filename) / sizeof(filename[0]);
|
||||
|
||||
class AudioOutputM5Speaker : public AudioOutput
|
||||
{
|
||||
public:
|
||||
AudioOutputM5Speaker(m5::Speaker_Class* m5sound, uint8_t virtual_sound_channel = 0)
|
||||
{
|
||||
_m5sound = m5sound;
|
||||
_virtual_ch = virtual_sound_channel;
|
||||
}
|
||||
virtual ~AudioOutputM5Speaker(void) {};
|
||||
virtual bool begin(void) override { return true; }
|
||||
virtual bool ConsumeSample(int16_t sample[2]) override
|
||||
{
|
||||
if (_tri_buffer_index < tri_buf_size)
|
||||
{
|
||||
_tri_buffer[_tri_index][_tri_buffer_index ] = sample[0];
|
||||
_tri_buffer[_tri_index][_tri_buffer_index+1] = sample[1];
|
||||
_tri_buffer_index += 2;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
flush();
|
||||
return false;
|
||||
}
|
||||
virtual void flush(void) override
|
||||
{
|
||||
if (_tri_buffer_index)
|
||||
{
|
||||
_m5sound->playRaw(_tri_buffer[_tri_index], _tri_buffer_index, hertz, true, 1, _virtual_ch);
|
||||
_tri_index = _tri_index < 2 ? _tri_index + 1 : 0;
|
||||
_tri_buffer_index = 0;
|
||||
}
|
||||
}
|
||||
virtual bool stop(void) override
|
||||
{
|
||||
flush();
|
||||
_m5sound->stop(_virtual_ch);
|
||||
return true;
|
||||
}
|
||||
|
||||
const int16_t* getBuffer(void) const { return _tri_buffer[(_tri_index + 2) % 3]; }
|
||||
|
||||
protected:
|
||||
m5::Speaker_Class* _m5sound;
|
||||
uint8_t _virtual_ch;
|
||||
static constexpr size_t tri_buf_size = 1536;
|
||||
int16_t _tri_buffer[3][tri_buf_size];
|
||||
size_t _tri_buffer_index = 0;
|
||||
size_t _tri_index = 0;
|
||||
};
|
||||
|
||||
|
||||
#define FFT_SIZE 256
|
||||
class fft_t
|
||||
{
|
||||
float _wr[FFT_SIZE + 1];
|
||||
float _wi[FFT_SIZE + 1];
|
||||
float _fr[FFT_SIZE + 1];
|
||||
float _fi[FFT_SIZE + 1];
|
||||
uint16_t _br[FFT_SIZE + 1];
|
||||
size_t _ie;
|
||||
|
||||
public:
|
||||
fft_t(void)
|
||||
{
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.141592653
|
||||
#endif
|
||||
_ie = logf( (float)FFT_SIZE ) / log(2.0) + 0.5;
|
||||
static constexpr float omega = 2.0f * M_PI / FFT_SIZE;
|
||||
static constexpr int s4 = FFT_SIZE / 4;
|
||||
static constexpr int s2 = FFT_SIZE / 2;
|
||||
for ( int i = 1 ; i < s4 ; ++i)
|
||||
{
|
||||
float f = cosf(omega * i);
|
||||
_wi[s4 + i] = f;
|
||||
_wi[s4 - i] = f;
|
||||
_wr[ i] = f;
|
||||
_wr[s2 - i] = -f;
|
||||
}
|
||||
_wi[s4] = _wr[0] = 1;
|
||||
|
||||
size_t je = 1;
|
||||
_br[0] = 0;
|
||||
_br[1] = FFT_SIZE / 2;
|
||||
for ( size_t i = 0 ; i < _ie - 1 ; ++i )
|
||||
{
|
||||
_br[ je << 1 ] = _br[ je ] >> 1;
|
||||
je = je << 1;
|
||||
for ( size_t j = 1 ; j < je ; ++j )
|
||||
{
|
||||
_br[je + j] = _br[je] + _br[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void exec(const int16_t* in)
|
||||
{
|
||||
memset(_fi, 0, sizeof(_fi));
|
||||
for ( size_t j = 0 ; j < FFT_SIZE / 2 ; ++j )
|
||||
{
|
||||
float basej = 0.25 * (1.0-_wr[j]);
|
||||
size_t r = FFT_SIZE - j - 1;
|
||||
|
||||
/// perform han window and stereo to mono convert.
|
||||
_fr[_br[j]] = basej * (in[j * 2] + in[j * 2 + 1]);
|
||||
_fr[_br[r]] = basej * (in[r * 2] + in[r * 2 + 1]);
|
||||
}
|
||||
|
||||
size_t s = 1;
|
||||
size_t i = 0;
|
||||
do
|
||||
{
|
||||
size_t ke = s;
|
||||
s <<= 1;
|
||||
size_t je = FFT_SIZE / s;
|
||||
size_t j = 0;
|
||||
do
|
||||
{
|
||||
size_t k = 0;
|
||||
do
|
||||
{
|
||||
size_t l = s * j + k;
|
||||
size_t m = ke * (2 * j + 1) + k;
|
||||
size_t p = je * k;
|
||||
float Wxmr = _fr[m] * _wr[p] + _fi[m] * _wi[p];
|
||||
float Wxmi = _fi[m] * _wr[p] - _fr[m] * _wi[p];
|
||||
_fr[m] = _fr[l] - Wxmr;
|
||||
_fi[m] = _fi[l] - Wxmi;
|
||||
_fr[l] += Wxmr;
|
||||
_fi[l] += Wxmi;
|
||||
} while ( ++k < ke) ;
|
||||
} while ( ++j < je );
|
||||
} while ( ++i < _ie );
|
||||
}
|
||||
|
||||
uint32_t get(size_t index)
|
||||
{
|
||||
return (index < FFT_SIZE / 2) ? (uint32_t)sqrtf(_fr[ index ] * _fr[ index ] + _fi[ index ] * _fi[ index ]) : 0u;
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr size_t WAVE_SIZE = 320;
|
||||
static AudioFileSourceSD file;
|
||||
static AudioOutputM5Speaker out(&M5.Speaker, m5spk_virtual_channel);
|
||||
static AudioGeneratorMP3 mp3;
|
||||
static AudioFileSourceID3* id3 = nullptr;
|
||||
static fft_t fft;
|
||||
static bool fft_enabled = false;
|
||||
static bool wave_enabled = false;
|
||||
static uint16_t prev_y[(FFT_SIZE / 2)+1];
|
||||
static uint16_t peak_y[(FFT_SIZE / 2)+1];
|
||||
static int16_t wave_y[WAVE_SIZE];
|
||||
static int16_t wave_h[WAVE_SIZE];
|
||||
static int16_t raw_data[WAVE_SIZE * 2];
|
||||
static int header_height = 0;
|
||||
static size_t fileindex = 0;
|
||||
|
||||
void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
|
||||
{
|
||||
(void)cbData;
|
||||
if (string[0] == 0) { return; }
|
||||
if (strcmp(type, "eof") == 0)
|
||||
{
|
||||
M5.Display.display();
|
||||
return;
|
||||
}
|
||||
int y = M5.Display.getCursorY();
|
||||
if (y+1 >= header_height) { return; }
|
||||
M5.Display.fillRect(0, y, M5.Display.width(), 12, M5.Display.getBaseColor());
|
||||
M5.Display.printf("%s: %s", type, string);
|
||||
M5.Display.setCursor(0, y+12);
|
||||
}
|
||||
|
||||
void stop(void)
|
||||
{
|
||||
if (id3 == nullptr) return;
|
||||
out.stop();
|
||||
mp3.stop();
|
||||
id3->RegisterMetadataCB(nullptr, nullptr);
|
||||
id3->close();
|
||||
file.close();
|
||||
delete id3;
|
||||
id3 = nullptr;
|
||||
}
|
||||
|
||||
void play(const char* fname)
|
||||
{
|
||||
if (id3 != nullptr) { stop(); }
|
||||
M5.Display.setCursor(0, 8);
|
||||
file.open(fname);
|
||||
id3 = new AudioFileSourceID3(&file);
|
||||
id3->RegisterMetadataCB(MDCallback, (void*)"ID3TAG");
|
||||
id3->open(fname);
|
||||
mp3.begin(id3, &out);
|
||||
}
|
||||
|
||||
uint32_t bgcolor(LGFX_Device* gfx, int y)
|
||||
{
|
||||
auto h = gfx->height();
|
||||
auto dh = h - header_height;
|
||||
int v = ((h - y)<<5) / dh;
|
||||
if (dh > 44)
|
||||
{
|
||||
int v2 = ((h - y - 1)<<5) / dh;
|
||||
if ((v >> 2) != (v2 >> 2))
|
||||
{
|
||||
return 0x666666u;
|
||||
}
|
||||
}
|
||||
return gfx->color888(v + 2, v, v + 6);
|
||||
}
|
||||
|
||||
void gfxSetup(LGFX_Device* gfx)
|
||||
{
|
||||
if (gfx == nullptr) { return; }
|
||||
if (gfx->width() < gfx->height())
|
||||
{
|
||||
gfx->setRotation(gfx->getRotation()^1);
|
||||
}
|
||||
gfx->setFont(&fonts::lgfxJapanGothic_12);
|
||||
gfx->setEpdMode(epd_mode_t::epd_fastest);
|
||||
gfx->setCursor(0, 8);
|
||||
gfx->println("MP3 player");
|
||||
gfx->setTextWrap(false);
|
||||
gfx->fillRect(0, 6, gfx->width(), 2, TFT_BLACK);
|
||||
|
||||
header_height = 45;
|
||||
fft_enabled = !gfx->isEPD();
|
||||
if (fft_enabled)
|
||||
{
|
||||
wave_enabled = (gfx->getBoard() != m5gfx::board_M5UnitLCD);
|
||||
|
||||
for (int y = header_height; y < gfx->height(); ++y)
|
||||
{
|
||||
gfx->drawFastHLine(0, y, gfx->width(), bgcolor(gfx, y));
|
||||
}
|
||||
}
|
||||
|
||||
for (int x = 0; x < (FFT_SIZE/2)+1; ++x)
|
||||
{
|
||||
prev_y[x] = INT16_MAX;
|
||||
peak_y[x] = INT16_MAX;
|
||||
}
|
||||
for (int x = 0; x < WAVE_SIZE; ++x)
|
||||
{
|
||||
wave_y[x] = gfx->height();
|
||||
wave_h[x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void gfxLoop(LGFX_Device* gfx)
|
||||
{
|
||||
if (gfx == nullptr) { return; }
|
||||
|
||||
if (!gfx->displayBusy())
|
||||
{ // draw volume bar
|
||||
static int px;
|
||||
uint8_t v = M5.Speaker.getVolume();
|
||||
int x = v * (gfx->width()) >> 8;
|
||||
if (px != x)
|
||||
{
|
||||
gfx->fillRect(x, 6, px - x, 2, px < x ? 0xAAFFAAu : 0u);
|
||||
gfx->display();
|
||||
px = x;
|
||||
}
|
||||
}
|
||||
|
||||
if (fft_enabled && !gfx->displayBusy() && M5.Speaker.isPlaying(m5spk_virtual_channel) > 1)
|
||||
{
|
||||
static int prev_x[2];
|
||||
static int peak_x[2];
|
||||
|
||||
auto buf = out.getBuffer();
|
||||
if (buf)
|
||||
{
|
||||
memcpy(raw_data, buf, WAVE_SIZE * 2 * sizeof(int16_t)); // stereo data copy
|
||||
gfx->startWrite();
|
||||
|
||||
// draw stereo level meter
|
||||
for (size_t i = 0; i < 2; ++i)
|
||||
{
|
||||
int32_t level = 0;
|
||||
for (size_t j = i; j < 640; j += 32)
|
||||
{
|
||||
uint32_t lv = abs(raw_data[j]);
|
||||
if (level < lv) { level = lv; }
|
||||
}
|
||||
|
||||
int32_t x = (level * gfx->width()) / INT16_MAX;
|
||||
int32_t px = prev_x[i];
|
||||
if (px != x)
|
||||
{
|
||||
gfx->fillRect(x, i * 3, px - x, 2, px < x ? 0xFF9900u : 0x330000u);
|
||||
prev_x[i] = x;
|
||||
}
|
||||
px = peak_x[i];
|
||||
if (px > x)
|
||||
{
|
||||
gfx->writeFastVLine(px, i * 3, 2, TFT_BLACK);
|
||||
px--;
|
||||
}
|
||||
else
|
||||
{
|
||||
px = x;
|
||||
}
|
||||
if (peak_x[i] != px)
|
||||
{
|
||||
peak_x[i] = px;
|
||||
gfx->writeFastVLine(px, i * 3, 2, TFT_WHITE);
|
||||
}
|
||||
}
|
||||
gfx->display();
|
||||
|
||||
// draw FFT level meter
|
||||
fft.exec(raw_data);
|
||||
size_t bw = gfx->width() / 60;
|
||||
if (bw < 3) { bw = 3; }
|
||||
int32_t dsp_height = gfx->height();
|
||||
int32_t fft_height = dsp_height - header_height - 1;
|
||||
size_t xe = gfx->width() / bw;
|
||||
if (xe > (FFT_SIZE/2)) { xe = (FFT_SIZE/2); }
|
||||
int32_t wave_next = ((header_height + dsp_height) >> 1) + (((256 - (raw_data[0] + raw_data[1])) * fft_height) >> 17);
|
||||
|
||||
uint32_t bar_color[2] = { 0x000033u, 0x99AAFFu };
|
||||
|
||||
for (size_t bx = 0; bx <= xe; ++bx)
|
||||
{
|
||||
size_t x = bx * bw;
|
||||
if ((x & 7) == 0) { gfx->display(); taskYIELD(); }
|
||||
int32_t f = fft.get(bx);
|
||||
int32_t y = (f * fft_height) >> 18;
|
||||
if (y > fft_height) { y = fft_height; }
|
||||
y = dsp_height - y;
|
||||
int32_t py = prev_y[bx];
|
||||
if (y != py)
|
||||
{
|
||||
gfx->fillRect(x, y, bw - 1, py - y, bar_color[(y < py)]);
|
||||
prev_y[bx] = y;
|
||||
}
|
||||
py = peak_y[bx] + 1;
|
||||
if (py < y)
|
||||
{
|
||||
gfx->writeFastHLine(x, py - 1, bw - 1, bgcolor(gfx, py - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
py = y - 1;
|
||||
}
|
||||
if (peak_y[bx] != py)
|
||||
{
|
||||
peak_y[bx] = py;
|
||||
gfx->writeFastHLine(x, py, bw - 1, TFT_WHITE);
|
||||
}
|
||||
|
||||
|
||||
if (wave_enabled)
|
||||
{
|
||||
for (size_t bi = 0; bi < bw; ++bi)
|
||||
{
|
||||
size_t i = x + bi;
|
||||
if (i >= gfx->width() || i >= WAVE_SIZE) { break; }
|
||||
y = wave_y[i];
|
||||
int32_t h = wave_h[i];
|
||||
bool use_bg = (bi+1 == bw);
|
||||
if (h>0)
|
||||
{ /// erase previous wave.
|
||||
gfx->setAddrWindow(i, y, 1, h);
|
||||
h += y;
|
||||
do
|
||||
{
|
||||
uint32_t bg = (use_bg || y < peak_y[bx]) ? bgcolor(gfx, y)
|
||||
: (y == peak_y[bx]) ? 0xFFFFFFu
|
||||
: bar_color[(y >= prev_y[bx])];
|
||||
gfx->writeColor(bg, 1);
|
||||
} while (++y < h);
|
||||
}
|
||||
size_t i2 = i << 1;
|
||||
int32_t y1 = wave_next;
|
||||
wave_next = ((header_height + dsp_height) >> 1) + (((256 - (raw_data[i2] + raw_data[i2 + 1])) * fft_height) >> 17);
|
||||
int32_t y2 = wave_next;
|
||||
if (y1 > y2)
|
||||
{
|
||||
int32_t tmp = y1;
|
||||
y1 = y2;
|
||||
y2 = tmp;
|
||||
}
|
||||
y = y1;
|
||||
h = y2 + 1 - y;
|
||||
wave_y[i] = y;
|
||||
wave_h[i] = h;
|
||||
if (h>0)
|
||||
{ /// draw new wave.
|
||||
gfx->setAddrWindow(i, y, 1, h);
|
||||
h += y;
|
||||
do
|
||||
{
|
||||
uint32_t bg = (y < prev_y[bx]) ? 0xFFCC33u : 0xFFFFFFu;
|
||||
gfx->writeColor(bg, 1);
|
||||
} while (++y < h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx->display();
|
||||
gfx->endWrite();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup(void)
|
||||
{
|
||||
auto cfg = M5.config();
|
||||
|
||||
// If you want to play sound from ModuleDisplay, write this
|
||||
// cfg.external_speaker.module_display = true;
|
||||
|
||||
// If you want to play sound from ModuleRCA, write this
|
||||
// cfg.external_speaker.module_rca = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker, write this
|
||||
cfg.external_speaker.hat_spk = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker2, write this
|
||||
// cfg.external_speaker.hat_spk2 = true;
|
||||
|
||||
// If you want to play sound from ATOMIC Speaker, write this
|
||||
cfg.external_speaker.atomic_spk = true;
|
||||
|
||||
M5.begin(cfg);
|
||||
|
||||
|
||||
{ /// custom setting
|
||||
auto spk_cfg = M5.Speaker.config();
|
||||
/// Increasing the sample_rate will improve the sound quality instead of increasing the CPU load.
|
||||
spk_cfg.sample_rate = 96000; // default:64000 (64kHz) e.g. 48000 , 50000 , 80000 , 96000 , 100000 , 128000 , 144000 , 192000 , 200000
|
||||
M5.Speaker.config(spk_cfg);
|
||||
}
|
||||
|
||||
|
||||
M5.Speaker.begin();
|
||||
|
||||
while (false == SD.begin(GPIO_NUM_4, SPI, 25000000))
|
||||
{
|
||||
M5.delay(500);
|
||||
}
|
||||
|
||||
gfxSetup(&M5.Display);
|
||||
|
||||
play(filename[fileindex]);
|
||||
}
|
||||
|
||||
void loop(void)
|
||||
{
|
||||
gfxLoop(&M5.Display);
|
||||
|
||||
if (mp3.isRunning())
|
||||
{
|
||||
if (!mp3.loop()) { mp3.stop(); }
|
||||
}
|
||||
else
|
||||
{
|
||||
M5.delay(1);
|
||||
}
|
||||
|
||||
M5.update();
|
||||
if (M5.BtnA.wasClicked())
|
||||
{
|
||||
M5.Speaker.tone(1000, 100);
|
||||
stop();
|
||||
if (++fileindex >= filecount) { fileindex = 0; }
|
||||
play(filename[fileindex]);
|
||||
}
|
||||
else
|
||||
if (M5.BtnA.isHolding() || M5.BtnB.isPressed() || M5.BtnC.isPressed())
|
||||
{
|
||||
size_t v = M5.Speaker.getVolume();
|
||||
if (M5.BtnB.isPressed()) { --v; } else { ++v; }
|
||||
if (v <= 255 || M5.BtnA.isHolding())
|
||||
{
|
||||
M5.Speaker.setVolume(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
777
libraries/M5Unified/examples/Advanced/Mic_FFT/Mic_FFT.ino
Normal file
777
libraries/M5Unified/examples/Advanced/Mic_FFT/Mic_FFT.ino
Normal file
|
|
@ -0,0 +1,777 @@
|
|||
// External displays can be enabled if necessary
|
||||
// #include <M5ModuleDisplay.h>
|
||||
// #include <M5AtomDisplay.h>
|
||||
// #include <M5UnitGLASS2.h>
|
||||
// #include <M5UnitOLED.h>
|
||||
// #include <M5UnitLCD.h>
|
||||
|
||||
#include <M5Unified.h>
|
||||
|
||||
#include <set>
|
||||
#include <map>
|
||||
|
||||
static void* memory_alloc(size_t size)
|
||||
{
|
||||
return heap_caps_malloc(size, MALLOC_CAP_8BIT);
|
||||
}
|
||||
|
||||
static void memory_free(void* ptr)
|
||||
{
|
||||
heap_caps_free(ptr);
|
||||
}
|
||||
|
||||
struct wav_data_t {
|
||||
int16_t* wav = nullptr;
|
||||
size_t length = 0;
|
||||
size_t latest_index = 0;
|
||||
|
||||
size_t searchEdge(size_t offset, size_t search_length) const {
|
||||
int mem_position = latest_index + offset;
|
||||
if (mem_position >= length) { mem_position -= length; }
|
||||
int mem_difference = 0;
|
||||
int position = - 1;
|
||||
uint32_t counter[2] = { 0,0 };
|
||||
bool prev_sign = false;
|
||||
|
||||
for (size_t i = 0; i < search_length; ++i) {
|
||||
size_t idx = latest_index + i;
|
||||
if (idx >= length) { idx -= length; }
|
||||
int value = wav[idx];
|
||||
bool sign = value < 0;
|
||||
if (prev_sign != sign) {
|
||||
prev_sign = sign;
|
||||
if (sign) { // When changing from positive to negative
|
||||
if (position >= 0) {
|
||||
int diff = counter[0] + counter[1];
|
||||
if (mem_difference < diff) {
|
||||
mem_difference = diff;
|
||||
mem_position = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
counter[sign] = 0;
|
||||
if (i >= offset) {
|
||||
int pidx = (idx ? idx : length) - 1;
|
||||
int cv = abs(wav[idx]);
|
||||
int pv = abs(wav[pidx]);
|
||||
position = (cv < pv) ? idx : pidx;
|
||||
}
|
||||
}
|
||||
uint32_t v = value * value;
|
||||
counter[sign] += v >> 7;
|
||||
}
|
||||
mem_position -= offset;
|
||||
if (mem_position < 0) { mem_position += length; }
|
||||
return mem_position;
|
||||
}
|
||||
};
|
||||
|
||||
struct fft_data_t {
|
||||
float *fdata = nullptr;
|
||||
size_t length = 0;
|
||||
size_t sample_rate;
|
||||
|
||||
wav_data_t *wav_data = nullptr;
|
||||
uint8_t fft_size_bits = 0;
|
||||
|
||||
float getDataByPixel(uint16_t x, uint16_t width) const {
|
||||
if (length <= width) {
|
||||
int index = x * length / width;
|
||||
if (index >= length) { index = length - 1; }
|
||||
return fdata[index];
|
||||
}
|
||||
int index0 = x * length / width;
|
||||
int index1 = (x + 1) * length / width;
|
||||
if (index0 >= length) { index0 = length - 1; }
|
||||
if (index1 >= length) { index1 = length - 1; }
|
||||
float value = 0;
|
||||
for (int i = index0; i < index1; ++i) {
|
||||
if (value < fdata[i]) {
|
||||
value = fdata[i];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
class fft_function_t {
|
||||
public:
|
||||
bool setup(uint8_t max_fft_size_bits)
|
||||
{
|
||||
close();
|
||||
int FFT_SIZE = 1 << max_fft_size_bits;
|
||||
int need_size = 1+(FFT_SIZE * 3 / 4) * sizeof(float) + (FFT_SIZE * sizeof(uint16_t));
|
||||
_work_area = memory_alloc(need_size);
|
||||
if (_work_area == nullptr) {
|
||||
return false;
|
||||
}
|
||||
_max_fft_size_bits = max_fft_size_bits;
|
||||
_initialized_fft_size_bits = 0;
|
||||
|
||||
fi = (float*)memory_alloc(sizeof(float) * FFT_SIZE + 1);
|
||||
fr = (float*)memory_alloc(sizeof(float) * FFT_SIZE + 1);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void close(void)
|
||||
{
|
||||
if (_work_area) {
|
||||
memory_free(_work_area);
|
||||
_work_area = nullptr;
|
||||
}
|
||||
_max_fft_size_bits = 0;
|
||||
_initialized_fft_size_bits = 0;
|
||||
}
|
||||
|
||||
__attribute((optimize("-O3")))
|
||||
bool update(fft_data_t* fft_data)
|
||||
{
|
||||
if (_work_area == nullptr || fft_data == nullptr || fft_data->fdata == nullptr || fft_data->fft_size_bits == 0) {
|
||||
return false;
|
||||
}
|
||||
if (_initialized_fft_size_bits != fft_data->fft_size_bits) {
|
||||
if (!_init(fft_data->fft_size_bits)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
int FFT_SIZE = 1 << fft_data->fft_size_bits;
|
||||
|
||||
uint16_t *br = (uint16_t*)_work_area;
|
||||
{
|
||||
auto src = fft_data->wav_data->wav;
|
||||
for (int i = 0; i < FFT_SIZE; ++i)
|
||||
{
|
||||
float lv = src[i];
|
||||
fr[br[i]] = lv;
|
||||
}
|
||||
memset(fi, 0, sizeof(float) * FFT_SIZE);
|
||||
}
|
||||
int s4 = FFT_SIZE / 4;
|
||||
|
||||
float *wi = (float*) (&br[FFT_SIZE]);
|
||||
size_t s = 1;
|
||||
size_t i = 0;
|
||||
size_t je = FFT_SIZE;
|
||||
do
|
||||
{
|
||||
size_t ke = s;
|
||||
s <<= 1;
|
||||
je >>= 1;
|
||||
size_t j = 0;
|
||||
do
|
||||
{
|
||||
size_t k = 0;
|
||||
size_t m = ke * ((j << 1) + 1);
|
||||
size_t l = s * j;
|
||||
auto frm_p = &fr[m];
|
||||
auto fim_p = &fi[m];
|
||||
auto frl_p = &fr[l];
|
||||
auto fil_p = &fi[l];
|
||||
auto wi_p = &wi[0];
|
||||
auto wr_p = &wi[s4];
|
||||
do
|
||||
{
|
||||
// size_t p = je * k;
|
||||
float wi = *wi_p;
|
||||
float wr = *wr_p;
|
||||
float frm = *frm_p;
|
||||
float fim = *fim_p;
|
||||
float Wxmr = frm * wr + fim * wi;
|
||||
float Wxmi = fim * wr - frm * wi;
|
||||
float frl = *frl_p;
|
||||
float fil = *fil_p;
|
||||
*frm_p++ = frl - Wxmr;
|
||||
*frl_p++ = frl + Wxmr;
|
||||
*fim_p++ = fil - Wxmi;
|
||||
*fil_p++ = fil + Wxmi;
|
||||
wi_p += je;
|
||||
wr_p += je;
|
||||
} while ( ++k < ke);
|
||||
} while ( ++j < je );
|
||||
} while ( ++i < _initialized_fft_size_bits );
|
||||
|
||||
|
||||
int loop_end = fft_data->length;
|
||||
if (loop_end > FFT_SIZE) {
|
||||
loop_end = FFT_SIZE;
|
||||
}
|
||||
|
||||
float max_value = 0.0f;
|
||||
for (uint32_t i = 0; i < loop_end; ++i) {
|
||||
float vr = fr[i];
|
||||
float vi = fi[i];
|
||||
float v = sqrtf(vr * vr + vi * vi);
|
||||
fft_data->fdata[i] = v;
|
||||
if (max_value < v) { max_value = v; }
|
||||
}
|
||||
|
||||
// peak level adjust
|
||||
float k = (65536.0f) / max_value;
|
||||
if (k > 0.03125f) { k = 0.03125f; } else
|
||||
{
|
||||
for (uint32_t i = 0; i < loop_end; ++i) {
|
||||
float tmp = fft_data->fdata[i];
|
||||
tmp *= k;
|
||||
fft_data->fdata[i] = tmp;
|
||||
}
|
||||
}
|
||||
//*/
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
private:
|
||||
bool _init(uint8_t size_bits)
|
||||
{
|
||||
if (_max_fft_size_bits < size_bits) { return false; }
|
||||
_initialized_fft_size_bits = size_bits;
|
||||
|
||||
uint16_t *br = (uint16_t*)_work_area;
|
||||
|
||||
int FFT_SIZE = 1 << size_bits;
|
||||
float *wi = (float*) (&br[FFT_SIZE]);
|
||||
|
||||
size_t je = 1;
|
||||
br[0] = 0;
|
||||
br[1] = FFT_SIZE >> 1;
|
||||
for ( size_t i = 0 ; i < size_bits - 1 ; ++i )
|
||||
{
|
||||
br[ je << 1 ] = br[ je ] >> 1;
|
||||
je = je << 1;
|
||||
for ( size_t j = 1 ; j < je ; ++j )
|
||||
{
|
||||
br[je + j] = br[je] + br[j];
|
||||
}
|
||||
}
|
||||
|
||||
float omega = 2.0f * M_PI / FFT_SIZE;
|
||||
int s2 = FFT_SIZE >> 1;
|
||||
int s4 = FFT_SIZE >> 2;
|
||||
wi[0] = 0;
|
||||
wi[s2] = 0;
|
||||
wi[s4] = 1;
|
||||
for ( int i = 1 ; i < s4 ; ++i)
|
||||
{
|
||||
float f = cosf(omega * i);
|
||||
wi[s4 + i] = f;
|
||||
wi[s4 - i] = f;
|
||||
wi[s4+s2 - i] = -f;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
float* fi;
|
||||
float* fr;
|
||||
void* _work_area = nullptr;
|
||||
uint8_t _max_fft_size_bits = 0;
|
||||
uint8_t _initialized_fft_size_bits;
|
||||
};
|
||||
|
||||
|
||||
struct rect_t
|
||||
{
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t w;
|
||||
int16_t h;
|
||||
};
|
||||
|
||||
|
||||
class wav_drawer_t
|
||||
{
|
||||
LGFX_Device* _gfx = nullptr;
|
||||
int16_t* prev_y = nullptr;
|
||||
int16_t* prev_h = nullptr;
|
||||
rect_t draw_rect = {0, 0, 0, 0};
|
||||
uint32_t bg_color = 0x000000u;
|
||||
uint32_t fg_color = 0xFFFFFFu;
|
||||
uint32_t line_color = 0x303030u;
|
||||
|
||||
public:
|
||||
bool setup(LGFX_Device* gfx, const rect_t& rect)
|
||||
{
|
||||
if (gfx == nullptr) { return false; }
|
||||
_gfx = gfx;
|
||||
draw_rect = rect;
|
||||
gfx->fillRect(rect.x, rect.y, rect.w, rect.h, bg_color);
|
||||
gfx->drawFastVLine(rect.x + (rect.w >> 1), rect.y, rect.h, line_color);
|
||||
int width = rect.w;
|
||||
prev_y = (int16_t*)memory_alloc(width * sizeof(int16_t));
|
||||
prev_h = (int16_t*)memory_alloc(width * sizeof(int16_t));
|
||||
memset(prev_y, 0, width * sizeof(int16_t));
|
||||
memset(prev_h, 0, width * sizeof(int16_t));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update(const wav_data_t& wav_data)
|
||||
{
|
||||
auto gfx = _gfx;
|
||||
|
||||
int32_t width = draw_rect.w;
|
||||
int32_t height = draw_rect.h;
|
||||
|
||||
int wav_count = wav_data.length;
|
||||
int wav_index = wav_data.searchEdge(draw_rect.w >> 1, wav_count / 3);
|
||||
auto wav = wav_data.wav;
|
||||
|
||||
int32_t max_value = 1;
|
||||
auto wi = wav_index;
|
||||
for (int i = 0; i < width; ++i)
|
||||
{
|
||||
int32_t tmp = abs(wav[wi]);
|
||||
if (max_value < tmp) { max_value = tmp; }
|
||||
if (++wi >= wav_count) { wi = 0; }
|
||||
}
|
||||
int new_k = (draw_rect.h << 15) / max_value;
|
||||
if (new_k > 65536)
|
||||
{ new_k = 65536; }
|
||||
static int k;
|
||||
if (k > new_k) { k = new_k; }
|
||||
else {
|
||||
k = (k * 127 + new_k) >> 7;
|
||||
}
|
||||
int32_t value1 = (32768-wav[wav_index] * k) >> 16;
|
||||
int32_t value2 = value1;
|
||||
int32_t base_y = draw_rect.y + (height >> 1);
|
||||
|
||||
for (int i = 0; i < width; ++i)
|
||||
{
|
||||
if (++wav_index >= wav_count) { wav_index = 0; }
|
||||
int32_t x = i + draw_rect.x;
|
||||
int32_t y = prev_y[i];
|
||||
int32_t h = prev_h[i];
|
||||
|
||||
gfx->setColor(i == (width >> 1) ? line_color : bg_color);
|
||||
gfx->drawFastVLine(x, base_y + y, h);
|
||||
|
||||
int32_t value0 = value1;
|
||||
value1 = value2;
|
||||
value2 = (32768-wav[wav_index] * k) >> 16;
|
||||
int32_t value_01 = (value0 + value1) >> 1;
|
||||
int32_t value_12 = (value1 + value2) >> 1;
|
||||
|
||||
int32_t y_min = (value_01 < value_12) ? value_01 : value_12;
|
||||
if (y_min > value1) { y_min = value1; }
|
||||
|
||||
int32_t y_max = (value_01 > value_12) ? value_01 : value_12;
|
||||
if (y_max < value1) { y_max = value1; }
|
||||
|
||||
y = y_min;
|
||||
h = y_max + 1 - y;
|
||||
prev_y[i] = y;
|
||||
prev_h[i] = h;
|
||||
gfx->drawPixel(x, base_y, line_color);
|
||||
/// draw new wave.
|
||||
gfx->drawFastVLine(x, base_y + y, h, fg_color);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class fft_drawer_t
|
||||
{
|
||||
LGFX_Device* _gfx = nullptr;
|
||||
|
||||
uint16_t* prev_y = nullptr;
|
||||
uint16_t* prev_h = nullptr;
|
||||
rect_t draw_rect = {0, 0, 0, 0};
|
||||
uint32_t bg_color = 0x000066u;
|
||||
uint32_t fg_color = 0x00FF00u;
|
||||
|
||||
public:
|
||||
bool setup(LGFX_Device* gfx, const rect_t& rect)
|
||||
{
|
||||
if (gfx == nullptr) { return false; }
|
||||
_gfx = gfx;
|
||||
draw_rect = rect;
|
||||
gfx->fillRect(rect.x, rect.y, rect.w, rect.h, bg_color);
|
||||
int width = rect.w;
|
||||
|
||||
prev_y = (uint16_t*)memory_alloc(width * sizeof(int16_t));
|
||||
prev_h = (uint16_t*)memory_alloc(width * sizeof(int16_t));
|
||||
memset(prev_y, 0, width * sizeof(int16_t));
|
||||
memset(prev_h, 0, width * sizeof(int16_t));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update(const fft_data_t& fft_data)
|
||||
{
|
||||
auto gfx = _gfx;
|
||||
|
||||
int32_t width = draw_rect.w;
|
||||
int32_t height = draw_rect.h - 1;
|
||||
|
||||
int32_t value1 = height - (((int32_t)(fft_data.getDataByPixel(0, width) * height)) >> 16);
|
||||
if (value1 < 0) { value1 = 0; }
|
||||
int32_t value2 = value1;
|
||||
|
||||
for (int i = 0; i < width; ++i)
|
||||
{
|
||||
int32_t x = i + draw_rect.x;
|
||||
int32_t y = prev_y[i];
|
||||
int32_t h = prev_h[i];
|
||||
|
||||
gfx->drawFastVLine(x, draw_rect.y + y, h, bg_color);
|
||||
|
||||
int32_t value0 = value1;
|
||||
value1 = value2;
|
||||
value2 = height - (((int32_t)(fft_data.getDataByPixel(i+1, width) * height)) >> 16);
|
||||
if (value2 < 0) { value2 = 0; }
|
||||
|
||||
int32_t value_01 = (value0 + value1) >> 1;
|
||||
int32_t value_12 = (value1 + value2) >> 1;
|
||||
|
||||
int32_t y_min = (value_01 < value_12) ? value_01 : value_12;
|
||||
if (y_min > value1) { y_min = value1; }
|
||||
|
||||
int32_t y_max = (value_01 > value_12) ? value_01 : value_12;
|
||||
if (y_max < value1) { y_max = value1; }
|
||||
|
||||
y = y_min;
|
||||
h = y_max + 1 - y;
|
||||
prev_y[i] = y;
|
||||
prev_h[i] = h;
|
||||
|
||||
gfx->drawFastVLine(x, draw_rect.y + y, h, fg_color);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class fft_peak_t
|
||||
{
|
||||
LGFX_Device* _gfx = nullptr;
|
||||
|
||||
rect_t draw_rect = {0, 0, 0, 0};
|
||||
uint32_t bg_color = 0x000000u;
|
||||
uint32_t fg_color = 0x00FFFFu;
|
||||
std::set<uint16_t> peak_index_set;
|
||||
uint16_t prev_peak_index = UINT16_MAX;
|
||||
char text_buf[10] = {0,};
|
||||
char prev_text[10] = {0,};
|
||||
uint8_t step = 0;
|
||||
public:
|
||||
bool setup(LGFX_Device* gfx, const rect_t& rect)
|
||||
{
|
||||
if (gfx == nullptr) { return false; }
|
||||
_gfx = gfx;
|
||||
draw_rect = rect;
|
||||
gfx->fillRect(rect.x, rect.y, rect.w, rect.h, bg_color);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update(const fft_data_t& fft_data)
|
||||
{
|
||||
auto gfx = _gfx;
|
||||
|
||||
auto fdata = fft_data.fdata;
|
||||
|
||||
int32_t loop_end = fft_data.length;
|
||||
|
||||
auto boarder_value = (uint16_t*)memory_alloc(sizeof(uint16_t) * loop_end);
|
||||
memset(boarder_value, 0, sizeof(uint16_t) * loop_end);
|
||||
{
|
||||
float thresh = 0;
|
||||
for (int i = 0; i < loop_end; ++i)
|
||||
{
|
||||
float value = fdata[i];
|
||||
if (thresh < value) {
|
||||
thresh = value;
|
||||
}
|
||||
boarder_value[i] = thresh;
|
||||
thresh = thresh * 0.9f;
|
||||
}
|
||||
for (int i = loop_end - 1; i >= 0; --i)
|
||||
{
|
||||
int value = fdata[i];
|
||||
if (thresh < value) {
|
||||
thresh = value;
|
||||
}
|
||||
if (boarder_value[i] < thresh) {
|
||||
boarder_value[i] = thresh;
|
||||
}
|
||||
thresh = thresh * 0.9f;
|
||||
}
|
||||
}
|
||||
static constexpr const size_t peak_index_set_size = 8;
|
||||
std::multimap<float, uint16_t> peak_map;
|
||||
for (int i = 0; i < loop_end; ++i)
|
||||
{
|
||||
float value = fdata[i];
|
||||
if (value >= boarder_value[i]) {
|
||||
peak_map.insert(std::make_pair(value, i));
|
||||
if (peak_map.size() > peak_index_set_size) {
|
||||
peak_map.erase(peak_map.begin());
|
||||
}
|
||||
}
|
||||
}
|
||||
memory_free(boarder_value);
|
||||
if (peak_map.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 8; ++i)
|
||||
{
|
||||
auto c = text_buf[step];
|
||||
if (prev_text[step] != c) {
|
||||
prev_text[step] = c;
|
||||
gfx->drawChar(c, draw_rect.x + (step * draw_rect.w >> 3), draw_rect.y);
|
||||
break;
|
||||
}
|
||||
if (++step >= 8)
|
||||
{
|
||||
step = 0;
|
||||
uint16_t peak_index = peak_map.rbegin()->second;
|
||||
if (prev_peak_index != peak_index) {
|
||||
prev_peak_index = peak_index;
|
||||
gfx->setTextDatum(m5gfx::datum_t::top_right);
|
||||
float freq = (float)(fft_data.sample_rate * peak_index) / (float)(1<<fft_data.fft_size_bits);
|
||||
snprintf(text_buf, sizeof(text_buf), "%8.1f", freq);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int width = draw_rect.w;
|
||||
int icon_size = peak_index_set_size;
|
||||
for (auto peak_index : peak_index_set)
|
||||
{
|
||||
int x = draw_rect.x + (peak_index * width / fft_data.length);
|
||||
int y = draw_rect.y + draw_rect.h - icon_size - 1;
|
||||
gfx->fillRect(x - icon_size, y, icon_size * 2 + 1, icon_size+1, bg_color);
|
||||
}
|
||||
peak_index_set.clear();
|
||||
|
||||
for (auto it = peak_map.rbegin(); it != peak_map.rend(); ++it)
|
||||
{
|
||||
int peak_index = it->second;
|
||||
int x = draw_rect.x + (peak_index * width / fft_data.length);
|
||||
int y = draw_rect.y + draw_rect.h - 1;
|
||||
gfx->drawTriangle(x, y, x - icon_size, y - icon_size, x + icon_size, y - icon_size, fg_color);
|
||||
peak_index_set.insert(peak_index);
|
||||
icon_size -= 2;
|
||||
if (icon_size <= 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class fft_history_t
|
||||
{
|
||||
LGFX_Device* _gfx = nullptr;
|
||||
|
||||
uint8_t* color_map = nullptr;
|
||||
rect_t draw_rect = {0, 0, 0, 0};
|
||||
uint32_t bg_color = 0x000033u;
|
||||
uint32_t fg_color = 0xFFFF00u;
|
||||
int step = 0;
|
||||
public:
|
||||
bool setup(LGFX_Device* gfx, const rect_t& rect)
|
||||
{
|
||||
if (gfx == nullptr) { return false; }
|
||||
_gfx = gfx;
|
||||
draw_rect = rect;
|
||||
int width = rect.w;
|
||||
int height = rect.h;
|
||||
|
||||
color_map = (uint8_t*)memory_alloc(width * height * sizeof(uint8_t));
|
||||
memset(color_map, 0, width * height * sizeof(uint8_t));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool update(const fft_data_t& fft_data)
|
||||
{
|
||||
int32_t width = draw_rect.w;
|
||||
int32_t height = draw_rect.h;
|
||||
|
||||
if (--step < 0) {
|
||||
step = 7;
|
||||
memmove(&color_map[width], color_map, width * (height - 1));
|
||||
memset(color_map, 0, width);
|
||||
}
|
||||
for (int i = 0; i < width; ++i)
|
||||
{
|
||||
int v = fft_data.getDataByPixel(i, width) / 256.0f;
|
||||
if (v > 255) { v = 255; }
|
||||
color_map[i] = color_map[i] > v ? color_map[i] : v;
|
||||
}
|
||||
int y0 = ( step * draw_rect.h) >> 3;
|
||||
int y1 = ((step + 1) * draw_rect.h) >> 3;
|
||||
_gfx->pushGrayscaleImage(draw_rect.x, draw_rect.y + y0, draw_rect.w, y1 - y0, &color_map[y0*width], m5gfx::color_depth_t::grayscale_8bit, fg_color, bg_color);
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
// The higher the sample rate, the higher the frequency results obtained by FFT.
|
||||
// If limited to the audible range, 24kHz to 48kHz is sufficient.
|
||||
static constexpr const size_t SAMPLE_RATE = 24000;
|
||||
// static constexpr const size_t SAMPLE_RATE = 96000;
|
||||
|
||||
// The larger the FFT_BITS, the higher the accuracy of the FFT, but the processing load also increases
|
||||
static constexpr const size_t FFT_BITS = 10;
|
||||
|
||||
// WAVE_BLOCK_SIZE is the size of one processing when capturing data from I2S.
|
||||
// If it is too large, the loop cycle will be slow and the frequency of drawing updates will decrease.
|
||||
// If it is too small, recoding interruptions will occur.
|
||||
// For example, if the sample rate is 96kHz and the block size is 384, data will be captured every 4ms.
|
||||
// Therefore, the drawing process, FFT process, and other loop iterations must be completed within 4ms.
|
||||
static constexpr const size_t WAVE_BLOCK_SIZE = 256;
|
||||
|
||||
static constexpr const size_t FFT_SIZE = 1u << FFT_BITS;
|
||||
static constexpr const size_t WAVE_BLOCK_COUNT = 3 + FFT_SIZE / WAVE_BLOCK_SIZE;
|
||||
static constexpr const size_t WAVE_TOTAL_SIZE = WAVE_BLOCK_SIZE * WAVE_BLOCK_COUNT;
|
||||
static fft_function_t fft_function;
|
||||
static fft_data_t fft_data;
|
||||
static wav_data_t wav_data;
|
||||
static wav_drawer_t wav_drawer;
|
||||
static fft_drawer_t fft_drawer;
|
||||
static fft_peak_t fft_peak;
|
||||
static fft_history_t fft_history;
|
||||
|
||||
void setup(void)
|
||||
{
|
||||
auto cfg = M5.config();
|
||||
|
||||
#if defined ( __M5GFX_M5MODULEDISPLAY__ )
|
||||
cfg.module_display.logical_width = 320;
|
||||
cfg.module_display.logical_height = 180;
|
||||
#endif
|
||||
#if defined ( __M5GFX_M5ATOMDISPLAY__ )
|
||||
cfg.atom_display.logical_width = 320;
|
||||
cfg.atom_display.logical_height = 180;
|
||||
#endif
|
||||
|
||||
// use for ATOMIC ECHO BASE
|
||||
cfg.external_speaker.atomic_echo = true;
|
||||
|
||||
M5.begin(cfg);
|
||||
M5.setPrimaryDisplayType({m5gfx::board_M5ModuleDisplay, m5gfx::board_M5AtomDisplay});
|
||||
M5.Speaker.tone(440, 100);
|
||||
M5.delay(100);
|
||||
{
|
||||
auto cfg = M5.Mic.config();
|
||||
cfg.dma_buf_count = 3;
|
||||
cfg.dma_buf_len = WAVE_BLOCK_SIZE;
|
||||
cfg.over_sampling = 1;
|
||||
cfg.noise_filter_level = 0;
|
||||
cfg.sample_rate = SAMPLE_RATE;
|
||||
cfg.magnification = cfg.use_adc ? 16 : 1;
|
||||
|
||||
/*
|
||||
{ // use for Unit PDM ( Port A )
|
||||
cfg.pin_data_in = M5.getPin(m5::pin_name_t::port_a_pin2);
|
||||
cfg.pin_ws = M5.getPin(m5::pin_name_t::port_a_pin1);
|
||||
cfg.pin_bck = -1;
|
||||
cfg.pin_mck = -1;
|
||||
cfg.use_adc = false;
|
||||
cfg.stereo = false;
|
||||
cfg.i2s_port = i2s_port_t::I2S_NUM_0;
|
||||
}
|
||||
//*/
|
||||
M5.Mic.config(cfg);
|
||||
}
|
||||
|
||||
if (!M5.Mic.isEnabled()) {
|
||||
M5.Display.printf("microphone is not available.");
|
||||
M5_LOGE("microphone is not available.");
|
||||
for (;;) {
|
||||
M5.delay(256);
|
||||
}
|
||||
}
|
||||
M5.Speaker.end();
|
||||
M5.Mic.begin();
|
||||
|
||||
M5.Display.startWrite();
|
||||
|
||||
int16_t w = M5.Display.width();
|
||||
int16_t h = M5.Display.height() >> 2;
|
||||
int16_t y = 0;
|
||||
|
||||
rect_t rect_fft_peak = {0, y, w, h};
|
||||
y += h;
|
||||
rect_t rect_fft_drawer = {0, y, w, h};
|
||||
y += h;
|
||||
rect_t rect_fft_history = {0, y, w, h};
|
||||
y += h;
|
||||
rect_t rect_wav_drawer = {0, y, w, h};
|
||||
|
||||
fft_function.setup(FFT_BITS);
|
||||
fft_peak.setup(&M5.Display, rect_fft_peak);
|
||||
fft_drawer.setup(&M5.Display, rect_fft_drawer);
|
||||
fft_history.setup(&M5.Display, rect_fft_history);
|
||||
wav_drawer.setup(&M5.Display, rect_wav_drawer);
|
||||
|
||||
M5.Display.setTextSize(w / 64.0f, h / 16.0f);
|
||||
M5.Display.setFont(&fonts::AsciiFont8x16);
|
||||
M5.Display.setEpdMode(epd_mode_t::epd_fastest);
|
||||
|
||||
fft_data.fft_size_bits = FFT_BITS;
|
||||
fft_data.sample_rate = SAMPLE_RATE;
|
||||
fft_data.wav_data = &wav_data;
|
||||
fft_data.length = (1 << (fft_data.fft_size_bits - 1)) + 1;
|
||||
fft_data.fdata = (typeof(fft_data.fdata))memory_alloc(fft_data.length * sizeof(fft_data.fdata[0]));
|
||||
|
||||
wav_data.length = WAVE_TOTAL_SIZE;
|
||||
wav_data.wav = (typeof(wav_data.wav))memory_alloc(WAVE_TOTAL_SIZE * sizeof(wav_data.wav[0]));
|
||||
memset(wav_data.wav, 0 , WAVE_TOTAL_SIZE * sizeof(int16_t));
|
||||
}
|
||||
|
||||
void loop(void)
|
||||
{
|
||||
static int step = -1;
|
||||
|
||||
if (M5.Mic.isEnabled())
|
||||
{
|
||||
int wav_idx = wav_data.latest_index;
|
||||
while (M5.Mic.isRecording() < 2) {
|
||||
M5.Mic.record(&(wav_data.wav[wav_idx]), WAVE_BLOCK_SIZE, SAMPLE_RATE, false);
|
||||
wav_idx += WAVE_BLOCK_SIZE;
|
||||
if (wav_idx >= WAVE_TOTAL_SIZE) { wav_idx = 0; }
|
||||
wav_data.latest_index = wav_idx;
|
||||
};
|
||||
|
||||
{
|
||||
switch (++step) {
|
||||
default:
|
||||
step = 0;
|
||||
M5.Display.display();
|
||||
fft_function.update(&fft_data);
|
||||
break;
|
||||
case 1:
|
||||
wav_drawer.update(wav_data);
|
||||
break;
|
||||
case 2:
|
||||
fft_drawer.update(fft_data);
|
||||
break;
|
||||
case 3:
|
||||
fft_history.update(fft_data);
|
||||
break;
|
||||
case 4:
|
||||
fft_peak.update(fft_data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for ESP-IDF
|
||||
extern "C" {
|
||||
__attribute__((weak))
|
||||
void app_main()
|
||||
{
|
||||
setup();
|
||||
for (;;) {
|
||||
loop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
|
||||
#include <M5UnitLCD.h>
|
||||
#include <M5UnitOLED.h>
|
||||
#include <M5Unified.h>
|
||||
|
||||
/// need AquesTalk library. ( URL : https://www.a-quest.com/ )
|
||||
#include <aquestalk.h>
|
||||
|
||||
/// set M5Speaker virtual channel (0-7)
|
||||
static constexpr uint8_t m5spk_virtual_channel = 0;
|
||||
|
||||
static constexpr uint8_t LEN_FRAME = 32;
|
||||
|
||||
static uint32_t workbuf[AQ_SIZE_WORKBUF];
|
||||
static TaskHandle_t task_handle = nullptr;
|
||||
volatile bool is_talking = false;
|
||||
|
||||
static void talk_task(void*)
|
||||
{
|
||||
int16_t wav[3][LEN_FRAME];
|
||||
int tri_index = 0;
|
||||
for (;;)
|
||||
{
|
||||
ulTaskNotifyTake( pdTRUE, portMAX_DELAY ); // wait notify
|
||||
while (is_talking)
|
||||
{
|
||||
uint16_t len;
|
||||
if (CAqTkPicoF_SyntheFrame(wav[tri_index], &len)) { is_talking = false; break; }
|
||||
M5.Speaker.playRaw(wav[tri_index], len, 8000, false, 1, m5spk_virtual_channel, false);
|
||||
tri_index = tri_index < 2 ? tri_index + 1 : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 音声再生の終了を待機する;
|
||||
static void waitAquesTalk(void)
|
||||
{
|
||||
while (is_talking) { vTaskDelay(1); }
|
||||
}
|
||||
|
||||
/// 音声再生を停止する;
|
||||
static void stopAquesTalk(void)
|
||||
{
|
||||
if (is_talking) { is_talking = false; vTaskDelay(1); }
|
||||
}
|
||||
|
||||
/// 音声再生を開始する。(再生中の場合は中断して新たな音声再生を開始する) ;
|
||||
static void playAquesTalk(const char *koe)
|
||||
{
|
||||
stopAquesTalk();
|
||||
|
||||
M5.Display.printf("Play:%s\n", koe);
|
||||
|
||||
int iret = CAqTkPicoF_SetKoe((const uint8_t*)koe, 100, 0xFFu);
|
||||
if (iret) { M5.Display.println("ERR:CAqTkPicoF_SetKoe"); }
|
||||
|
||||
is_talking = true;
|
||||
xTaskNotifyGive(task_handle);
|
||||
}
|
||||
|
||||
void setup(void)
|
||||
{
|
||||
auto cfg = M5.config();
|
||||
|
||||
// If you want to play sound from ModuleDisplay, write this
|
||||
// cfg.external_speaker.module_display = true;
|
||||
|
||||
// If you want to play sound from ModuleRCA, write this
|
||||
// cfg.external_speaker.module_rca = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker, write this
|
||||
cfg.external_speaker.hat_spk = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker2, write this
|
||||
// cfg.external_speaker.hat_spk2 = true;
|
||||
|
||||
// If you want to play sound from ATOMIC Speaker, write this
|
||||
cfg.external_speaker.atomic_spk = true;
|
||||
|
||||
M5.begin(cfg);
|
||||
|
||||
xTaskCreateUniversal(talk_task, "talk_task", 4096, nullptr, 1, &task_handle, APP_CPU_NUM);
|
||||
/*
|
||||
/// Increasing the sample_rate will improve the sound quality instead of increasing the CPU load.
|
||||
auto spk_cfg = M5.Speaker.config();
|
||||
spk_cfg.sample_rate = 96000; // default:64000 (64kHz) e.g. 48000 , 50000 , 80000 , 96000 , 100000 , 128000 , 144000 , 192000 , 200000
|
||||
M5.Speaker.config(spk_cfg);
|
||||
//*/
|
||||
M5.Speaker.setVolume(128);
|
||||
|
||||
M5.Display.setEpdMode(epd_mode_t::epd_fastest);
|
||||
M5.Display.setTextWrap(true);
|
||||
|
||||
int iret = CAqTkPicoF_Init(workbuf, LEN_FRAME, "XXX-XXX-XXX");
|
||||
if (iret) {
|
||||
M5.Display.println("ERR:CAqTkPicoF_Init");
|
||||
}
|
||||
|
||||
playAquesTalk("akue_suto'-_ku/kido-shima'_shita.");
|
||||
waitAquesTalk();
|
||||
playAquesTalk("botanno/o_shitekudasa'i.");
|
||||
}
|
||||
|
||||
void loop(void)
|
||||
{
|
||||
M5.update();
|
||||
|
||||
if ( M5.BtnA.wasClicked()) { playAquesTalk("kuri'kku"); }
|
||||
else if (M5.BtnA.wasHold()) { playAquesTalk("ho'-rudo"); }
|
||||
else if (M5.BtnA.wasReleased()) { playAquesTalk("riri'-su"); }
|
||||
else if (M5.BtnB.wasReleased()) { playAquesTalk("korewa;te'_sutode_su."); }
|
||||
else if (M5.BtnC.wasReleased()) { playAquesTalk("yukkuri_siteittene?"); }
|
||||
}
|
||||
|
||||
#if !defined ( ARDUINO )
|
||||
extern "C" {
|
||||
void loopTask(void*)
|
||||
{
|
||||
setup();
|
||||
for (;;) {
|
||||
loop();
|
||||
}
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void app_main()
|
||||
{
|
||||
xTaskCreateUniversal(loopTask, "loopTask", 8192, NULL, 1, NULL, APP_CPU_NUM);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
#include <SD.h>
|
||||
#include <M5Unified.h>
|
||||
|
||||
#include <esp_log.h>
|
||||
|
||||
static constexpr const gpio_num_t SDCARD_CSPIN = GPIO_NUM_4;
|
||||
|
||||
static constexpr const char* files[] = {
|
||||
"/file1.wav",
|
||||
"/file2.wav",
|
||||
"/file3.wav",
|
||||
};
|
||||
|
||||
static constexpr const size_t buf_num = 3;
|
||||
static constexpr const size_t buf_size = 1024;
|
||||
static uint8_t wav_data[buf_num][buf_size];
|
||||
|
||||
struct __attribute__((packed)) wav_header_t
|
||||
{
|
||||
char RIFF[4];
|
||||
uint32_t chunk_size;
|
||||
char WAVEfmt[8];
|
||||
uint32_t fmt_chunk_size;
|
||||
uint16_t audiofmt;
|
||||
uint16_t channel;
|
||||
uint32_t sample_rate;
|
||||
uint32_t byte_per_sec;
|
||||
uint16_t block_size;
|
||||
uint16_t bit_per_sample;
|
||||
};
|
||||
|
||||
struct __attribute__((packed)) sub_chunk_t
|
||||
{
|
||||
char identifier[4];
|
||||
uint32_t chunk_size;
|
||||
uint8_t data[1];
|
||||
};
|
||||
|
||||
static bool playSdWav(const char* filename)
|
||||
{
|
||||
auto file = SD.open(filename);
|
||||
|
||||
if (!file) { return false; }
|
||||
|
||||
wav_header_t wav_header;
|
||||
file.read((uint8_t*)&wav_header, sizeof(wav_header_t));
|
||||
|
||||
ESP_LOGD("wav", "RIFF : %.4s" , wav_header.RIFF );
|
||||
ESP_LOGD("wav", "chunk_size : %d" , wav_header.chunk_size );
|
||||
ESP_LOGD("wav", "WAVEfmt : %.8s" , wav_header.WAVEfmt );
|
||||
ESP_LOGD("wav", "fmt_chunk_size : %d" , wav_header.fmt_chunk_size);
|
||||
ESP_LOGD("wav", "audiofmt : %d" , wav_header.audiofmt );
|
||||
ESP_LOGD("wav", "channel : %d" , wav_header.channel );
|
||||
ESP_LOGD("wav", "sample_rate : %d" , wav_header.sample_rate );
|
||||
ESP_LOGD("wav", "byte_per_sec : %d" , wav_header.byte_per_sec );
|
||||
ESP_LOGD("wav", "block_size : %d" , wav_header.block_size );
|
||||
ESP_LOGD("wav", "bit_per_sample : %d" , wav_header.bit_per_sample);
|
||||
|
||||
if ( memcmp(wav_header.RIFF, "RIFF", 4)
|
||||
|| memcmp(wav_header.WAVEfmt, "WAVEfmt ", 8)
|
||||
|| wav_header.audiofmt != 1
|
||||
|| wav_header.bit_per_sample < 8
|
||||
|| wav_header.bit_per_sample > 16
|
||||
|| wav_header.channel == 0
|
||||
|| wav_header.channel > 2
|
||||
)
|
||||
{
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seek(offsetof(wav_header_t, audiofmt) + wav_header.fmt_chunk_size);
|
||||
sub_chunk_t sub_chunk;
|
||||
|
||||
file.read((uint8_t*)&sub_chunk, 8);
|
||||
|
||||
ESP_LOGD("wav", "sub id : %.4s" , sub_chunk.identifier);
|
||||
ESP_LOGD("wav", "sub chunk_size : %d" , sub_chunk.chunk_size);
|
||||
|
||||
while(memcmp(sub_chunk.identifier, "data", 4))
|
||||
{
|
||||
if (!file.seek(sub_chunk.chunk_size, SeekMode::SeekCur)) { break; }
|
||||
file.read((uint8_t*)&sub_chunk, 8);
|
||||
|
||||
ESP_LOGD("wav", "sub id : %.4s" , sub_chunk.identifier);
|
||||
ESP_LOGD("wav", "sub chunk_size : %d" , sub_chunk.chunk_size);
|
||||
}
|
||||
|
||||
if (memcmp(sub_chunk.identifier, "data", 4))
|
||||
{
|
||||
file.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
int32_t data_len = sub_chunk.chunk_size;
|
||||
bool flg_16bit = (wav_header.bit_per_sample >> 4);
|
||||
|
||||
size_t idx = 0;
|
||||
while (data_len > 0) {
|
||||
size_t len = data_len < buf_size ? data_len : buf_size;
|
||||
len = file.read(wav_data[idx], len);
|
||||
data_len -= len;
|
||||
|
||||
if (flg_16bit) {
|
||||
M5.Speaker.playRaw((const int16_t*)wav_data[idx], len >> 1, wav_header.sample_rate, wav_header.channel > 1, 1, 0);
|
||||
} else {
|
||||
M5.Speaker.playRaw((const uint8_t*)wav_data[idx], len, wav_header.sample_rate, wav_header.channel > 1, 1, 0);
|
||||
}
|
||||
idx = idx < (buf_num - 1) ? idx + 1 : 0;
|
||||
}
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void setup(void)
|
||||
{
|
||||
M5.begin();
|
||||
|
||||
SD.begin(SDCARD_CSPIN, SPI, 25000000);
|
||||
|
||||
// M5.Speaker.setVolume(32);
|
||||
}
|
||||
|
||||
void loop(void)
|
||||
{
|
||||
for (auto filename : files) {
|
||||
playSdWav(filename);
|
||||
M5.delay(500);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,681 @@
|
|||
#define WIFI_SSID "SET YOUR WIFI SSID"
|
||||
#define WIFI_PASS "SET YOUR WIFI PASS"
|
||||
|
||||
|
||||
#include <WiFi.h>
|
||||
#include <HTTPClient.h>
|
||||
#include <math.h>
|
||||
|
||||
/// need ESP8266Audio library. ( URL : https://github.com/earlephilhower/ESP8266Audio/ )
|
||||
#include <AudioOutput.h>
|
||||
#include <AudioFileSourceICYStream.h>
|
||||
#include <AudioFileSource.h>
|
||||
#include <AudioFileSourceBuffer.h>
|
||||
#include <AudioGeneratorMP3.h>
|
||||
|
||||
#include <M5UnitLCD.h>
|
||||
#include <M5UnitOLED.h>
|
||||
#include <M5Unified.h>
|
||||
|
||||
/// set M5Speaker virtual channel (0-7)
|
||||
static constexpr uint8_t m5spk_virtual_channel = 0;
|
||||
|
||||
/// set web radio station url
|
||||
static constexpr const char* station_list[][2] =
|
||||
{
|
||||
{"thejazzstream" , "http://wbgo.streamguys.net/thejazzstream"},
|
||||
{"181-beatles_128k" , "http://listen.181fm.com/181-beatles_128k.mp3"},
|
||||
{"illstreet-128-mp3" , "http://ice1.somafm.com/illstreet-128-mp3"},
|
||||
{"bootliquor-128-mp3", "http://ice1.somafm.com/bootliquor-128-mp3"},
|
||||
{"dronezone-128-mp3" , "http://ice1.somafm.com/dronezone-128-mp3"},
|
||||
{"Lite Favorites" , "http://naxos.cdnstream.com:80/1255_128"},
|
||||
{"Classic FM" , "http://media-ice.musicradio.com:80/ClassicFMMP3"},
|
||||
{"MAXXED Out" , "http://149.56.195.94:8015/steam"},
|
||||
{"Asia Dream" , "http://igor.torontocast.com:1025/;.-mp3"},
|
||||
};
|
||||
static constexpr const size_t stations = sizeof(station_list) / sizeof(station_list[0]);
|
||||
|
||||
class AudioOutputM5Speaker : public AudioOutput
|
||||
{
|
||||
public:
|
||||
AudioOutputM5Speaker(m5::Speaker_Class* m5sound, uint8_t virtual_sound_channel = 0)
|
||||
{
|
||||
_m5sound = m5sound;
|
||||
_virtual_ch = virtual_sound_channel;
|
||||
}
|
||||
virtual ~AudioOutputM5Speaker(void) {};
|
||||
virtual bool begin(void) override { return true; }
|
||||
virtual bool ConsumeSample(int16_t sample[2]) override
|
||||
{
|
||||
if (_tri_buffer_index < tri_buf_size)
|
||||
{
|
||||
_tri_buffer[_tri_index][_tri_buffer_index ] = sample[0];
|
||||
_tri_buffer[_tri_index][_tri_buffer_index+1] = sample[1];
|
||||
_tri_buffer_index += 2;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
flush();
|
||||
return false;
|
||||
}
|
||||
virtual void flush(void) override
|
||||
{
|
||||
if (_tri_buffer_index)
|
||||
{
|
||||
_m5sound->playRaw(_tri_buffer[_tri_index], _tri_buffer_index, hertz, true, 1, _virtual_ch);
|
||||
_tri_index = _tri_index < 2 ? _tri_index + 1 : 0;
|
||||
_tri_buffer_index = 0;
|
||||
++_update_count;
|
||||
}
|
||||
}
|
||||
virtual bool stop(void) override
|
||||
{
|
||||
flush();
|
||||
_m5sound->stop(_virtual_ch);
|
||||
for (size_t i = 0; i < 3; ++i)
|
||||
{
|
||||
memset(_tri_buffer[i], 0, tri_buf_size * sizeof(int16_t));
|
||||
}
|
||||
++_update_count;
|
||||
return true;
|
||||
}
|
||||
|
||||
const int16_t* getBuffer(void) const { return _tri_buffer[(_tri_index + 2) % 3]; }
|
||||
const uint32_t getUpdateCount(void) const { return _update_count; }
|
||||
|
||||
protected:
|
||||
m5::Speaker_Class* _m5sound;
|
||||
uint8_t _virtual_ch;
|
||||
static constexpr size_t tri_buf_size = 640;
|
||||
int16_t _tri_buffer[3][tri_buf_size];
|
||||
size_t _tri_buffer_index = 0;
|
||||
size_t _tri_index = 0;
|
||||
size_t _update_count = 0;
|
||||
};
|
||||
|
||||
|
||||
#define FFT_SIZE 256
|
||||
class fft_t
|
||||
{
|
||||
float _wr[FFT_SIZE + 1];
|
||||
float _wi[FFT_SIZE + 1];
|
||||
float _fr[FFT_SIZE + 1];
|
||||
float _fi[FFT_SIZE + 1];
|
||||
uint16_t _br[FFT_SIZE + 1];
|
||||
size_t _ie;
|
||||
|
||||
public:
|
||||
fft_t(void)
|
||||
{
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.141592653
|
||||
#endif
|
||||
_ie = logf( (float)FFT_SIZE ) / log(2.0) + 0.5;
|
||||
static constexpr float omega = 2.0f * M_PI / FFT_SIZE;
|
||||
static constexpr int s4 = FFT_SIZE / 4;
|
||||
static constexpr int s2 = FFT_SIZE / 2;
|
||||
for ( int i = 1 ; i < s4 ; ++i)
|
||||
{
|
||||
float f = cosf(omega * i);
|
||||
_wi[s4 + i] = f;
|
||||
_wi[s4 - i] = f;
|
||||
_wr[ i] = f;
|
||||
_wr[s2 - i] = -f;
|
||||
}
|
||||
_wi[s4] = _wr[0] = 1;
|
||||
|
||||
size_t je = 1;
|
||||
_br[0] = 0;
|
||||
_br[1] = FFT_SIZE / 2;
|
||||
for ( size_t i = 0 ; i < _ie - 1 ; ++i )
|
||||
{
|
||||
_br[ je << 1 ] = _br[ je ] >> 1;
|
||||
je = je << 1;
|
||||
for ( size_t j = 1 ; j < je ; ++j )
|
||||
{
|
||||
_br[je + j] = _br[je] + _br[j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void exec(const int16_t* in)
|
||||
{
|
||||
memset(_fi, 0, sizeof(_fi));
|
||||
for ( size_t j = 0 ; j < FFT_SIZE / 2 ; ++j )
|
||||
{
|
||||
float basej = 0.25 * (1.0-_wr[j]);
|
||||
size_t r = FFT_SIZE - j - 1;
|
||||
|
||||
/// perform han window and stereo to mono convert.
|
||||
_fr[_br[j]] = basej * (in[j * 2] + in[j * 2 + 1]);
|
||||
_fr[_br[r]] = basej * (in[r * 2] + in[r * 2 + 1]);
|
||||
}
|
||||
|
||||
size_t s = 1;
|
||||
size_t i = 0;
|
||||
do
|
||||
{
|
||||
size_t ke = s;
|
||||
s <<= 1;
|
||||
size_t je = FFT_SIZE / s;
|
||||
size_t j = 0;
|
||||
do
|
||||
{
|
||||
size_t k = 0;
|
||||
do
|
||||
{
|
||||
size_t l = s * j + k;
|
||||
size_t m = ke * (2 * j + 1) + k;
|
||||
size_t p = je * k;
|
||||
float Wxmr = _fr[m] * _wr[p] + _fi[m] * _wi[p];
|
||||
float Wxmi = _fi[m] * _wr[p] - _fr[m] * _wi[p];
|
||||
_fr[m] = _fr[l] - Wxmr;
|
||||
_fi[m] = _fi[l] - Wxmi;
|
||||
_fr[l] += Wxmr;
|
||||
_fi[l] += Wxmi;
|
||||
} while ( ++k < ke) ;
|
||||
} while ( ++j < je );
|
||||
} while ( ++i < _ie );
|
||||
}
|
||||
|
||||
uint32_t get(size_t index)
|
||||
{
|
||||
return (index < FFT_SIZE / 2) ? (uint32_t)sqrtf(_fr[ index ] * _fr[ index ] + _fi[ index ] * _fi[ index ]) : 0u;
|
||||
}
|
||||
};
|
||||
|
||||
static constexpr const int preallocateBufferSize = 5 * 1024;
|
||||
static constexpr const int preallocateCodecSize = 29192; // MP3 codec max mem needed
|
||||
static void* preallocateBuffer = nullptr;
|
||||
static void* preallocateCodec = nullptr;
|
||||
static constexpr size_t WAVE_SIZE = 320;
|
||||
static AudioOutputM5Speaker out(&M5.Speaker, m5spk_virtual_channel);
|
||||
static AudioGenerator *decoder = nullptr;
|
||||
static AudioFileSourceICYStream *file = nullptr;
|
||||
static AudioFileSourceBuffer *buff = nullptr;
|
||||
static fft_t fft;
|
||||
static bool fft_enabled = false;
|
||||
static bool wave_enabled = false;
|
||||
static uint16_t prev_y[(FFT_SIZE / 2)+1];
|
||||
static uint16_t peak_y[(FFT_SIZE / 2)+1];
|
||||
static int16_t wave_y[WAVE_SIZE];
|
||||
static int16_t wave_h[WAVE_SIZE];
|
||||
static int16_t raw_data[WAVE_SIZE * 2];
|
||||
static int header_height = 0;
|
||||
static size_t station_index = 0;
|
||||
static char stream_title[128] = { 0 };
|
||||
static const char* meta_text[2] = { nullptr, stream_title };
|
||||
static const size_t meta_text_num = sizeof(meta_text) / sizeof(meta_text[0]);
|
||||
static uint8_t meta_mod_bits = 0;
|
||||
static volatile size_t playindex = ~0u;
|
||||
|
||||
static void MDCallback(void *cbData, const char *type, bool isUnicode, const char *string)
|
||||
{
|
||||
(void)cbData;
|
||||
if ((strcmp(type, "StreamTitle") == 0) && (strcmp(stream_title, string) != 0))
|
||||
{
|
||||
strncpy(stream_title, string, sizeof(stream_title));
|
||||
meta_mod_bits |= 2;
|
||||
}
|
||||
}
|
||||
|
||||
static void stop(void)
|
||||
{
|
||||
if (decoder) {
|
||||
decoder->stop();
|
||||
delete decoder;
|
||||
decoder = nullptr;
|
||||
}
|
||||
|
||||
if (buff) {
|
||||
buff->close();
|
||||
delete buff;
|
||||
buff = nullptr;
|
||||
}
|
||||
if (file) {
|
||||
file->close();
|
||||
delete file;
|
||||
file = nullptr;
|
||||
}
|
||||
out.stop();
|
||||
}
|
||||
|
||||
static void play(size_t index)
|
||||
{
|
||||
playindex = index;
|
||||
}
|
||||
|
||||
static void decodeTask(void*)
|
||||
{
|
||||
for (;;)
|
||||
{
|
||||
M5.delay(1);
|
||||
if (playindex != ~0u)
|
||||
{
|
||||
auto index = playindex;
|
||||
playindex = ~0u;
|
||||
stop();
|
||||
meta_text[0] = station_list[index][0];
|
||||
stream_title[0] = 0;
|
||||
meta_mod_bits = 3;
|
||||
file = new AudioFileSourceICYStream(station_list[index][1]);
|
||||
file->RegisterMetadataCB(MDCallback, (void*)"ICY");
|
||||
buff = new AudioFileSourceBuffer(file, preallocateBuffer, preallocateBufferSize);
|
||||
decoder = new AudioGeneratorMP3(preallocateCodec, preallocateCodecSize);
|
||||
decoder->begin(buff, &out);
|
||||
}
|
||||
if (decoder && decoder->isRunning())
|
||||
{
|
||||
if (!decoder->loop()) { decoder->stop(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t bgcolor(LGFX_Device* gfx, int y)
|
||||
{
|
||||
auto h = gfx->height();
|
||||
auto dh = h - header_height;
|
||||
int v = ((h - y)<<5) / dh;
|
||||
if (dh > 44)
|
||||
{
|
||||
int v2 = ((h - y - 1)<<5) / dh;
|
||||
if ((v >> 2) != (v2 >> 2))
|
||||
{
|
||||
return 0x666666u;
|
||||
}
|
||||
}
|
||||
return gfx->color888(v + 2, v, v + 6);
|
||||
}
|
||||
|
||||
static void gfxSetup(LGFX_Device* gfx)
|
||||
{
|
||||
if (gfx == nullptr) { return; }
|
||||
if (gfx->width() < gfx->height())
|
||||
{
|
||||
gfx->setRotation(gfx->getRotation()^1);
|
||||
}
|
||||
gfx->setFont(&fonts::lgfxJapanGothic_12);
|
||||
gfx->setEpdMode(epd_mode_t::epd_fastest);
|
||||
gfx->setTextWrap(false);
|
||||
gfx->setCursor(0, 8);
|
||||
gfx->println("WebRadio player");
|
||||
gfx->fillRect(0, 6, gfx->width(), 2, TFT_BLACK);
|
||||
|
||||
header_height = (gfx->height() > 80) ? 33 : 21;
|
||||
fft_enabled = !gfx->isEPD();
|
||||
if (fft_enabled)
|
||||
{
|
||||
wave_enabled = (gfx->getBoard() != m5gfx::board_M5UnitLCD);
|
||||
|
||||
for (int y = header_height; y < gfx->height(); ++y)
|
||||
{
|
||||
gfx->drawFastHLine(0, y, gfx->width(), bgcolor(gfx, y));
|
||||
}
|
||||
}
|
||||
|
||||
for (int x = 0; x < (FFT_SIZE/2)+1; ++x)
|
||||
{
|
||||
prev_y[x] = INT16_MAX;
|
||||
peak_y[x] = INT16_MAX;
|
||||
}
|
||||
for (int x = 0; x < WAVE_SIZE; ++x)
|
||||
{
|
||||
wave_y[x] = gfx->height();
|
||||
wave_h[x] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void gfxLoop(LGFX_Device* gfx)
|
||||
{
|
||||
if (gfx == nullptr) { return; }
|
||||
if (header_height > 32)
|
||||
{
|
||||
if (meta_mod_bits)
|
||||
{
|
||||
gfx->startWrite();
|
||||
for (int id = 0; id < meta_text_num; ++id)
|
||||
{
|
||||
if (0 == (meta_mod_bits & (1<<id))) { continue; }
|
||||
meta_mod_bits &= ~(1<<id);
|
||||
size_t y = id * 12;
|
||||
if (y+12 >= header_height) { continue; }
|
||||
gfx->setCursor(4, 8 + y);
|
||||
gfx->fillRect(0, 8 + y, gfx->width(), 12, gfx->getBaseColor());
|
||||
gfx->print(meta_text[id]);
|
||||
gfx->print(" "); // Garbage data removal when UTF8 characters are broken in the middle.
|
||||
}
|
||||
gfx->display();
|
||||
gfx->endWrite();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
static int title_x;
|
||||
static int title_id;
|
||||
static int wait = INT16_MAX;
|
||||
|
||||
if (meta_mod_bits)
|
||||
{
|
||||
if (meta_mod_bits & 1)
|
||||
{
|
||||
title_x = 4;
|
||||
title_id = 0;
|
||||
gfx->fillRect(0, 8, gfx->width(), 12, gfx->getBaseColor());
|
||||
}
|
||||
meta_mod_bits = 0;
|
||||
wait = 0;
|
||||
}
|
||||
|
||||
if (--wait < 0)
|
||||
{
|
||||
int tx = title_x;
|
||||
int tid = title_id;
|
||||
wait = 3;
|
||||
gfx->startWrite();
|
||||
uint_fast8_t no_data_bits = 0;
|
||||
do
|
||||
{
|
||||
if (tx == 4) { wait = 255; }
|
||||
gfx->setCursor(tx, 8);
|
||||
const char* meta = meta_text[tid];
|
||||
if (meta[0] != 0)
|
||||
{
|
||||
gfx->print(meta);
|
||||
gfx->print(" / ");
|
||||
tx = gfx->getCursorX();
|
||||
if (++tid == meta_text_num) { tid = 0; }
|
||||
if (tx <= 4)
|
||||
{
|
||||
title_x = tx;
|
||||
title_id = tid;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ((no_data_bits |= 1 << tid) == ((1 << meta_text_num) - 1))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (++tid == meta_text_num) { tid = 0; }
|
||||
}
|
||||
} while (tx < gfx->width());
|
||||
--title_x;
|
||||
gfx->display();
|
||||
gfx->endWrite();
|
||||
}
|
||||
}
|
||||
|
||||
if (fft_enabled)
|
||||
{
|
||||
static int prev_x[2];
|
||||
static int peak_x[2];
|
||||
|
||||
auto buf = out.getBuffer();
|
||||
if (buf)
|
||||
{
|
||||
memcpy(raw_data, buf, WAVE_SIZE * 2 * sizeof(int16_t)); // stereo data copy
|
||||
gfx->startWrite();
|
||||
|
||||
// draw stereo level meter
|
||||
for (size_t i = 0; i < 2; ++i)
|
||||
{
|
||||
int32_t level = 0;
|
||||
for (size_t j = i; j < 640; j += 32)
|
||||
{
|
||||
uint32_t lv = abs(raw_data[j]);
|
||||
if (level < lv) { level = lv; }
|
||||
}
|
||||
|
||||
int32_t x = (level * gfx->width()) / INT16_MAX;
|
||||
int32_t px = prev_x[i];
|
||||
if (px != x)
|
||||
{
|
||||
gfx->fillRect(x, i * 3, px - x, 2, px < x ? 0xFF9900u : 0x330000u);
|
||||
prev_x[i] = x;
|
||||
}
|
||||
px = peak_x[i];
|
||||
if (px > x)
|
||||
{
|
||||
gfx->writeFastVLine(px, i * 3, 2, TFT_BLACK);
|
||||
px--;
|
||||
}
|
||||
else
|
||||
{
|
||||
px = x;
|
||||
}
|
||||
if (peak_x[i] != px)
|
||||
{
|
||||
peak_x[i] = px;
|
||||
gfx->writeFastVLine(px, i * 3, 2, TFT_WHITE);
|
||||
}
|
||||
}
|
||||
gfx->display();
|
||||
|
||||
// draw FFT level meter
|
||||
fft.exec(raw_data);
|
||||
size_t bw = gfx->width() / 60;
|
||||
if (bw < 3) { bw = 3; }
|
||||
int32_t dsp_height = gfx->height();
|
||||
int32_t fft_height = dsp_height - header_height - 1;
|
||||
size_t xe = gfx->width() / bw;
|
||||
if (xe > (FFT_SIZE/2)) { xe = (FFT_SIZE/2); }
|
||||
int32_t wave_next = ((header_height + dsp_height) >> 1) + (((256 - (raw_data[0] + raw_data[1])) * fft_height) >> 17);
|
||||
|
||||
uint32_t bar_color[2] = { 0x000033u, 0x99AAFFu };
|
||||
|
||||
for (size_t bx = 0; bx <= xe; ++bx)
|
||||
{
|
||||
size_t x = bx * bw;
|
||||
if ((x & 7) == 0) { gfx->display(); taskYIELD(); }
|
||||
int32_t f = fft.get(bx);
|
||||
int32_t y = (f * fft_height) >> 18;
|
||||
if (y > fft_height) { y = fft_height; }
|
||||
y = dsp_height - y;
|
||||
int32_t py = prev_y[bx];
|
||||
if (y != py)
|
||||
{
|
||||
gfx->fillRect(x, y, bw - 1, py - y, bar_color[(y < py)]);
|
||||
prev_y[bx] = y;
|
||||
}
|
||||
py = peak_y[bx] + 1;
|
||||
if (py < y)
|
||||
{
|
||||
gfx->writeFastHLine(x, py - 1, bw - 1, bgcolor(gfx, py - 1));
|
||||
}
|
||||
else
|
||||
{
|
||||
py = y - 1;
|
||||
}
|
||||
if (peak_y[bx] != py)
|
||||
{
|
||||
peak_y[bx] = py;
|
||||
gfx->writeFastHLine(x, py, bw - 1, TFT_WHITE);
|
||||
}
|
||||
|
||||
|
||||
if (wave_enabled)
|
||||
{
|
||||
for (size_t bi = 0; bi < bw; ++bi)
|
||||
{
|
||||
size_t i = x + bi;
|
||||
if (i >= gfx->width() || i >= WAVE_SIZE) { break; }
|
||||
y = wave_y[i];
|
||||
int32_t h = wave_h[i];
|
||||
bool use_bg = (bi+1 == bw);
|
||||
if (h>0)
|
||||
{ /// erase previous wave.
|
||||
gfx->setAddrWindow(i, y, 1, h);
|
||||
h += y;
|
||||
do
|
||||
{
|
||||
uint32_t bg = (use_bg || y < peak_y[bx]) ? bgcolor(gfx, y)
|
||||
: (y == peak_y[bx]) ? 0xFFFFFFu
|
||||
: bar_color[(y >= prev_y[bx])];
|
||||
gfx->writeColor(bg, 1);
|
||||
} while (++y < h);
|
||||
}
|
||||
size_t i2 = i << 1;
|
||||
int32_t y1 = wave_next;
|
||||
wave_next = ((header_height + dsp_height) >> 1) + (((256 - (raw_data[i2] + raw_data[i2 + 1])) * fft_height) >> 17);
|
||||
int32_t y2 = wave_next;
|
||||
if (y1 > y2)
|
||||
{
|
||||
int32_t tmp = y1;
|
||||
y1 = y2;
|
||||
y2 = tmp;
|
||||
}
|
||||
y = y1;
|
||||
h = y2 + 1 - y;
|
||||
wave_y[i] = y;
|
||||
wave_h[i] = h;
|
||||
if (h>0)
|
||||
{ /// draw new wave.
|
||||
gfx->setAddrWindow(i, y, 1, h);
|
||||
h += y;
|
||||
do
|
||||
{
|
||||
uint32_t bg = (y < prev_y[bx]) ? 0xFFCC33u : 0xFFFFFFu;
|
||||
gfx->writeColor(bg, 1);
|
||||
} while (++y < h);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gfx->display();
|
||||
gfx->endWrite();
|
||||
}
|
||||
}
|
||||
|
||||
if (!gfx->displayBusy())
|
||||
{ // draw volume bar
|
||||
static int px;
|
||||
uint8_t v = M5.Speaker.getVolume();
|
||||
int x = v * (gfx->width()) >> 8;
|
||||
if (px != x)
|
||||
{
|
||||
gfx->fillRect(x, 6, px - x, 2, px < x ? 0xAAFFAAu : 0u);
|
||||
gfx->display();
|
||||
px = x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setup(void)
|
||||
{
|
||||
auto cfg = M5.config();
|
||||
|
||||
// If you want to play sound from ModuleDisplay, write this
|
||||
// cfg.external_speaker.module_display = true;
|
||||
|
||||
// If you want to play sound from ModuleRCA, write this
|
||||
// cfg.external_speaker.module_rca = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker, write this
|
||||
cfg.external_speaker.hat_spk = true;
|
||||
|
||||
// If you want to play sound from HAT Speaker2, write this
|
||||
// cfg.external_speaker.hat_spk2 = true;
|
||||
|
||||
// If you want to play sound from ATOMIC Speaker, write this
|
||||
cfg.external_speaker.atomic_spk = true;
|
||||
|
||||
M5.begin(cfg);
|
||||
|
||||
preallocateBuffer = malloc(preallocateBufferSize);
|
||||
preallocateCodec = malloc(preallocateCodecSize);
|
||||
if (!preallocateBuffer || !preallocateCodec) {
|
||||
M5.Display.printf("FATAL ERROR: Unable to preallocate %d bytes for app\n", preallocateBufferSize + preallocateCodecSize);
|
||||
for (;;) { M5.delay(1000); }
|
||||
}
|
||||
|
||||
{ /// custom setting
|
||||
auto spk_cfg = M5.Speaker.config();
|
||||
/// Increasing the sample_rate will improve the sound quality instead of increasing the CPU load.
|
||||
spk_cfg.sample_rate = 48000; // default:64000 (64kHz) e.g. 48000 , 50000 , 80000 , 96000 , 100000 , 128000 , 144000 , 192000 , 200000
|
||||
spk_cfg.task_pinned_core = APP_CPU_NUM;
|
||||
M5.Speaker.config(spk_cfg);
|
||||
}
|
||||
|
||||
|
||||
M5.Speaker.begin();
|
||||
|
||||
M5.Display.println("Connecting to WiFi");
|
||||
WiFi.disconnect();
|
||||
WiFi.softAPdisconnect(true);
|
||||
WiFi.mode(WIFI_STA);
|
||||
|
||||
#if defined ( WIFI_SSID ) && defined ( WIFI_PASS )
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASS);
|
||||
#else
|
||||
WiFi.begin();
|
||||
#endif
|
||||
|
||||
// Try forever
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
M5.Display.print(".");
|
||||
M5.delay(100);
|
||||
}
|
||||
M5.Display.clear();
|
||||
|
||||
gfxSetup(&M5.Display);
|
||||
|
||||
play(station_index);
|
||||
|
||||
xTaskCreatePinnedToCore(decodeTask, "decodeTask", 4096, nullptr, 1, nullptr, PRO_CPU_NUM);
|
||||
}
|
||||
|
||||
void loop(void)
|
||||
{
|
||||
gfxLoop(&M5.Display);
|
||||
|
||||
{
|
||||
static int prev_frame;
|
||||
int frame;
|
||||
do
|
||||
{
|
||||
M5.delay(1);
|
||||
} while (prev_frame == (frame = millis() >> 3)); /// 8 msec cycle wait
|
||||
prev_frame = frame;
|
||||
}
|
||||
|
||||
M5.update();
|
||||
auto td = M5.Touch.getDetail();
|
||||
if (M5.BtnA.wasPressed() || td.wasPressed())
|
||||
{
|
||||
M5.Speaker.tone(440, 50);
|
||||
}
|
||||
int cc = M5.BtnA.getClickCount();
|
||||
if (cc == 0) cc = td.getClickCount();
|
||||
if (M5.BtnA.wasDecideClickCount() || td.wasClicked())
|
||||
{
|
||||
switch (cc)
|
||||
{
|
||||
case 1:
|
||||
M5.Speaker.tone(1000, 100);
|
||||
if (++station_index >= stations) { station_index = 0; }
|
||||
play(station_index);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
M5.Speaker.tone(800, 100);
|
||||
if (station_index == 0) { station_index = stations; }
|
||||
play(--station_index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (M5.BtnA.isHolding() || M5.BtnB.isPressed() || M5.BtnC.isPressed() || td.isHolding())
|
||||
{
|
||||
size_t v = M5.Speaker.getVolume();
|
||||
int add = (M5.BtnB.isPressed()) ? -1 : 1;
|
||||
if (M5.BtnA.isHolding() || td.isHolding())
|
||||
{
|
||||
add = (M5.BtnA.getClickCount() || td.getClickCount()) ? -1 : 1;
|
||||
}
|
||||
v += add;
|
||||
if (v <= 255)
|
||||
{
|
||||
M5.Speaker.setVolume(v);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue