change to 16khz, add audio filter, refactor to use DMA
This commit is contained in:
+206
-26
@@ -2883,6 +2883,12 @@
|
|||||||
(color 0 0 0 0)
|
(color 0 0 0 0)
|
||||||
(uuid "27e5f52e-0ae1-42be-b638-2ba1b684ea4c")
|
(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
|
(junction
|
||||||
(at 339.09 76.18)
|
(at 339.09 76.18)
|
||||||
(diameter 0)
|
(diameter 0)
|
||||||
@@ -2947,16 +2953,6 @@
|
|||||||
)
|
)
|
||||||
(uuid "0a315227-f5b7-4de4-9dc2-5a9da0d1e499")
|
(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
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 342.9 92.69) (xy 342.9 92.61)
|
(xy 342.9 92.69) (xy 342.9 92.61)
|
||||||
@@ -3067,6 +3063,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "39ffb2c8-fe04-4380-a8d4-b2edce0fe6c0")
|
(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
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 207.01 201.93) (xy 207.01 204.47)
|
(xy 207.01 201.93) (xy 207.01 204.47)
|
||||||
@@ -3187,6 +3193,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "5c62cb6a-8252-484a-be0e-c05c74bf8a38")
|
(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
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 231.14 229.87) (xy 243.84 229.87)
|
(xy 231.14 229.87) (xy 243.84 229.87)
|
||||||
@@ -3267,6 +3283,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "8f5e7827-d21e-468e-b028-9b178eb97914")
|
(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
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 330.2 135.89) (xy 330.2 135.97)
|
(xy 330.2 135.89) (xy 330.2 135.97)
|
||||||
@@ -3307,6 +3333,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "a2cef328-0826-46a6-997f-fcedb293b343")
|
(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
|
(wire
|
||||||
(pts
|
(pts
|
||||||
(xy 207.01 204.47) (xy 223.52 204.47)
|
(xy 207.01 204.47) (xy 223.52 204.47)
|
||||||
@@ -3547,6 +3583,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "02b9416a-d87a-410f-8104-ff4ee4156d8d")
|
(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"
|
(label "GND"
|
||||||
(at 334.02 82.55 0)
|
(at 334.02 82.55 0)
|
||||||
(effects
|
(effects
|
||||||
@@ -3587,6 +3633,16 @@
|
|||||||
)
|
)
|
||||||
(uuid "20059635-02ae-4ce0-8d0b-fbe46035b4f2")
|
(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"
|
(label "GND"
|
||||||
(at 223.52 209.55 180)
|
(at 223.52 209.55 180)
|
||||||
(effects
|
(effects
|
||||||
@@ -3727,16 +3783,6 @@
|
|||||||
)
|
)
|
||||||
(uuid "663e9f4c-2079-4248-a551-ced079e7c1e8")
|
(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"
|
(label "C_BTN1"
|
||||||
(at 223.52 201.93 180)
|
(at 223.52 201.93 180)
|
||||||
(effects
|
(effects
|
||||||
@@ -4067,6 +4113,74 @@
|
|||||||
)
|
)
|
||||||
(uuid "fcb61f0a-8efc-4687-bfbd-16fb3032447e")
|
(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
|
(symbol
|
||||||
(lib_id "Jumper:SolderJumper_2_Bridged")
|
(lib_id "Jumper:SolderJumper_2_Bridged")
|
||||||
(at 227.33 232.41 0)
|
(at 227.33 232.41 0)
|
||||||
@@ -4205,7 +4319,7 @@
|
|||||||
)
|
)
|
||||||
(symbol
|
(symbol
|
||||||
(lib_id "bm:C")
|
(lib_id "bm:C")
|
||||||
(at 313.69 162.56 270)
|
(at 309.88 162.56 270)
|
||||||
(unit 1)
|
(unit 1)
|
||||||
(exclude_from_sim no)
|
(exclude_from_sim no)
|
||||||
(in_bom yes)
|
(in_bom yes)
|
||||||
@@ -4213,7 +4327,7 @@
|
|||||||
(dnp no)
|
(dnp no)
|
||||||
(uuid "16705866-bee7-4f26-a10e-6f7bfbff4c09")
|
(uuid "16705866-bee7-4f26-a10e-6f7bfbff4c09")
|
||||||
(property "Reference" "C5"
|
(property "Reference" "C5"
|
||||||
(at 310.896 165.608 0)
|
(at 307.086 165.608 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4221,7 +4335,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Value" "0.1uF"
|
(property "Value" "0.1uF"
|
||||||
(at 313.944 167.64 0)
|
(at 310.134 167.64 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4229,7 +4343,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Footprint" "Capacitor_SMD:C_0805_2012Metric"
|
(property "Footprint" "Capacitor_SMD:C_0805_2012Metric"
|
||||||
(at 313.69 162.56 0)
|
(at 309.88 162.56 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4238,7 +4352,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Datasheet" ""
|
(property "Datasheet" ""
|
||||||
(at 313.69 162.56 0)
|
(at 309.88 162.56 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(size 1.27 1.27)
|
||||||
@@ -4247,7 +4361,7 @@
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
(property "Description" ""
|
(property "Description" ""
|
||||||
(at 313.69 162.56 0)
|
(at 309.88 162.56 0)
|
||||||
(effects
|
(effects
|
||||||
(font
|
(font
|
||||||
(size 1.27 1.27)
|
(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
|
(symbol
|
||||||
(lib_id "Jumper:SolderJumper_2_Bridged")
|
(lib_id "Jumper:SolderJumper_2_Bridged")
|
||||||
(at 227.33 204.47 0)
|
(at 227.33 204.47 0)
|
||||||
|
|||||||
+116
-165
@@ -149,7 +149,7 @@ const uint8_t BTN_PINS[] = {
|
|||||||
// AUDIO CONFIG
|
// AUDIO CONFIG
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
#define SAMPLE_RATE 8000
|
#define SAMPLE_RATE 16000
|
||||||
#define AUDIO_BUF_SIZE 512 // larger buffer = fewer SPI transactions
|
#define AUDIO_BUF_SIZE 512 // larger buffer = fewer SPI transactions
|
||||||
#define SILENCE 128 // 0x80 = center for unsigned 8-bit
|
#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];
|
uint32_t g_trackLen[MAX_TRACKS];
|
||||||
uint8_t g_currentTrack = 0;
|
uint8_t g_currentTrack = 0;
|
||||||
|
|
||||||
// Audio double-buffer
|
// Audio DMA buffers — uint16_t PWM duty-cycle values for EasyDMA
|
||||||
uint8_t g_audioBuf[2][AUDIO_BUF_SIZE];
|
static uint16_t g_pwmBuf[2][AUDIO_BUF_SIZE];
|
||||||
volatile uint8_t g_activeBuf = 0;
|
|
||||||
volatile uint16_t g_bufPos = 0;
|
|
||||||
volatile bool g_bufReady[2] = {false, false};
|
volatile bool g_bufReady[2] = {false, false};
|
||||||
uint32_t g_nextReadAddr = 0;
|
uint32_t g_nextReadAddr = 0;
|
||||||
uint32_t g_playEnd = 0;
|
uint32_t g_playEnd = 0;
|
||||||
@@ -633,6 +631,7 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void setupBLE() {
|
void setupBLE() {
|
||||||
|
/* Note BLE requires a custom app to communicate, not regular bluetooth */
|
||||||
Bluefruit.begin();
|
Bluefruit.begin();
|
||||||
Bluefruit.setName("BabyMobile");
|
Bluefruit.setName("BabyMobile");
|
||||||
Bluefruit.setTxPower(0); // 0 dBm — save power, short range is fine
|
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)
|
#define PWM_COUNTERTOP 500 // 16 MHz / 500 = 32 kHz carrier
|
||||||
// We use the nrf_pwm peripheral directly for audio output
|
#define PWM_SILENCE 250 // midpoint of 0–500 duty-cycle range
|
||||||
// and a TIMER for the sample rate interrupt.
|
|
||||||
|
|
||||||
// For Arduino compatibility, we use analogWrite for the PWM
|
// Software gain: 1=unity, 2=2x, etc. Adjustable via 'u'/'d' serial.
|
||||||
// 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.
|
|
||||||
static uint8_t g_audioGain = 1;
|
static uint8_t g_audioGain = 1;
|
||||||
|
|
||||||
// Called from loop() every 125µs while g_playing
|
// Read PCM from current source into g_pwmBuf[b], applying gain and scaling.
|
||||||
void audioTick(void) {
|
// Pads the tail with silence if fewer than AUDIO_BUF_SIZE bytes remain.
|
||||||
// Output sample with software gain (stretches away from center 128)
|
static void audioFillBuf(uint8_t b) {
|
||||||
int16_t s = (int16_t)g_audioBuf[g_activeBuf][g_bufPos] - 128;
|
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;
|
s *= g_audioGain;
|
||||||
if (s > 127) s = 127;
|
if (s > 127) s = 127;
|
||||||
if (s < -128) s = -128;
|
if (s < -128) s = -128;
|
||||||
analogWrite(PIN_AUDIO_PWM, (uint8_t)(s + 128));
|
g_pwmBuf[b][i] = (uint16_t)((uint8_t)(s + 128)) * PWM_COUNTERTOP / 256;
|
||||||
g_bufPos++;
|
}
|
||||||
|
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) {
|
// PWM0 ISR — fires every 32 ms (512 samples at 16 kHz).
|
||||||
g_bufReady[g_activeBuf] = false;
|
// Just signals which buffer needs refilling; loop() does the actual I/O.
|
||||||
g_activeBuf ^= 1;
|
extern "C" void PWM0_IRQHandler() {
|
||||||
g_bufPos = 0;
|
if (NRF_PWM0->EVENTS_SEQEND[0]) {
|
||||||
|
NRF_PWM0->EVENTS_SEQEND[0] = 0;
|
||||||
if (!g_bufReady[g_activeBuf]) {
|
if (g_playing) {
|
||||||
// Buffer underrun — stop cleanly
|
g_bufReady[0] = false;
|
||||||
g_playing = false;
|
NRF_PWM0->TASKS_SEQSTART[1] = 1; // start buf 1 immediately
|
||||||
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
}
|
||||||
|
}
|
||||||
|
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() {
|
void audioInit() {
|
||||||
pinMode(PIN_AUDIO_PWM, OUTPUT);
|
// Silence both DMA buffers
|
||||||
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
for (int i = 0; i < AUDIO_BUF_SIZE; i++) {
|
||||||
analogWriteResolution(8); // 8-bit PWM
|
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) {
|
void audioStart(uint8_t trackNum) {
|
||||||
Serial.print("Playing track ");
|
Serial.print("Playing track ");
|
||||||
Serial.print(trackNum);
|
Serial.print(trackNum + 1);
|
||||||
Serial.print(" of ");
|
Serial.print(" of ");
|
||||||
Serial.println(g_numTracks);
|
Serial.println(g_numTracks);
|
||||||
if (trackNum >= g_numTracks) return;
|
if (trackNum >= g_numTracks) return;
|
||||||
|
|
||||||
g_currentTrack = trackNum;
|
g_currentTrack = trackNum;
|
||||||
g_playEnd = g_trackLen[trackNum];
|
|
||||||
g_nextReadAddr = 0;
|
|
||||||
|
|
||||||
#ifdef POC_INTERNAL_FLASH
|
#ifdef POC_INTERNAL_FLASH
|
||||||
if (g_pocFile) g_pocFile.close();
|
if (g_pocFile) g_pocFile.close();
|
||||||
char fname[16];
|
char fname[16];
|
||||||
pocFilename(trackNum, fname);
|
pocFilename(trackNum, fname);
|
||||||
if (!g_pocFile.open(fname, FILE_O_READ)) {
|
if (!g_pocFile.open(fname, FILE_O_READ)) {
|
||||||
Serial.print("audioStart: cannot open ");
|
Serial.print("audioStart: cannot open "); Serial.println(fname);
|
||||||
Serial.println(fname);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
g_nextReadAddr = 0;
|
||||||
// Pre-fill buf 0
|
g_playEnd = g_trackLen[trackNum];
|
||||||
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;
|
|
||||||
}
|
|
||||||
#else
|
#else
|
||||||
uint32_t start = g_trackStart[trackNum];
|
g_nextReadAddr = g_trackStart[trackNum];
|
||||||
g_playEnd = start + g_trackLen[trackNum];
|
g_playEnd = g_nextReadAddr + 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;
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
g_activeBuf = 0;
|
// Stop DMA cleanly, fill both buffers, restart
|
||||||
g_bufPos = 0;
|
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
|
// Enable amplifier
|
||||||
digitalWrite(PIN_AMP_SD, HIGH);
|
digitalWrite(PIN_AMP_SD, HIGH);
|
||||||
delay(10);
|
delay(10);
|
||||||
|
|
||||||
g_playing = true;
|
g_playing = true;
|
||||||
g_nextSampleUs = micros();
|
NRF_PWM0->TASKS_SEQSTART[0] = 1;
|
||||||
|
|
||||||
Serial.print("Playing track ");
|
|
||||||
Serial.println(trackNum + 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void audioStop() {
|
void audioStop() {
|
||||||
g_playing = false;
|
g_playing = false;
|
||||||
analogWrite(PIN_AUDIO_PWM, SILENCE);
|
NRF_PWM0->TASKS_STOP = 1;
|
||||||
|
|
||||||
#ifdef POC_INTERNAL_FLASH
|
#ifdef POC_INTERNAL_FLASH
|
||||||
if (g_pocFile) g_pocFile.close();
|
if (g_pocFile) g_pocFile.close();
|
||||||
@@ -804,7 +831,6 @@ void audioStop() {
|
|||||||
|
|
||||||
// Disable amp
|
// Disable amp
|
||||||
digitalWrite(PIN_AMP_SD, LOW);
|
digitalWrite(PIN_AMP_SD, LOW);
|
||||||
|
|
||||||
Serial.println("Playback stopped");
|
Serial.println("Playback stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -856,16 +882,7 @@ uint8_t buttonRead() {
|
|||||||
|
|
||||||
void waitButtonRelease(uint8_t btn) {
|
void waitButtonRelease(uint8_t btn) {
|
||||||
unsigned long deadline = millis() + 300;
|
unsigned long deadline = millis() + 300;
|
||||||
while (buttonRead() == btn && millis() < deadline) {
|
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -982,59 +999,14 @@ void setup() {
|
|||||||
audioInit();
|
audioInit();
|
||||||
|
|
||||||
#ifdef POC_INTERNAL_FLASH
|
#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) {
|
if (g_numTracks > 0) {
|
||||||
char diagName[16]; pocFilename(0, diagName);
|
Serial.println("Diag: playing track0 for 3s...");
|
||||||
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...");
|
|
||||||
audioStart(0);
|
audioStart(0);
|
||||||
uint32_t diagEnd = millis() + 3000;
|
uint32_t diagEnd = millis() + 3000;
|
||||||
while (millis() < diagEnd && g_playing) {
|
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++) {
|
for (uint8_t b = 0; b < 2; b++) {
|
||||||
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
|
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
|
||||||
uint32_t toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr);
|
audioFillBuf(b);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1212,43 +1184,22 @@ static void serUploadTick() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
// ---- Audio sample output (8kHz, loop-driven) ----
|
if (g_playing) g_lastActivity = millis();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- Serial upload (POC mode) ----
|
// ---- Serial upload (POC mode) ----
|
||||||
#ifdef POC_INTERNAL_FLASH
|
#ifdef POC_INTERNAL_FLASH
|
||||||
serUploadTick();
|
serUploadTick();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// ---- Refill audio buffers ----
|
// ---- Refill audio buffers (ISR sets g_bufReady[b]=false when buffer done) ----
|
||||||
if (g_playing) {
|
if (g_playing) {
|
||||||
for (uint8_t b = 0; b < 2; b++) {
|
for (uint8_t b = 0; b < 2; b++) {
|
||||||
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
|
if (!g_bufReady[b] && g_nextReadAddr < g_playEnd) {
|
||||||
uint32_t remaining = g_playEnd - g_nextReadAddr;
|
audioFillBuf(b);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
if (!g_bufReady[0] && !g_bufReady[1] && g_nextReadAddr >= g_playEnd) {
|
||||||
audioStop();
|
audioStop();
|
||||||
if (g_numTracks > 0) {
|
if (g_numTracks > 0) {
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ def convert_to_pcm(path: str) -> bytes:
|
|||||||
sample_rate = struct.unpack_from("<I", data, 24)[0]
|
sample_rate = struct.unpack_from("<I", data, 24)[0]
|
||||||
bits_per_samp = struct.unpack_from("<H", data, 34)[0]
|
bits_per_samp = struct.unpack_from("<H", data, 34)[0]
|
||||||
needs_convert = (audio_format != 1 or num_channels != 1
|
needs_convert = (audio_format != 1 or num_channels != 1
|
||||||
or sample_rate != 8000 or bits_per_samp != 8)
|
or sample_rate != 16000 or bits_per_samp != 8)
|
||||||
except Exception:
|
except Exception:
|
||||||
needs_convert = True
|
needs_convert = True
|
||||||
|
|
||||||
if not needs_convert:
|
if not needs_convert:
|
||||||
print("WAV is already 8-bit mono 8kHz — stripping header")
|
print("WAV is already 8-bit mono 16kHz — stripping header")
|
||||||
return data[44:]
|
return data[44:]
|
||||||
print("WAV needs resampling — converting via ffmpeg")
|
print("WAV needs resampling — converting via ffmpeg")
|
||||||
# fall through to ffmpeg conversion
|
# fall through to ffmpeg conversion
|
||||||
@@ -64,14 +64,14 @@ def convert_to_pcm(path: str) -> bytes:
|
|||||||
print(f"Converting {os.path.basename(path)} via ffmpeg...")
|
print(f"Converting {os.path.basename(path)} via ffmpeg...")
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
['ffmpeg', '-y', '-i', path,
|
['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
|
capture_output=True
|
||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
sys.exit(f"ERROR: ffmpeg failed:\n{result.stderr.decode(errors='replace')[-400:]}")
|
sys.exit(f"ERROR: ffmpeg failed:\n{result.stderr.decode(errors='replace')[-400:]}")
|
||||||
|
|
||||||
pcm = result.stdout
|
pcm = result.stdout
|
||||||
duration = len(pcm) / 8000
|
duration = len(pcm) / 16000
|
||||||
print(f" Converted: {len(pcm):,} bytes ({duration:.1f}s)")
|
print(f" Converted: {len(pcm):,} bytes ({duration:.1f}s)")
|
||||||
return pcm
|
return pcm
|
||||||
|
|
||||||
@@ -133,10 +133,10 @@ def upload(port: str, track_num: int, pcm: bytes) -> None:
|
|||||||
if "at=" in err_line:
|
if "at=" in err_line:
|
||||||
accepted = int(err_line.split("at=")[1].split()[0])
|
accepted = int(err_line.split("at=")[1].split()[0])
|
||||||
safe = int(accepted * 0.9) # 10% headroom
|
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" LittleFS accepted {accepted} bytes before full.")
|
||||||
print(f" Safe clip length: ~{max_s:.1f}s")
|
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)
|
sys.exit(1)
|
||||||
|
|
||||||
# Wait for OK
|
# Wait for OK
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user