change to 16khz, add audio filter, refactor to use DMA

This commit is contained in:
zyphlar
2026-07-04 03:28:13 -07:00
parent 8a6d096635
commit 0697c3d6f3
4 changed files with 333 additions and 201 deletions
+120 -169
View File
@@ -149,7 +149,7 @@ const uint8_t BTN_PINS[] = {
// AUDIO CONFIG
// ============================================================
#define SAMPLE_RATE 8000
#define SAMPLE_RATE 16000
#define AUDIO_BUF_SIZE 512 // larger buffer = fewer SPI transactions
#define SILENCE 128 // 0x80 = center for unsigned 8-bit
@@ -205,10 +205,8 @@ uint32_t g_trackStart[MAX_TRACKS];
uint32_t g_trackLen[MAX_TRACKS];
uint8_t g_currentTrack = 0;
// Audio double-buffer
uint8_t g_audioBuf[2][AUDIO_BUF_SIZE];
volatile uint8_t g_activeBuf = 0;
volatile uint16_t g_bufPos = 0;
// Audio DMA buffers — uint16_t PWM duty-cycle values for EasyDMA
static uint16_t g_pwmBuf[2][AUDIO_BUF_SIZE];
volatile bool g_bufReady[2] = {false, false};
uint32_t g_nextReadAddr = 0;
uint32_t g_playEnd = 0;
@@ -633,6 +631,7 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
}
void setupBLE() {
/* Note BLE requires a custom app to communicate, not regular bluetooth */
Bluefruit.begin();
Bluefruit.setName("BabyMobile");
Bluefruit.setTxPower(0); // 0 dBm — save power, short range is fine
@@ -676,127 +675,155 @@ void setupBLE() {
// ============================================================
// AUDIO PLAYBACK
// AUDIO PLAYBACK — nRF52840 PWM EasyDMA
// ============================================================
//
// NRF_PWM0 drives the audio pin directly via DMA — no per-sample ISR.
// PRESCALER = 0 → 16 MHz base clock
// COUNTERTOP = 500 → PWM carrier = 16 MHz / 500 = 32 kHz
// SEQ REFRESH = 1 → each sample plays 2 carrier cycles → 16 kHz sample rate
//
// Double-buffer: SEQ[0] / SEQ[1] auto-chain via SHORTS.
// ISR fires only on SEQEND (every 512 samples = 32 ms), sets g_bufReady[b]=false.
// loop() calls audioFillBuf() to reload the finished buffer.
// nRF52840 has hardware PWM (up to 4 instances, 16MHz base clock)
// We use the nrf_pwm peripheral directly for audio output
// and a TIMER for the sample rate interrupt.
#define PWM_COUNTERTOP 500 // 16 MHz / 500 = 32 kHz carrier
#define PWM_SILENCE 250 // midpoint of 0500 duty-cycle range
// For Arduino compatibility, we use analogWrite for the PWM
// and a software timer for sample feeding.
// micros() timestamp for next audio sample (loop-driven at 8kHz)
uint32_t g_nextSampleUs = 0;
// Software gain: 1=unity, 2=2x, etc. (clips at 0/255). Adjustable via 'u'/'d' serial.
// Software gain: 1=unity, 2=2x, etc. Adjustable via 'u'/'d' serial.
static uint8_t g_audioGain = 1;
// Called from loop() every 125µs while g_playing
void audioTick(void) {
// Output sample with software gain (stretches away from center 128)
int16_t s = (int16_t)g_audioBuf[g_activeBuf][g_bufPos] - 128;
s *= g_audioGain;
if (s > 127) s = 127;
if (s < -128) s = -128;
analogWrite(PIN_AUDIO_PWM, (uint8_t)(s + 128));
g_bufPos++;
// Read PCM from current source into g_pwmBuf[b], applying gain and scaling.
// Pads the tail with silence if fewer than AUDIO_BUF_SIZE bytes remain.
static void audioFillBuf(uint8_t b) {
uint8_t pcm[AUDIO_BUF_SIZE];
uint32_t toRead = 0;
if (g_nextReadAddr < g_playEnd) {
toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
#ifdef POC_INTERNAL_FLASH
g_pocFile.read(pcm, toRead);
#else
flashReadBytes(g_nextReadAddr, pcm, toRead);
#endif
g_nextReadAddr += toRead;
}
for (uint32_t i = 0; i < toRead; i++) {
int16_t s = (int16_t)pcm[i] - 128;
s *= g_audioGain;
if (s > 127) s = 127;
if (s < -128) s = -128;
g_pwmBuf[b][i] = (uint16_t)((uint8_t)(s + 128)) * PWM_COUNTERTOP / 256;
}
for (uint32_t i = toRead; i < AUDIO_BUF_SIZE; i++) {
g_pwmBuf[b][i] = PWM_SILENCE;
}
g_bufReady[b] = true;
}
if (g_bufPos >= AUDIO_BUF_SIZE) {
g_bufReady[g_activeBuf] = false;
g_activeBuf ^= 1;
g_bufPos = 0;
if (!g_bufReady[g_activeBuf]) {
// Buffer underrun — stop cleanly
g_playing = false;
analogWrite(PIN_AUDIO_PWM, SILENCE);
// PWM0 ISR — fires every 32 ms (512 samples at 16 kHz).
// Just signals which buffer needs refilling; loop() does the actual I/O.
extern "C" void PWM0_IRQHandler() {
if (NRF_PWM0->EVENTS_SEQEND[0]) {
NRF_PWM0->EVENTS_SEQEND[0] = 0;
if (g_playing) {
g_bufReady[0] = false;
NRF_PWM0->TASKS_SEQSTART[1] = 1; // start buf 1 immediately
}
}
if (NRF_PWM0->EVENTS_SEQEND[1]) {
NRF_PWM0->EVENTS_SEQEND[1] = 0;
if (g_playing) {
g_bufReady[1] = false;
NRF_PWM0->TASKS_SEQSTART[0] = 1; // start buf 0 immediately
}
}
}
void audioInit() {
pinMode(PIN_AUDIO_PWM, OUTPUT);
analogWrite(PIN_AUDIO_PWM, SILENCE);
analogWriteResolution(8); // 8-bit PWM
// Silence both DMA buffers
for (int i = 0; i < AUDIO_BUF_SIZE; i++) {
g_pwmBuf[0][i] = PWM_SILENCE;
g_pwmBuf[1][i] = PWM_SILENCE;
}
// Configure NRF_PWM0 for EasyDMA sequence mode
NRF_PWM0->PSEL.OUT[0] = g_ADigitalPinMap[PIN_AUDIO_PWM];
NRF_PWM0->PSEL.OUT[1] = 0x80000000UL; // disconnected
NRF_PWM0->PSEL.OUT[2] = 0x80000000UL;
NRF_PWM0->PSEL.OUT[3] = 0x80000000UL;
NRF_PWM0->ENABLE = PWM_ENABLE_ENABLE_Enabled;
NRF_PWM0->MODE = PWM_MODE_UPDOWN_Up;
NRF_PWM0->PRESCALER = PWM_PRESCALER_PRESCALER_DIV_1; // 16 MHz
NRF_PWM0->COUNTERTOP = PWM_COUNTERTOP;
NRF_PWM0->LOOP = 0;
NRF_PWM0->DECODER = (PWM_DECODER_LOAD_Common << PWM_DECODER_LOAD_Pos) |
(PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos);
NRF_PWM0->SEQ[0].PTR = (uint32_t)g_pwmBuf[0];
NRF_PWM0->SEQ[0].CNT = AUDIO_BUF_SIZE;
NRF_PWM0->SEQ[0].REFRESH = 1; // each value plays for 2 PWM periods
NRF_PWM0->SEQ[0].ENDDELAY = 0;
NRF_PWM0->SEQ[1].PTR = (uint32_t)g_pwmBuf[1];
NRF_PWM0->SEQ[1].CNT = AUDIO_BUF_SIZE;
NRF_PWM0->SEQ[1].REFRESH = 1;
NRF_PWM0->SEQ[1].ENDDELAY = 0;
NRF_PWM0->SHORTS = 0; // no auto-shorts; ISR handles sequence chaining
NRF_PWM0->INTENSET = PWM_INTENSET_SEQEND0_Msk | PWM_INTENSET_SEQEND1_Msk;
NVIC_SetPriority(PWM0_IRQn, 7);
NVIC_EnableIRQ(PWM0_IRQn);
// Start PWM outputting silence (amp is off, so this is silent)
NRF_PWM0->TASKS_SEQSTART[0] = 1;
}
void audioStart(uint8_t trackNum) {
Serial.print("Playing track");
Serial.print(trackNum);
Serial.print("Playing track ");
Serial.print(trackNum + 1);
Serial.print(" of ");
Serial.println(g_numTracks);
if (trackNum >= g_numTracks) return;
g_currentTrack = trackNum;
g_playEnd = g_trackLen[trackNum];
g_nextReadAddr = 0;
#ifdef POC_INTERNAL_FLASH
if (g_pocFile) g_pocFile.close();
char fname[16];
pocFilename(trackNum, fname);
if (!g_pocFile.open(fname, FILE_O_READ)) {
Serial.print("audioStart: cannot open ");
Serial.println(fname);
Serial.print("audioStart: cannot open "); Serial.println(fname);
return;
}
// Pre-fill buf 0
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
g_pocFile.read(g_audioBuf[0], toRead);
g_nextReadAddr += toRead;
g_bufReady[0] = true;
// Pre-fill buf 1
if (g_nextReadAddr < g_playEnd) {
toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
g_pocFile.read(g_audioBuf[1], toRead);
g_nextReadAddr += toRead;
g_bufReady[1] = true;
} else {
memset(g_audioBuf[1], SILENCE, AUDIO_BUF_SIZE);
g_bufReady[1] = true;
}
g_nextReadAddr = 0;
g_playEnd = g_trackLen[trackNum];
#else
uint32_t start = g_trackStart[trackNum];
g_playEnd = start + g_trackLen[trackNum];
g_nextReadAddr = start;
// Pre-fill both buffers
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
flashReadBytes(g_nextReadAddr, g_audioBuf[0], toRead);
g_nextReadAddr += toRead;
g_bufReady[0] = true;
if (g_nextReadAddr < g_playEnd) {
toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
flashReadBytes(g_nextReadAddr, g_audioBuf[1], toRead);
g_nextReadAddr += toRead;
g_bufReady[1] = true;
} else {
memset(g_audioBuf[1], SILENCE, AUDIO_BUF_SIZE);
g_bufReady[1] = true;
}
g_nextReadAddr = g_trackStart[trackNum];
g_playEnd = g_nextReadAddr + g_trackLen[trackNum];
#endif
g_activeBuf = 0;
g_bufPos = 0;
// Stop DMA cleanly, fill both buffers, restart
NRF_PWM0->TASKS_STOP = 1;
uint32_t t = millis();
while (!NRF_PWM0->EVENTS_STOPPED && millis() - t < 10) {}
NRF_PWM0->EVENTS_STOPPED = 0;
audioFillBuf(0);
audioFillBuf(1);
NRF_PWM0->SEQ[0].PTR = (uint32_t)g_pwmBuf[0];
NRF_PWM0->SEQ[1].PTR = (uint32_t)g_pwmBuf[1];
// Enable amplifier
digitalWrite(PIN_AMP_SD, HIGH);
delay(10);
g_playing = true;
g_nextSampleUs = micros();
Serial.print("Playing track ");
Serial.println(trackNum + 1);
NRF_PWM0->TASKS_SEQSTART[0] = 1;
}
void audioStop() {
g_playing = false;
analogWrite(PIN_AUDIO_PWM, SILENCE);
NRF_PWM0->TASKS_STOP = 1;
#ifdef POC_INTERNAL_FLASH
if (g_pocFile) g_pocFile.close();
@@ -804,7 +831,6 @@ void audioStop() {
// Disable amp
digitalWrite(PIN_AMP_SD, LOW);
Serial.println("Playback stopped");
}
@@ -856,16 +882,7 @@ uint8_t buttonRead() {
void waitButtonRelease(uint8_t btn) {
unsigned long deadline = millis() + 300;
while (buttonRead() == btn && millis() < deadline) {
// Keep audio running while waiting for button release
if (g_playing) {
uint32_t now = micros();
if ((int32_t)(now - g_nextSampleUs) >= 0) {
g_nextSampleUs += 125;
audioTick();
}
}
}
while (buttonRead() == btn && millis() < deadline) {}
}
@@ -982,59 +999,14 @@ void setup() {
audioInit();
#ifdef POC_INTERNAL_FLASH
// Boot test tone: 440Hz square wave for 1 second
// If you hear a beep, the amp/PWM chain is working.
Serial.println("Boot tone disabled...");
// digitalWrite(PIN_AMP_SD, HIGH);
delay(20);
for (int i = 0; i < 8000; i++) {
// 440Hz at 8kHz sample rate: 8000/440 ≈ 18 samples/cycle
analogWrite(PIN_AUDIO_PWM, (i % 18) < 9 ? 255 : 0);
delayMicroseconds(125);
}
analogWrite(PIN_AUDIO_PWM, SILENCE);
delay(20);
digitalWrite(PIN_AMP_SD, LOW);
Serial.println("Boot tone done");
// Hex dump track 0 + blocking playback diagnostic
// (after audioInit so amp/PWM are ready)
if (g_numTracks > 0) {
char diagName[16]; pocFilename(0, diagName);
File diagF(InternalFS);
if (diagF.open(diagName, FILE_O_READ)) {
uint32_t fsz = diagF.size();
Serial.print("Track0 size: "); Serial.println(fsz);
uint8_t hbuf[64];
int nr = diagF.read(hbuf, sizeof(hbuf));
diagF.close();
Serial.print("First "); Serial.print(nr); Serial.println(" bytes (hex):");
for (int i = 0; i < nr; i++) {
if (hbuf[i] < 0x10) Serial.print("0");
Serial.print(hbuf[i], HEX);
Serial.print(i % 16 == 15 ? "\n" : " ");
}
Serial.println();
} else {
Serial.println("Cannot open track0 for read!");
}
Serial.println("Diag: blocking play track0 for 3s...");
Serial.println("Diag: playing track0 for 3s...");
audioStart(0);
uint32_t diagEnd = millis() + 3000;
while (millis() < diagEnd && g_playing) {
uint32_t now = micros();
if ((int32_t)(now - g_nextSampleUs) >= 0) {
g_nextSampleUs += 125;
audioTick();
}
for (uint8_t b = 0; b < 2; b++) {
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
g_pocFile.read(g_audioBuf[b], toRead);
for (uint32_t i = toRead; i < AUDIO_BUF_SIZE; i++) g_audioBuf[b][i] = SILENCE;
g_nextReadAddr += toRead;
g_bufReady[b] = true;
audioFillBuf(b);
}
}
}
@@ -1212,43 +1184,22 @@ static void serUploadTick() {
#endif
void loop() {
// ---- Audio sample output (8kHz, loop-driven) ----
if (g_playing) {
g_lastActivity = millis(); // stay awake while audio is playing
uint32_t now = micros();
if ((int32_t)(now - g_nextSampleUs) >= 0) {
g_nextSampleUs += 125; // 1/8000s = 125µs
audioTick();
}
}
if (g_playing) g_lastActivity = millis();
// ---- Serial upload (POC mode) ----
#ifdef POC_INTERNAL_FLASH
serUploadTick();
#endif
// ---- Refill audio buffers ----
// ---- Refill audio buffers (ISR sets g_bufReady[b]=false when buffer done) ----
if (g_playing) {
for (uint8_t b = 0; b < 2; b++) {
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
uint32_t remaining = g_playEnd - g_nextReadAddr;
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, remaining);
#ifdef POC_INTERNAL_FLASH
g_pocFile.read(g_audioBuf[b], toRead);
#else
flashReadBytes(g_nextReadAddr, g_audioBuf[b], toRead);
#endif
// Pad with silence
for (uint32_t i = toRead; i < AUDIO_BUF_SIZE; i++) {
g_audioBuf[b][i] = SILENCE;
}
g_nextReadAddr += toRead;
g_bufReady[b] = true;
audioFillBuf(b);
}
}
// Track finished? Auto-advance and loop
// Track finished — both buffers drained and no data left
if (!g_bufReady[0] && !g_bufReady[1] && g_nextReadAddr >= g_playEnd) {
audioStop();
if (g_numTracks > 0) {