diff --git a/baby-mobile-v5.kicad_sch b/baby-mobile-v5.kicad_sch index 387ffb5..a70f515 100644 --- a/baby-mobile-v5.kicad_sch +++ b/baby-mobile-v5.kicad_sch @@ -2883,6 +2883,12 @@ (color 0 0 0 0) (uuid "27e5f52e-0ae1-42be-b638-2ba1b684ea4c") ) + (junction + (at 302.26 162.56) + (diameter 0) + (color 0 0 0 0) + (uuid "493dddab-eea7-4bd3-80f3-98e4f109b241") + ) (junction (at 339.09 76.18) (diameter 0) @@ -2947,16 +2953,6 @@ ) (uuid "0a315227-f5b7-4de4-9dc2-5a9da0d1e499") ) - (wire - (pts - (xy 318.69 162.56) (xy 320.04 162.56) - ) - (stroke - (width 0) - (type default) - ) - (uuid "105d0608-0c50-435d-ab00-ee5708be0d5c") - ) (wire (pts (xy 342.9 92.69) (xy 342.9 92.61) @@ -3067,6 +3063,16 @@ ) (uuid "39ffb2c8-fe04-4380-a8d4-b2edce0fe6c0") ) + (wire + (pts + (xy 302.26 166.45) (xy 302.26 162.56) + ) + (stroke + (width 0) + (type default) + ) + (uuid "3e8b71b2-91ca-4a51-b4ed-10be32554bc2") + ) (wire (pts (xy 207.01 201.93) (xy 207.01 204.47) @@ -3187,6 +3193,16 @@ ) (uuid "5c62cb6a-8252-484a-be0e-c05c74bf8a38") ) + (wire + (pts + (xy 314.88 162.56) (xy 320.04 162.56) + ) + (stroke + (width 0) + (type default) + ) + (uuid "650f524d-d388-4dcd-b479-9ecdf63dc629") + ) (wire (pts (xy 231.14 229.87) (xy 243.84 229.87) @@ -3267,6 +3283,16 @@ ) (uuid "8f5e7827-d21e-468e-b028-9b178eb97914") ) + (wire + (pts + (xy 302.26 162.56) (xy 299.72 162.56) + ) + (stroke + (width 0) + (type default) + ) + (uuid "93b77e73-d20f-4fe2-bf42-a7c0d0018958") + ) (wire (pts (xy 330.2 135.89) (xy 330.2 135.97) @@ -3307,6 +3333,16 @@ ) (uuid "a2cef328-0826-46a6-997f-fcedb293b343") ) + (wire + (pts + (xy 304.8 162.56) (xy 302.26 162.56) + ) + (stroke + (width 0) + (type default) + ) + (uuid "a80cbefa-0f08-40b2-9854-090a0f94a904") + ) (wire (pts (xy 207.01 204.47) (xy 223.52 204.47) @@ -3547,6 +3583,16 @@ ) (uuid "02b9416a-d87a-410f-8104-ff4ee4156d8d") ) + (label "AUDIO_PWM" + (at 292.1 162.56 180) + (effects + (font + (size 1.27 1.27) + ) + (justify right bottom) + ) + (uuid "14dfac6d-2b8c-4d46-b75d-10037718af51") + ) (label "GND" (at 334.02 82.55 0) (effects @@ -3587,6 +3633,16 @@ ) (uuid "20059635-02ae-4ce0-8d0b-fbe46035b4f2") ) + (label "GND" + (at 302.26 176.45 270) + (effects + (font + (size 1.27 1.27) + ) + (justify right bottom) + ) + (uuid "206b27d5-3a5c-4ff1-8a01-a0976b0bb04c") + ) (label "GND" (at 223.52 209.55 180) (effects @@ -3727,16 +3783,6 @@ ) (uuid "663e9f4c-2079-4248-a551-ced079e7c1e8") ) - (label "AUDIO_PWM" - (at 308.69 162.56 180) - (effects - (font - (size 1.27 1.27) - ) - (justify right bottom) - ) - (uuid "67c49a5e-335d-40a3-a3b6-ba2bd352f99e") - ) (label "C_BTN1" (at 223.52 201.93 180) (effects @@ -4067,6 +4113,74 @@ ) (uuid "fcb61f0a-8efc-4687-bfbd-16fb3032447e") ) + (symbol + (lib_id "Device:R") + (at 295.91 162.56 270) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (fields_autoplaced yes) + (uuid "07a6ad63-d769-4a1d-a5f8-ac0dbcfcf37c") + (property "Reference" "R5" + (at 295.91 156.21 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "1K" + (at 295.91 158.75 90) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Resistor_THT:R_Axial_DIN0204_L3.6mm_D1.6mm_P1.90mm_Vertical" + (at 295.91 160.782 90) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "~" + (at 295.91 162.56 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "Resistor" + (at 295.91 162.56 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (pin "1" + (uuid "0c6ee5ba-b07d-4d0b-9f2e-a2f679d3f5d5") + ) + (pin "2" + (uuid "20ea8652-17f4-4f08-affb-aeb74ca61b56") + ) + (instances + (project "baby-mobile-v5" + (path "/3033f063-d051-4b2e-89f5-f1cfe4577b32" + (reference "R5") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "Jumper:SolderJumper_2_Bridged") (at 227.33 232.41 0) @@ -4205,7 +4319,7 @@ ) (symbol (lib_id "bm:C") - (at 313.69 162.56 270) + (at 309.88 162.56 270) (unit 1) (exclude_from_sim no) (in_bom yes) @@ -4213,7 +4327,7 @@ (dnp no) (uuid "16705866-bee7-4f26-a10e-6f7bfbff4c09") (property "Reference" "C5" - (at 310.896 165.608 0) + (at 307.086 165.608 0) (effects (font (size 1.27 1.27) @@ -4221,7 +4335,7 @@ ) ) (property "Value" "0.1uF" - (at 313.944 167.64 0) + (at 310.134 167.64 0) (effects (font (size 1.27 1.27) @@ -4229,7 +4343,7 @@ ) ) (property "Footprint" "Capacitor_SMD:C_0805_2012Metric" - (at 313.69 162.56 0) + (at 309.88 162.56 0) (effects (font (size 1.27 1.27) @@ -4238,7 +4352,7 @@ ) ) (property "Datasheet" "" - (at 313.69 162.56 0) + (at 309.88 162.56 0) (effects (font (size 1.27 1.27) @@ -4247,7 +4361,7 @@ ) ) (property "Description" "" - (at 313.69 162.56 0) + (at 309.88 162.56 0) (effects (font (size 1.27 1.27) @@ -4335,6 +4449,72 @@ ) ) ) + (symbol + (lib_id "bm:C") + (at 302.26 171.45 0) + (unit 1) + (exclude_from_sim no) + (in_bom yes) + (on_board yes) + (dnp no) + (uuid "248137b6-5edd-42cd-819b-f475cf23c6e8") + (property "Reference" "C9" + (at 298.45 173.656 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Value" "1nF" + (at 298.196 171.116 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (property "Footprint" "Capacitor_SMD:C_0805_2012Metric" + (at 302.26 171.45 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Datasheet" "" + (at 302.26 171.45 0) + (effects + (font + (size 1.27 1.27) + ) + (hide yes) + ) + ) + (property "Description" "" + (at 302.26 171.45 0) + (effects + (font + (size 1.27 1.27) + ) + ) + ) + (pin "1" + (uuid "299b06dd-f42f-47c1-98ae-d0f74816099b") + ) + (pin "2" + (uuid "4eb980db-42f4-43fe-bdba-41a0fb426a69") + ) + (instances + (project "baby-mobile-v5" + (path "/3033f063-d051-4b2e-89f5-f1cfe4577b32" + (reference "C9") + (unit 1) + ) + ) + ) + ) (symbol (lib_id "Jumper:SolderJumper_2_Bridged") (at 227.33 204.47 0) diff --git a/baby_mobile_v2/baby_mobile_v2.ino b/baby_mobile_v2/baby_mobile_v2.ino index 6137ba5..949ce9a 100644 --- a/baby_mobile_v2/baby_mobile_v2.ino +++ b/baby_mobile_v2/baby_mobile_v2.ino @@ -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) { diff --git a/baby_mobile_v2/upload_track.py b/baby_mobile_v2/upload_track.py index c82c04a..4d4d0e5 100644 --- a/baby_mobile_v2/upload_track.py +++ b/baby_mobile_v2/upload_track.py @@ -44,12 +44,12 @@ def convert_to_pcm(path: str) -> bytes: sample_rate = struct.unpack_from(" bytes: print(f"Converting {os.path.basename(path)} via ffmpeg...") result = subprocess.run( ['ffmpeg', '-y', '-i', path, - '-ar', '8000', '-ac', '1', '-f', 'u8', '-acodec', 'pcm_u8', 'pipe:1'], + '-ar', '16000', '-ac', '1', '-f', 'u8', '-acodec', 'pcm_u8', 'pipe:1'], capture_output=True ) if result.returncode != 0: sys.exit(f"ERROR: ffmpeg failed:\n{result.stderr.decode(errors='replace')[-400:]}") pcm = result.stdout - duration = len(pcm) / 8000 + duration = len(pcm) / 16000 print(f" Converted: {len(pcm):,} bytes ({duration:.1f}s)") return pcm @@ -133,10 +133,10 @@ def upload(port: str, track_num: int, pcm: bytes) -> None: if "at=" in err_line: accepted = int(err_line.split("at=")[1].split()[0]) safe = int(accepted * 0.9) # 10% headroom - max_s = safe / 8000 + max_s = safe / 16000 print(f" LittleFS accepted {accepted} bytes before full.") print(f" Safe clip length: ~{max_s:.1f}s") - print(f" Trim with: ffmpeg -i input.mp3 -t {max_s:.0f} -ar 8000 -ac 1 -f u8 out.raw") + print(f" Trim with: ffmpeg -i input.mp3 -t {max_s:.0f} -ar 16000 -ac 1 -f u8 out.raw") sys.exit(1) # Wait for OK diff --git a/despacito_clip.raw b/despacito_clip.raw new file mode 100644 index 0000000..6aeed36 --- /dev/null +++ b/despacito_clip.raw @@ -0,0 +1 @@ +~~~~}}||{xy{}|{}}{|}z|}}|z}~}}|}|}}|z}~yy{zywxwwtpqu|~~}~~|xvw|~|yy}~zyyzzxvvsroookjq|wprvpg`_^aa`aepvwx{}yvuusrllqv|~{vtrrqomnovxwuty|{zshiwsia`elja__ahqst{~vqqyxjhmvwsstzvtxzzuqsuwxwwz~|pourheosor|vsqg^SJO[gdadjt~~zwuoiijjiijmpxz|yssy~vuy~z~~yuxtpmpk[X_kllzunnj\OKIJUbghkqs|rgfkmihgeempq{}{zxwsvx}vnfbid]Yblhppiih_OHIHKZkppxwgZ\ilghmmotwy|sx}xuux~}zuj_]eaWUekcgýzkfhfWA=HOIOeu~|qcY[glfchklrz~~{~~|zz{wwxrkekoe_^jnnszkefcYRPNQXdlr{}zvuqfaeigdfmtw{~|y|~}y{|x|}|z|~{vrkqqaTTgpmnźwhehdWNOFHYirtz}~ujcjpieimqnlnz~}~z|~zx|}wy}zwwusj\Y`oojkyrechg\QNQOTcu}xy{zul`aksvvpnsvzy~zxstv|}}wtpurg][kljl{xlfcc`TFEOaoqsx~odennihnyypluxhhq~ytyrcetvmdirxsgbiy}}}rqtusnotytoqxvuzyzu{zssk[R\my}yofaZUX_cegkqyxwyp`Z\imiffoy{y|vlzdalw}sjkorpt}yy~|z{rmqvsnjZRR_n|vstnd]ZYWW]gqusrz{umeaiprkc`eo}tWNfpkt{mc`_fp{yor{z}yifmwzrlgWKPd{sgeed]ZbmrusquyolmququvvqoxnULXl{yxqnd^^alsxxssw~|yqnqvy~~vppp`SR^my~zrjgea\`hsxtss|xvtpomt|}xrvmXILfykccjhcfqwxtou~|tprrqstwzyxjWLPcz~{}|yl^X\jwvlmuzx{voihr|~|zxsgZR]oyxvuma\aly}}usv~vu|{{tnloxz}{hUL^v~~rf^^ctzzrpqwxy|yy~vnnqw~}zwk^Uatvpjgfeox|tor~}wvvzz}vpnsz~o[MZm~{{zxyslcbix}rru~yssosuuxtxzy\GXs}vonoicdsvpu{|{wqpuz~|tont}bNLkplszvohijmrwz}}y{~}xxpomrxuvxw}mOTtuikw~sa]et}}|~{wu}yuliiozxv~u`]pn^ey}mcbq~~}{zxuphcou|xyuYO\~xhhpvyohlltz|~zy}}}zolklnnvw`P`}yccmvwlhkvyxw|~u{~|zvpgdks~nYRf{qlkmmllkhoqt{{tvx{tg^bv|tZR_xnkmsrljdhqy~~~zxxti^_huzoQQnomqvucadeilu~~{vx|ti`]lyucU^n|{nc`adbem|ys{{|z{wi_X`wiSSm{zuoojkhchoz~z{zzyw{rjbbcr{qaVhy|l`Y]cpxywrzokg]_`r}w[JUpzlbbcffiyvn|}reacjty}reUVf}~uic^dhozupu}{h^`iwx|thXQ^t}yne]^blwzwuvwvz~yphacpyq]GMg}uhdb]eix|zsry~{ylkfkpzvfNM^s|oeedghku{xqwqbZfzgOMbu}zndehgidry{uv{uhZ]iqXN\pynbbhgfcr}twxxyzn_`m{|h[Vhpst|uffnpmbgt}zsv~}|nfm{{`ML\kryoihmkikq{{{rsy}{{qjlwiYR_jv{umjikjqzxuo{{zwugfvt^Xbppsv|milqnkfsyz}yux|~}qjlvl_Xalvsnfljhjkz}~~}|twospquqz{xgdbqqovliopfa_fyv~omqz|nefw}}thddmps~}sheeacepw}zz}zz|{qjov{{ndZ_kt{~rg``_bkqyw{}wwusqlphrure_jxvzzygecfiglox{~{~yqplnhinu{m\\hz|~|qgaehkhkrz~uqushcdu~ue`_kpsxyoigia`bpy{}|zlvywqcem}ma[dnvsi^a_hiinxytkhmjqp|xhX`jxtl][Z`hnux}~rniokgfm~lb`lt}|i]Uggl_jx~wpnliabnzvf]gxxup}i]Ucimdbswnfsur_cq}fTYr}yqz|h`[`e_`kw}tqqnmffo{ueVfs}~{ujZ\_fgis|}vvsslcfkytd]kt}vkgagefghpxwqkojieisxgY`w{zuri`fglphlrztkiglomzpi]er|qxqlbeemlhlr~}lknimdrxykYan|~vtufdahmhmqz{tiklqunlnof`aq{wpsulh_ceglny~xrilpsuqpwqnbfj{txsom^edjmssw}}xwplmltrppecdo}y{ztqdfbikruv}~{uswprmtuusf`et|xz|sqclemkkwu{{}}}uronmppyoh^lz{|sl_^dkpnty|z{slfitvyl_Zwwz~tc`]npspmqw}vsqlkimwxjbp{}gf[dmkrsx~~|}|qpkljotzqjdi|xum`\dnqssvwz|trpgfapxoh^mwzkcadkcnk{z|zpoileim{rk`ilyta`aekonku|{wofejxmaWbnz|rghafgolkqwxspmoqzxiZYh}}okdccilktuxumrquus{|yiamxx~{rhadinlhhkwvwxqpnqrvv|vlgfxxvjbcimvnijp|||x{rnlpwvpwputpyv|rngecmwwsrv|z~|yu{zumfkmy~wrklwugkgjlnqjpv|}vtkhmsuwrwuect{pfnfi`krswpyz~zx|yxiealt{y}gqs{yjd^ko|vofhv}yxrimuost|lSe{qkozwpjinpmox{uuyvsnqszvimk{lkqxurkmpxuw{truuwpqnwz}qcnsqljosvxkhhs{xxu{vvpsu|spu~pbg|~rllxwukmsxy}{tmoptttux~lmqspktvz{vont|ytxwvuwrlovst|kqt{mekz{urpx{}}~}uswromnqxxpkt}tpox}}|twqu{xzwu{|xqgilv}oir|yrvsqrxyztw{{opv}{spqx}{lnp~tks}qnoy~rmv~}uwuywt}{ywt~||s}{vtntvv|{wrxz{vttuytwyz~{zwnko{|~zyzx|~}xvuw~|zuz}~|xvvw{xywruvwww{zx~xztxx|{wu|}~zxptvxxz~}}}}wppt|}yxyuv{{}}xwqopy}}}{uwzvuqtu}{zsvz~|wvwzttsz|yxw~}}yzwsosx~xsp}v}|zpkrx~~z}xw{~zx{}|sqzzzw}~}yzx}xqpyy|}tu{xpp|wrt{t}{|{sx~|vy~{|~|wru|~}usty~|zyy}{wry}~xvtxyxvvwxxw|{~||zxx|vz{z|~|zxzz}~|}}}{y{y~}{y{z}{zxwux}~}zzvy}~yww}}|vwz|{v{{~{||{xx{zvy{~yv}}~{~{|||soq|}~~urw{}zzzz}~~zx|ytqw~~|}{xvx}~unt}}yz~zy~~spu|zzy}}xy~{vu{}{~~~}|zxwxxzz{|}}{xz|x{{{~}{zvwyy|~{{yz||{}}yqlit~yuvz~xux{zywy|}~}ytzy{|}}~}yvxz~}zusomrzxww}}}~~|wwx}}zy|~|y~{z~~~z}}~xvu{|}{||{}{}~{|y~}|~~}~~~||~~{yzyz|z~|z{}~}~~~||~}~|{z}yyvvy{~{xz}}|wz}~~|z||~~~{}|z{}xxuwvvxw~~|yz~}|y|||}z{|}|yy{|~}}}~~~}zzzz|~~~|xxw{|~||z|}{}|||wuy}~~}|yz}}|{|~~|ywy}~~|z{{|~|{{}}{|~~~||~~~~~~}y{}~}|{|}}}~~{z{|~}}}}}zz}~~~}}}{y{z}~|}|}{yz~~|z~}||}}|}~}yxxz}}z{}~~~}z{}~{yz||ywz~~}{|}}}~}||z|~}~~{zyx|}~~{|}~~}{|~}{{}~~|~{{~~~|~|||~}~~|zzy{~}{{}|z|}~}{z{{||~}}{wwz~}|{~||}~~{~~}|~}~|{{{}|||{|{~~~{}~}|}}~{z{|~}}{|wz{|}}{z}~}yy||||zxy|}~}~~~|{}~yxyx|||}x}~}~~~{z{y{~}~|z|{z}~~~}{~{yyx{{{|{z~|{}~~}~z}}~~{w|{~~~}|{~~~~~z}z|~}~||}~~~}|}~~~{z~~~~}{}}y|~|}}||}~~}}~|~~~~z|~~~||~{~|z~{}z||~~~}}~~|~~}}}}|||}~~~~|}|}|~~}|{|}}~|}{{~|~|~~~~}|}|~|~~~~}}}~||}}~}~|~}|{~~}}||~|}}~}}|}y||z~}~x~}z}~~}|~}}~}~~}|~|~}~|~~~~~{~}z~~~~}|}}}~}|{||z{||~xw}|~~||{}{z}}}|}z}|wyyw~{|{~zwyzv~~{|vx~w{~|zxrty{uzxzxy|}}||{{{}|zz}|}||{|}|xx}~}x{z~~~~{|~|~{}y{{}|~~{zyy}}yzx}~~~|z~||{z}|x||wy{}y}~}z}x}|}x}y~xwy}~~y~~~|xz~{~y}|~~~}||~~}|yy{~{w{~~~~{}yx{|||{}yzxz~||~{~}}~{z|}~~~|~y~x~}{|~|y|{|~}||{~||}z||~}|xu|{{|~~}|~~~z||}~|{~zu||}~~|~}~}|}z}~zwz|y{||~||~|{|w}{~}{}|z~~~}}}|~}~z}}~}~}}|~|{|{}}y~{}}}{}|{}}w||}z~|~~~}}}{zzy}z}~zu~u}zzx}|}|}~|~~~{|~~|{xxzy~~yxwx~{}{y|~|}z|x}z}~z}x}|yv{}w}xw{v||}usuux}~~}~y}~{|}}~||~}~~~z~}||}~~xz|z|tz~pu}}utxzxz~{~}|}}w~soxwosx|to{uo}muupwrr~zt}jjr|~wurtz}zxyzuy{~{}~}w{z}y~~}|}x}}~||}|~|~|z|xz~~~|x{~}~|~}}|}~}~}}}}~~}~~~}~|}~|~|~}}}|}}~}{|~~}|~|z}|~|}~~}~~|~}~|~||~{~{z~~}~~}}}~~~~~~~~~~~~~~~~~~~~~|wz}xwy{}~~}}zz~|zy|~}~~~~~~~~~~~~}~~~~}~~~~}~~~~}}}}~}}~}~~}~~~~~~~~~~}}}||}||~~~~~}||{{{{{zzyxz{|||}}}||{zxy{{||||}|{yvvutstwyy|}~}}|yvuttvvy}{skgiq|{zzuoi`XPMQYeq}zvtrrsuvy}~~~}|~~~{wurqsrvx{|{{{{zyy{~~~|zyyyy|xpmkjhghghkou||vniec^[agpȼvpkd\OHLT]cn|}wqia[\_djpy~}xwtsqqruxy{}}|{|xwtuwwyy|~}wvsqpnmmru|}~{wuruuxy}ysomnorvx~|ywusttwwxy}~~}}z|}womic\_ftĴznbXPRTUV[fp{}tokhe_\_cnx}|wvuxyy{|zuqqruwz|}~xuomllqysdTOXmurrpstrof^Z^fs|vnkfioy|~}{zz~zxuy}}~wqlotw|~yoigmrx~|qg]NEKWp̼{qh`TNKOV\gr|tleehhihpuz||{vutvz}~yvwwxxyuttw}vppsvy~qg`RMN]ht˿~l_UTNFHSdt~vidcehgkqx{yqpsz|wrrpnoprv}{yttqpnnpsy}~}yp]PFPawǶzk^RHEHQ\jyzwrpmnjilpwyz~{|~~}tmeju{wokhfffgls~yqjihmmqv|{yz}|zvxy{{~wmeet|wrkfbbacgoy~ysmhdimtz}vuu}vlhjmvƾ|unjgggeegmxxurmhfgjkt|{uuw~}yg[WcmǶyqib]\YW[fw{wswtnifjkqx~zrpuz}xnebhnzuojfc`\]ajy}uusoib`fmu~{vvx{voc[Z_gvĪ}tkgfc]\W^dpyuyxpi`cdlw~wtuy~|o`Y]gtʼ|wnifaa__agvzuuqlb_`gpxwzzzm_ZWao~wqmidb]Z[doxxvtmicdhku{yuxy{|sh\[cqɽysjjikb]\ds{vwvunh`bhpy}{xzxg[RVcv˽zvqlgdaa^bhu}vvuroigegsz}zx{|rcWWbn}ʻ{rga`^_`cgn||ywuomffeeov|yl[RW_sijyqjb]^^bdir}z{urljkkosv|zeUPXfyǺrj__]a`cjx|{yvshjmsyxwy}xcRU[l}ƾzrhc`_bchs~~{wvllmnv{}~~ygYX_nxpf_][`cmx~xunjjjosuz|~gZNXcwƴxnf^[YY`h}~{swtrojgekt}}~o_NRZnʽ|ypiaYUR[g|zxutmkccimvz|fVKQ^oʼ}vpdbZXV\jwvnjgbeipt{|}mZKKRc}|tc[TSW\jxzulgabckty~~{gUJS^q}ulf^YUZap{tnhhbbcfrx~qbQNWby}mc\WXV]crwiecbiiow{}~{eQIQaxseZTRU[gxytjeabir|}}{xz{lSPVf~~vh]YSWam|wkfchpv|{zxxwzucLQXl~uke]UVXgzznleinu|~}z{z}qYRWe{|ogaX[[dszwoiiity}uvt~~cRP^sĹ~y|~|tmj_^_dr}{vmihsyvww~gTN[utwwxshf\bfoxyunkiisy|{|xaTS`u¿wyusnfc_fit|vvnjgjv|{yyscZYgvŽ||xqplokfcer||ywuolglt~~||xw~h_^cyxwxqlca`dho}}}tnlhhhnu~~y~}{~zue`^gyxxwtmfb[\dt{vokchitxvx{ocY]h|~zug`[]\cp|xsmh`ckxzyyxzzyn`[[l}¼}yvod`]``fq~trolgcgn}~z|z~{p`YXcy»~~uf\]ceim|}tnioifbit~{{ncY\_pxrmb`]`cky}slhf^]cp{z|}un\ZVc{Ŀxtme^[^^iuxoljdbdrx}{y~ocY`j{}yrd\Z^gkvyxppb\^i{{{|y~n\Z\r}x~}oaX[chnssmjjb\ao|qtykZW[kzk[U\`mx~woi`_dp~{qww|ugbamtrtvnfbdinx|vnjeehu||uw}}p\V^oxxx|tj_[`cltztpfa]gq~{w}rZUZi~~~oe[^agq}tkoifdkyzsv|wf]\fuxxsxkiadeiryyrkedgp|wsrw}v]VWk}y{vi\[_ipzrmgc_cl{{ux||ic_cyywuuoh`aglu~{qfccbip~{tr~n^XVdu|~qj^\`eqwpi^\^my}v~~oXWWi}yi_\_gjv|qj`]Zftzw|lXUXlxi]Z\ejz{qi`YYgvxxweOPPjxgZY\ci|~tg_X]ix~vv~zaWR[p~~ug^[bgu|rh__cnxvz|s\VWeyxnccbhis}ztme_bn{tqymXX]r}{xrf`^fmvymfdajo{ztr|n_[^f||uvnf^_afvzmb^amu~ys{}rYY[o}{voe`_bkz~ui``dox~wqx|oVOUj|ndZ]ds}m`^`go|zrwyeSQWpug`\^cn|wtja]aoyqzy[QP`}q`Y]fktqic][apqlwoWKLih]V^dl{|qc_[`iw{mp{y`POWu{vhXX_fu{qh^Zam~vu{qXRPb}l^YZdlwvlb``fu~qluz}jTVZtog]afq}thcbdmz{suxvaYU_x|ujd^eiuyqfa^cptkrzs[QOa}xbYXchtyog_Z_n|{qx~r\SRgmdX_bkz~rhb^bhyypl{wcOPWpl\UXbo|qja_co~ytt|wdY[h}tme_`bnw~sf`]ak{|tqv}}eWXcyri\]^kw|kc]`gqyqryp]]_qyma^^cq~znb`alvupqxh__f~|rkdchkv{qjefls|~xquyva]^qzpecbgp}wkfehlt}}yvyyfa^f{|~|tjc]`hxwla_eky}~vxz~wac^lxnidbbjz~ulaciy~zvsxxj]\a|vic^egu}{|wkifkvz{xqtxwqdaezxqgbekvumghkowy|xzuz|q]^aw}lhaagpyrjfekv}~zuqszyc\[e~tjb_ek{upihjqz~}wtqw|~oc_dsuhabjs}~~skedkt{row{~l_Xavsfbcjtzohfinw}~~xvwo^[euxofdejmzzsjfgnw}xvrz{yneio}~pg`gnw|uolklrw}~y{y}thklx~yukgcjr~{rllnstvz|z||xlcim~ytmgegmw|ulglvwyttvzwa]eroeadilw}~}wqlinty|~yvu|r_[btpe`dhnv|{~{rhgiuy}|wvw}r]\as{l_`elw~}wlghosxtrw~uf[bi{i`^fnywy}~rjfkr||sptzrdalyridfktx||yy}utnmjs{{srx|ma_gwpf`iqx}uot{xsoqvtnmxxe[dpzokhkmrz}yw~zrsrz}wsnstxj[cjskfhkqxvtxzsru{}~wrqtzwi_aiwujehknv|{ww~urry~~|trry{k][iywhehltw}~~{xz|~|y|{ywturwz~wmffoy{pokljry|w{~}wvw|~vrmux}~rhdnuzslmknov||zz|}zyy||xtrpru}~}skdjt~vvuwpnot{}~|zyzzvpsuxxy{||tljpu}xsppqsx}}|{~wruxvurtsz}ztkfiu|ysmpquw~}|}|xvxxursuvz||vnfht}ytrssstw{}~vrrsvsvy{}{|}~zohcnx|onqvvsrt|~xsqtuvty}{xy|~xpkmvy||wtqnklt}~wqpstvz{~|z~|zribis~~uqklkpu{~{zvuwwwwyz||{yx{{qmkuy|wuqqmls|}}yqot{}zzz~~|zz}~znjir||pikosv|~|{zywxz|{vtv|}||~|rjmq}zwsrotu|~}~||{yxuww{y{{~{~}|zuqlmr}yrssz{|}~z}}z}~{zzz{yz{~}zy||}wtnos~~~}zxy{~}{z~}}~}yvvy~}~}|{|zz{~vont~{xux}~~yx{{vux{}|yxx{}|{y{|~ztqv~yz}}yxy}}|yy}|~~~|yvvwyzyz{|yuuyytv{~|{yvx|~}{~{yuvwwwwxxz}}|y|}{xvz}}~zwvy}|z}~~xwxxyxy{z{z{{z}|zww{{xz~|{z{yyz}~}z||~~|xyz|xwwywvvyyx|}}~yxz}~{yyz|~|{{}}vrooru{{~}}|{}~yutx}~{{~|}}{wuuy{ywvwy{|}~zuyzy{z{yxy|}{|~}{|{zy{{||z{z}~}~~}xuyxrs|{||yuwz}}zxz}}}}~{ywyzzz|}zzy{~~yuxz}y{{||{zxvvy}}|zyyvvx|~|{y{~|}xrt{}yyyywwz|}}yz~~zyy{|{zx|}~|z|~}{yx|~{uv~{yy|}||~~{yyzzyxy{}}~}{yxxz}}|y}{x}}yuvz|}|}{||~|{{{{{|{{yyxy{~}{{~xsu|}}yuux~||}~{|~{xw{}~}|}~|{y{{}{wuxzz}}yyz~}|{|}~}}~|}}~~~~||{zxwwxz{}~zzzuwz{~~}{|}~~|usvz~|{|}||}zux~~xv{{vuy~||}}}{}|{|~zyy|{yyz|||yy{}x{~|xuy|~||}~}}~|z{||zxwy}{xxz|}{|trw|{~~~}}}{|}}|}|||}|zz{|~}|ywyz{y{}xx{}|{{|z}||~~~||||~|ywwyz||{|{yyz|wu|{zzyz{~zy{~|{ywxz}~~|{{{{z{|}~~~zxx|}{{}~~}}||||{}|ywx}}}|~}|{{z|{{~zwx~~~|yz{|}|}}|zwx|~}{zz{}|ywx|~}ywy~~{|}|||~~||{z}}{}~{yz~|ywwxw{~}zz}}|}~}}{}~~}}}|z{||||}|z|{z|}}|{}}zzy~~}}|}~~|}|zyyzzzyz|~~}~||}{|}~}yxz}~~}~{{{}|zyy}~}|{z{|~~{wvw{~}zz|~}~~~|yy{}|yyz{yyz{}~~}||}~~}|~}}~}}|}}zxy|}|{xwy{{z|~{xz|||{}~~~}{|z|}}{|z{|zyx|}zy{}{yz}~}~|{~|||xvtw~ \ No newline at end of file