diff --git a/baby_mobile_v2/baby_mobile_v2.ino b/baby_mobile_v2/baby_mobile_v2.ino index 7195885..d91e2b3 100644 --- a/baby_mobile_v2/baby_mobile_v2.ino +++ b/baby_mobile_v2/baby_mobile_v2.ino @@ -13,8 +13,8 @@ * - Deep sleep with button wake (~10µA) * - Auto-shutoff timer * - * Audio format: unsigned 8-bit PCM, 8000 Hz, mono - * Convert: ffmpeg -i song.mp3 -ar 8000 -ac 1 -f u8 -acodec pcm_u8 song.raw + * Audio format: WAV (8 or 16-bit PCM, 8/16/32 kHz, mono) stored in LittleFS. + * Upload via upload_track.py — ffmpeg handles any source format. * * Board setup in Arduino IDE: * Board: "Seeed XIAO nRF52840" (or "Adafruit Feather nRF52840") @@ -50,8 +50,9 @@ // ============================================================ // POC MODE: stream audio from internal LittleFS instead of SPI flash. -// Upload tracks via BLE (CMD 0x02 [track_num] + data packets). -// WAV files are accepted — 44-byte PCM header is stripped on receipt. +// Upload tracks via upload_track.py (serial) or BLE (CMD 0x02). +// WAV files are stored intact; header is parsed at load time for +// dynamic bit-depth (8/16-bit) and sample-rate support. // Disable this define to revert to full SPI flash mode. // ============================================================ #define POC_INTERNAL_FLASH @@ -77,14 +78,14 @@ using namespace Adafruit_LittleFS_Namespace; #define PIN_MOTOR_PWM 15 // PWM to MOSFET gate // SPI Flash -#define PIN_FLASH_CS 2 // Flash chip select -// SPI MOSI/MISO/SCK use default SPI pins +#define PIN_FLASH_CS 4 // Flash chip select +// SPI MOSI/MISO/SCK use default SPI pins (8/9/10?) // Buttons (directly to GPIO, active LOW with internal pull-up) -#define PIN_BTN1 4 +#define PIN_BTN1 1 #define PIN_BTN2 1 -#define PIN_BTN3 5 -#define PIN_BTN4 3 +#define PIN_BTN3 1 +#define PIN_BTN4 1 /* #define PIN_BTN5 4 #define PIN_BTN6 5 @@ -189,7 +190,6 @@ volatile uint32_t g_bleWriteLen = 0; #ifdef POC_INTERNAL_FLASH File g_pocFile(InternalFS); // open file handle (read or write) uint8_t g_pocWriteTrack = 0; // track slot being written via BLE -bool g_pocSkipHeader = false; // strip WAV header from first data packet #endif // ============================================================ @@ -202,7 +202,10 @@ uint8_t g_motorSpeed = 160; uint8_t g_numTracks = 0; uint32_t g_trackStart[MAX_TRACKS]; -uint32_t g_trackLen[MAX_TRACKS]; +uint32_t g_trackLen[MAX_TRACKS]; // total file size in bytes (including WAV header) +uint8_t g_trackBits[MAX_TRACKS]; // bits per sample: 8 or 16 +uint32_t g_trackRate[MAX_TRACKS]; // sample rate in Hz +uint32_t g_trackDataOff[MAX_TRACKS]; // byte offset of audio data within file (44 for WAV, 0 for raw) uint8_t g_currentTrack = 0; bool g_loopTracks = false; // false = play once and stop; true = auto-advance through all tracks @@ -223,7 +226,7 @@ volatile bool g_usbConnected = false; #ifdef POC_INTERNAL_FLASH static void pocFilename(uint8_t n, char *buf) { // buf must be >=16 bytes - snprintf(buf, 16, "/track%d.raw", n); + snprintf(buf, 16, "/track%d.wav", n); } #endif @@ -347,10 +350,39 @@ void loadTrackTable() { pocFilename(i, fname); File f(InternalFS); if (!f.open(fname, FILE_O_READ)) break; - g_trackLen[i] = f.size(); + g_trackLen[i] = f.size(); + g_trackBits[i] = 8; + g_trackRate[i] = SAMPLE_RATE; + g_trackDataOff[i] = 0; + + // Parse WAV header to extract format metadata + if (g_trackLen[i] >= 44) { + uint8_t hdr[44]; + f.seek(0); + f.read(hdr, 44); + if (hdr[0]=='R' && hdr[1]=='I' && hdr[2]=='F' && hdr[3]=='F') { + uint16_t bits = (uint16_t)hdr[34] | ((uint16_t)hdr[35] << 8); + uint32_t rate = (uint32_t)hdr[24] | ((uint32_t)hdr[25] << 8) + | ((uint32_t)hdr[26] << 16) | ((uint32_t)hdr[27] << 24); + if ((bits == 8 || bits == 16) && rate > 0) { + g_trackBits[i] = (uint8_t)bits; + g_trackRate[i] = rate; + g_trackDataOff[i] = 44; + } + } + } f.close(); + if (g_trackLen[i] == 0) break; // stop at first empty file g_numTracks = i + 1; + + Serial.print(" track"); Serial.print(i); + Serial.print(": "); Serial.print(g_trackBits[i]); Serial.print("bit "); + Serial.print(g_trackRate[i] / 1000); Serial.print("kHz "); + uint32_t audioBytes = g_trackLen[i] - g_trackDataOff[i]; + uint32_t bytesPerSample = g_trackBits[i] / 8; + Serial.print((audioBytes / bytesPerSample) / g_trackRate[i]); + Serial.println("s"); } Serial.print("Found "); Serial.print(g_numTracks); @@ -495,10 +527,13 @@ void ble_disconnect_cb(uint16_t conn_handle, uint8_t reason) { // BLE command characteristic: receives commands // CMD 0x01 [num_tracks] [track_entries...] = write track table -// CMD 0x02 [addr_3bytes] = start writing audio at address +// CMD 0x02 [track_num] = start writing audio to track slot // CMD 0x03 = finish upload, reload tracks // CMD 0x04 [track_num] = play track // CMD 0x05 = stop playback +// CMD 0x06 = list tracks → audioStat notifications: +// [0x80|idx, bits, rate_kHz, dur_s] per track, then [0xFF, count, 0, 0] +// CMD 0x07 [track_num] = delete track → audioStat: [0xD0, idx, ok, 0] void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr, uint8_t* data, uint16_t len) { if (len < 1) return; @@ -535,7 +570,6 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr, break; } g_bleUploading = true; - g_pocSkipHeader = true; g_bleWriteLen = 0; Serial.print("BLE: Start write track "); Serial.println(g_pocWriteTrack); @@ -555,7 +589,6 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr, case 0x03: // Finish upload #ifdef POC_INTERNAL_FLASH if (g_pocFile) g_pocFile.close(); - g_pocSkipHeader = false; #endif g_bleUploading = false; loadTrackTable(); @@ -572,6 +605,57 @@ void audioCmd_write_cb(uint16_t conn_handle, BLECharacteristic* chr, audioStop(); motorStop(); break; + + case 0x06: // List tracks + // Responds via audioStat notifications (one per track + end marker). + // Per-track packet: [0x80|idx, bits, rate_kHz, duration_s] + // End packet: [0xFF, num_tracks, 0, 0] + // Byte 0 >= 0x80 distinguishes list responses from upload-progress + // packets (which always have byte 0 == 0x00 for files < 16 MB). +#ifdef POC_INTERNAL_FLASH + for (uint8_t i = 0; i < g_numTracks; i++) { + uint32_t audioBytes = g_trackLen[i] - g_trackDataOff[i]; + uint32_t bps = g_trackBits[i] / 8; + uint32_t durS = (bps > 0 && g_trackRate[i] > 0) + ? (audioBytes / bps) / g_trackRate[i] : 0; + uint8_t pkt[4] = { + (uint8_t)(0x80 | i), + g_trackBits[i], + (uint8_t)(g_trackRate[i] / 1000), + (uint8_t)min(durS, (uint32_t)255) + }; + audioStat.write(pkt, 4); + audioStat.notify(pkt, 4); + delay(20); + } + { + uint8_t end[4] = {0xFF, g_numTracks, 0, 0}; + audioStat.write(end, 4); + audioStat.notify(end, 4); + } + Serial.print("BLE: Listed "); Serial.print(g_numTracks); Serial.println(" tracks"); +#endif + break; + + case 0x07: // Delete track + // data[1] = track index to delete. + // Responds via audioStat: [0xD0, track_idx, success(0/1), 0] +#ifdef POC_INTERNAL_FLASH + if (len >= 2) { + uint8_t trkNum = data[1]; + char fname[16]; + pocFilename(trkNum, fname); + bool ok = InternalFS.remove(fname); + if (ok) loadTrackTable(); + uint8_t resp[4] = {0xD0, trkNum, (uint8_t)(ok ? 1 : 0), 0}; + audioStat.write(resp, 4); + audioStat.notify(resp, 4); + Serial.print("BLE: Delete track "); + Serial.print(trkNum); + Serial.println(ok ? " OK" : " FAILED"); + } +#endif + break; } } @@ -582,25 +666,8 @@ void audioData_write_cb(uint16_t conn_handle, BLECharacteristic* chr, g_lastActivity = millis(); #ifdef POC_INTERNAL_FLASH - uint8_t *src = data; - uint16_t srcLen = len; - - // Strip 44-byte WAV header from first packet if present - if (g_pocSkipHeader) { - g_pocSkipHeader = false; - if (srcLen >= 4 && src[0]=='R' && src[1]=='I' && src[2]=='F' && src[3]=='F') { - if (srcLen > 44) { - src += 44; - srcLen -= 44; - } else { - // Header spans packets — drop whole packet (document: send raw PCM instead) - return; - } - } - } - - g_pocFile.write(src, srcLen); - g_bleWriteLen += srcLen; + g_pocFile.write(data, len); + g_bleWriteLen += len; #else // Erase new sectors as we cross boundaries uint32_t endAddr = g_bleWriteAddr + len; @@ -697,32 +764,63 @@ void setupBLE() { static uint8_t g_audioGain = 1; // Read PCM into g_pwmBuf[b], pad tail with silence. +// Supports 8-bit unsigned and 16-bit signed WAV; format read from g_trackBits[]. // On the first pure-silence fill (track exhausted), arms the stop timer. static void audioFillBuf(uint8_t b) { - uint8_t pcm[AUDIO_BUF_SIZE]; - uint32_t toRead = 0; + bool is16 = (g_trackBits[g_currentTrack] == 16); + uint32_t bytesPerSample = is16 ? 2 : 1; + uint32_t toReadBytes = 0; + if (g_nextReadAddr < g_playEnd) { - toRead = min((uint32_t)AUDIO_BUF_SIZE, g_playEnd - g_nextReadAddr); + uint32_t remaining = g_playEnd - g_nextReadAddr; + toReadBytes = min((uint32_t)(AUDIO_BUF_SIZE * bytesPerSample), remaining); + if (is16) toReadBytes &= ~1u; // keep sample-aligned + } + + uint32_t samples = toReadBytes / bytesPerSample; + + if (is16) { + uint8_t raw[AUDIO_BUF_SIZE * 2]; + if (toReadBytes) { #ifdef POC_INTERNAL_FLASH - g_pocFile.read(pcm, toRead); + g_pocFile.read(raw, toReadBytes); #else - flashReadBytes(g_nextReadAddr, pcm, toRead); + flashReadBytes(g_nextReadAddr, raw, toReadBytes); #endif - g_nextReadAddr += toRead; + g_nextReadAddr += toReadBytes; + } + for (uint32_t i = 0; i < samples; i++) { + int16_t s = (int16_t)((uint16_t)raw[i * 2] | ((uint16_t)raw[i * 2 + 1] << 8)); + int32_t sv = (int32_t)s * g_audioGain; + if (sv > 32767) sv = 32767; + if (sv < -32768) sv = -32768; + g_pwmBuf[b][i] = (uint16_t)(((uint32_t)(sv + 32768)) * PWM_COUNTERTOP / 65536); + } + } else { + uint8_t pcm[AUDIO_BUF_SIZE]; + if (toReadBytes) { +#ifdef POC_INTERNAL_FLASH + g_pocFile.read(pcm, toReadBytes); +#else + flashReadBytes(g_nextReadAddr, pcm, toReadBytes); +#endif + g_nextReadAddr += toReadBytes; + } + for (uint32_t i = 0; i < samples; 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 = 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++) { + + for (uint32_t i = samples; i < AUDIO_BUF_SIZE; i++) { g_pwmBuf[b][i] = PWM_SILENCE; } g_bufReady[b] = true; // Arm stop timer on first pure-silence fill (all audio already sent to DMA) - if (toRead == 0 && g_trackDoneMs == 0 && g_playing) { + if (toReadBytes == 0 && g_trackDoneMs == 0 && g_playing) { g_trackDoneMs = millis() + 100; // 100ms > 3 buffer lengths (3 × 32ms) } } @@ -796,13 +894,25 @@ void audioStart(uint8_t trackNum) { Serial.print("audioStart: cannot open "); Serial.println(fname); return; } - g_nextReadAddr = 0; + g_pocFile.seek(g_trackDataOff[trackNum]); + g_nextReadAddr = g_trackDataOff[trackNum]; g_playEnd = g_trackLen[trackNum]; #else - g_nextReadAddr = g_trackStart[trackNum]; - g_playEnd = g_nextReadAddr + g_trackLen[trackNum]; + g_nextReadAddr = g_trackStart[trackNum] + g_trackDataOff[trackNum]; + g_playEnd = g_trackStart[trackNum] + g_trackLen[trackNum]; #endif + // Set PWM REFRESH for this track's sample rate. + // carrier = 32kHz; effective_rate = 32000 / (REFRESH + 1) + // 16kHz → REFRESH=1, 8kHz → REFRESH=3, 32kHz → REFRESH=0 + { + uint32_t rate = g_trackRate[trackNum]; + if (rate == 0) rate = SAMPLE_RATE; + uint8_t refresh = (uint8_t)((32000u / rate) - 1); + NRF_PWM0->SEQ[0].REFRESH = refresh; + NRF_PWM0->SEQ[1].REFRESH = refresh; + } + g_trackDoneMs = 0; g_bufReady[0] = false; g_bufReady[1] = false; @@ -1044,7 +1154,6 @@ static uint8_t g_serLineLen = 0; static uint8_t g_serTrack = 0; static uint32_t g_serBytesExpected = 0; static uint32_t g_serBytesReceived = 0; -static bool g_serSkipHeader = false; static File g_serFile(InternalFS); static void serUploadTick() { @@ -1062,7 +1171,6 @@ static void serUploadTick() { g_serTrack = (uint8_t)utrk; g_serBytesExpected = (uint32_t)ulen; g_serBytesReceived = 0; - g_serSkipHeader = true; char fname[16]; pocFilename(g_serTrack, fname); @@ -1127,6 +1235,12 @@ static void serUploadTick() { Serial.print("ERR no file "); Serial.println(fname); } loadTrackTable(); + } else if (strcmp(g_serLineBuf, "FORMAT") == 0) { + audioStop(); + Serial.println("Formatting LittleFS..."); + InternalFS.format(); + g_numTracks = 0; + Serial.println("FORMAT OK"); } else { Serial.print("ERR bad cmd: "); Serial.println(g_serLineBuf); @@ -1149,20 +1263,8 @@ static void serUploadTick() { (int)(g_serBytesExpected - g_serBytesReceived))); if (n <= 0) break; - uint8_t *src = chunk; - uint16_t srcLen = (uint16_t)n; - - // Strip 44-byte WAV header from first chunk if present - if (g_serSkipHeader) { - g_serSkipHeader = false; - if (srcLen >= 4 && src[0]=='R' && src[1]=='I' && src[2]=='F' && src[3]=='F') { - if (srcLen > 44) { src += 44; srcLen -= 44; } - else { g_serBytesReceived += n; continue; } - } - } - - int32_t wr = g_serFile.write(src, srcLen); - if (wr != (int32_t)srcLen) { + int32_t wr = g_serFile.write(chunk, (uint16_t)n); + if (wr != n) { // Write failed (LittleFS full or error) — abort immediately. // Do NOT loop printing errors; that fills USB CDC TX and hangs. g_serFile.close(); diff --git a/baby_mobile_v2/upload_track.py b/baby_mobile_v2/upload_track.py index 4d4d0e5..0a4057f 100644 --- a/baby_mobile_v2/upload_track.py +++ b/baby_mobile_v2/upload_track.py @@ -5,10 +5,15 @@ upload_track.py — serial audio uploader for baby_mobile_v2 POC_INTERNAL_FLASH Usage: python upload_track.py -Supported formats: .raw, .wav, .mp3, .ogg, .flac, .aac, .m4a (anything ffmpeg handles) +Supported formats: .wav, .mp3, .ogg, .flac, .aac, .m4a, .raw (anything ffmpeg handles) + +Output format: 16-bit signed PCM WAV, 16 kHz, mono (WAV header included). + - Already-conformant WAV files (PCM, mono, 8 or 16-bit, 8/16/32 kHz) are sent as-is. + - All other formats are converted via ffmpeg to 16-bit 16 kHz mono WAV. + - .raw files are treated as legacy 8-bit unsigned 16 kHz and wrapped in a WAV header. Requires: pyserial (pip install pyserial) - ffmpeg in PATH for non-WAV/RAW files + ffmpeg in PATH for non-WAV/RAW files or non-conformant WAV files """ import sys @@ -23,69 +28,96 @@ CHUNK_SIZE = 512 BAUD_RATE = 115200 TIMEOUT_S = 10.0 -RAW_EXTS = {'.raw'} -WAV_EXTS = {'.wav'} +# Sample rates the firmware PWM can reproduce (32kHz carrier / (REFRESH+1)) +SUPPORTED_RATES = {8000, 16000, 32000} -def convert_to_pcm(path: str) -> bytes: +def _make_wav_header(num_samples: int, sample_rate: int, bits: int) -> bytes: + """Build a minimal 44-byte PCM WAV header.""" + num_channels = 1 + byte_rate = sample_rate * num_channels * bits // 8 + block_align = num_channels * bits // 8 + data_size = num_samples * block_align + chunk_size = 36 + data_size + return struct.pack('<4sI4s4sIHHIIHH4sI', + b'RIFF', chunk_size, b'WAVE', + b'fmt ', 16, 1, num_channels, sample_rate, + byte_rate, block_align, bits, + b'data', data_size) + + +def load_wav(path: str) -> bytes: + """Return full WAV bytes (header + PCM data) ready to upload.""" ext = os.path.splitext(path)[1].lower() - if ext in RAW_EXTS: - with open(path, "rb") as f: - return f.read() + # ── Legacy .raw: 8-bit unsigned 16 kHz mono ───────────────────────────── + if ext == '.raw': + with open(path, 'rb') as f: + pcm = f.read() + print(f"RAW file: wrapping {len(pcm):,} bytes as 8-bit 16 kHz mono WAV") + header = _make_wav_header(len(pcm), 16000, 8) + return header + pcm - if ext in WAV_EXTS: - with open(path, "rb") as f: + # ── WAV: check if already conformant ──────────────────────────────────── + if ext == '.wav': + with open(path, 'rb') as f: data = f.read() - if data[:4] == b"RIFF": + if data[:4] == b'RIFF' and len(data) >= 44: try: - audio_format = struct.unpack_from(" 16-bit 16 kHz mono WAV...") result = subprocess.run( ['ffmpeg', '-y', '-i', path, - '-ar', '16000', '-ac', '1', '-f', 'u8', '-acodec', 'pcm_u8', 'pipe:1'], + '-ar', '16000', '-ac', '1', '-f', 'wav', '-acodec', 'pcm_s16le', '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) / 16000 - print(f" Converted: {len(pcm):,} bytes ({duration:.1f}s)") - return pcm + wav = result.stdout + if len(wav) >= 44: + bits = struct.unpack_from(' None: - total = len(pcm) +def upload(port: str, track_num: int, wav: bytes) -> None: + total = len(wav) print(f"Uploading {total} bytes as track {track_num} via {port} ...") - # dsrdtr=False / rtscts=False prevents Windows from toggling DTR/RTS on - # port open, which would reset the nRF52840 and stall the 5-second boot loop. with serial.Serial(port, BAUD_RATE, timeout=2.0, dsrdtr=False, rtscts=False) as ser: - # Drain any boot output; send a bare newline first to flush any - # partial line sitting in the firmware's line buffer. time.sleep(0.5) ser.reset_input_buffer() ser.write(b"\n") @@ -98,7 +130,7 @@ def upload(port: str, track_num: int, pcm: bytes) -> None: ser.write(cmd.encode()) ser.flush() - # Wait for READY — print everything received so failures are diagnosable + # Wait for READY deadline = time.time() + TIMEOUT_S while True: if time.time() > deadline: @@ -109,17 +141,16 @@ def upload(port: str, track_num: int, pcm: bytes) -> None: if line: print(f" firmware: {line}") - # Stream PCM in chunks; stop early if firmware sends ERR + # Stream WAV data in chunks; poll for ERR sent = 0 err_line = None - ser.timeout = 0.05 # short timeout so we can poll for ERR while sending + ser.timeout = 0.05 while sent < total: - chunk = pcm[sent:sent + CHUNK_SIZE] + chunk = wav[sent:sent + CHUNK_SIZE] ser.write(chunk) sent += len(chunk) pct = sent * 100 // total print(f"\r {sent}/{total} bytes ({pct}%) ", end="", flush=True) - # Check for early ERR from firmware (e.g. LittleFS full) line = ser.readline().decode(errors="replace").strip() if line.startswith("ERR"): err_line = line @@ -129,14 +160,18 @@ def upload(port: str, track_num: int, pcm: bytes) -> None: if err_line: print(f"ERROR from firmware: {err_line}") - # at= tells us how many bytes were accepted before the failure if "at=" in err_line: accepted = int(err_line.split("at=")[1].split()[0]) - safe = int(accepted * 0.9) # 10% headroom - max_s = safe / 16000 + safe = int(accepted * 0.9) + # estimate audio bytes (subtract header) + audio_accepted = max(0, safe - 44) + bits = struct.unpack_from('= 44 else 16 + rate = struct.unpack_from('= 44 else 16000 + max_s = audio_accepted / (bits // 8) / rate 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 16000 -ac 1 -f u8 out.raw") + print(f" Trim with: ffmpeg -i input.mp3 -t {max_s:.0f} -ar 16000 -ac 1 " + f"-acodec pcm_s16le out.wav") sys.exit(1) # Wait for OK @@ -154,7 +189,7 @@ def upload(port: str, track_num: int, pcm: bytes) -> None: if line: print(f" firmware: {line}") - # Verify: request hex dump of first 256 bytes and compare + # Verify: hex-dump first 256 bytes and check WAV header is intact print("Verifying...") ser.reset_input_buffer() ser.write(f"DUMP {track_num}\n".encode()) @@ -178,7 +213,6 @@ def upload(port: str, track_num: int, pcm: bytes) -> None: print("ERROR: firmware says file does not exist!") break else: - # Parse hex bytes for tok in line.split(): try: fw_bytes.append(int(tok, 16)) @@ -194,14 +228,15 @@ def upload(port: str, track_num: int, pcm: bytes) -> None: else: print(f" Size OK: {fw_size}/{total}") - compare_len = min(len(fw_bytes), len(pcm), 256) + # Compare first 256 bytes (includes WAV header — confirms format metadata made it) + compare_len = min(len(fw_bytes), len(wav), 256) if compare_len > 0: - mismatches = sum(1 for i in range(compare_len) if fw_bytes[i] != pcm[i]) + mismatches = sum(1 for i in range(compare_len) if fw_bytes[i] != wav[i]) if mismatches == 0: - print(f" Data OK: first {compare_len} bytes match") + print(f" Data OK: first {compare_len} bytes match (WAV header intact)") else: print(f" ERROR: {mismatches}/{compare_len} byte mismatches in first {compare_len} bytes") - print(f" Expected: {' '.join(f'{b:02X}' for b in pcm[:16])}") + print(f" Expected: {' '.join(f'{b:02X}' for b in wav[:16])}") print(f" Got: {' '.join(f'{b:02X}' for b in fw_bytes[:16])}") @@ -217,11 +252,11 @@ def main(): if not 0 <= track_num < 32: sys.exit("track_num must be 0–31") - pcm = convert_to_pcm(path) - if not pcm: + wav = load_wav(path) + if not wav: sys.exit("File is empty after conversion") - upload(port, track_num, pcm) + upload(port, track_num, wav) if __name__ == "__main__":