first commit

This commit is contained in:
stuce-bot 2025-06-30 20:47:33 +02:00
commit 5893b00dd2
1669 changed files with 1982740 additions and 0 deletions

View file

@ -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

View file

@ -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);
}
}
}

View 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();
}
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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);
}
}
}