change to 16khz, add audio filter, refactor to use DMA
This commit is contained in:
+120
-169
@@ -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 0–500 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) {
|
||||
|
||||
Reference in New Issue
Block a user