// SPDX-License-Identifier: GPL-3.0-only #include "storage.h" #include #include "audio_config.h" #include "pins.h" namespace { // A dedicated SPI bus for the SD card so it does not clash with other peripherals. SPIClass sdSpi(HSPI); // Write a 44-byte canonical PCM WAV header. dataBytes may be 0 at open (patched on close). void writeWavHeader(File &f, uint32_t sampleRate, uint16_t bits, uint16_t channels, uint32_t dataBytes) { const uint32_t byteRate = sampleRate * channels * (bits / 8); const uint16_t blockAlign = channels * (bits / 8); const uint32_t riffSize = 36 + dataBytes; auto u32 = [&](uint32_t v) { f.write(reinterpret_cast(&v), 4); }; auto u16 = [&](uint16_t v) { f.write(reinterpret_cast(&v), 2); }; f.write(reinterpret_cast("RIFF"), 4); u32(riffSize); f.write(reinterpret_cast("WAVE"), 4); f.write(reinterpret_cast("fmt "), 4); u32(16); // PCM fmt chunk size u16(1); // audio format = PCM u16(channels); u32(sampleRate); u32(byteRate); u16(blockAlign); u16(bits); f.write(reinterpret_cast("data"), 4); u32(dataBytes); } } // namespace namespace storage { bool begin() { sdSpi.begin(PIN_SD_SCK, PIN_SD_MISO, PIN_SD_MOSI, PIN_SD_CS); if (!SD.begin(PIN_SD_CS, sdSpi)) { return false; } if (!SD.exists(OSC_REC_DIR)) { SD.mkdir(OSC_REC_DIR); } return true; } uint64_t totalBytes() { return SD.totalBytes(); } uint64_t freeBytes() { return SD.totalBytes() - SD.usedBytes(); } bool WavWriter::open(const String &path, uint32_t sampleRate, uint16_t bitsPerSample, uint16_t channels) { file_ = SD.open(path, FILE_WRITE); if (!file_) return false; sampleRate_ = sampleRate; bits_ = bitsPerSample; channels_ = channels; dataBytes_ = 0; writeWavHeader(file_, sampleRate_, bits_, channels_, 0); // placeholder sizes return true; } size_t WavWriter::write(const uint8_t *data, size_t len) { if (!file_) return 0; size_t n = file_.write(data, len); dataBytes_ += n; return n; } bool WavWriter::close() { if (!file_) return false; // Patch RIFF size (offset 4) and data size (offset 40) now that we know the length. file_.flush(); file_.seek(4); uint32_t riffSize = 36 + dataBytes_; file_.write(reinterpret_cast(&riffSize), 4); file_.seek(40); file_.write(reinterpret_cast(&dataBytes_), 4); file_.close(); return true; } bool writeMetadata(const String &path, const String &json) { File f = SD.open(path, FILE_WRITE); if (!f) return false; f.print(json); f.close(); return true; } String recWavPath(const String &id) { return String(OSC_REC_DIR) + "/" + id + ".wav"; } String recMetaPath(const String &id) { return String(OSC_REC_DIR) + "/" + id + ".json"; } String readFile(const String &path) { File f = SD.open(path, FILE_READ); if (!f) return String(); String s; s.reserve(f.size() + 1); while (f.available()) s += static_cast(f.read()); f.close(); return s; } bool removeRecording(const String &id) { bool a = SD.remove(recWavPath(id)); bool b = SD.remove(recMetaPath(id)); return a || b; } uint32_t countRecordings() { uint32_t n = 0; File dir = SD.open(OSC_REC_DIR); if (!dir) return 0; for (File f = dir.openNextFile(); f; f = dir.openNextFile()) { if (String(f.name()).endsWith(".wav")) n++; f.close(); } dir.close(); return n; } String listRecordingsJson() { String out = "{\"items\":["; bool first = true; File dir = SD.open(OSC_REC_DIR); if (dir) { for (File f = dir.openNextFile(); f; f = dir.openNextFile()) { if (String(f.name()).endsWith(".json")) { String content; content.reserve(f.size() + 1); while (f.available()) content += static_cast(f.read()); if (!first) out += ","; out += content; first = false; } f.close(); } dir.close(); } out += "],\"next_cursor\":null}"; return out; } } // namespace storage