// Ti-Pote — Audio I/O via a single full-duplex I2S bus. // // I2S_NUM_0 is configured as MASTER in RX+TX mode. BCLK and WS are // shared between the INMP441 microphone (RX) and the MAX98357A // amplifier (TX), which is the standard I2S bus layout — exactly // what was working on the Raspberry Pi side. // // Pin map (single shared I2S bus): // BCLK = GPIO 32 shared mic SCK + speaker BCLK // LRCLK / WS = GPIO 33 shared mic WS + speaker LRC // Mic data in = GPIO 34 INMP441 SD (input-only pin, perfect) // Speaker DOUT = GPIO 22 MAX98357A DIN // // Mic L/R stays tied to GND → talks on the LEFT slot of the I2S frame. // // Format exchanged with the Pi on the UART: // PCM signed 16-bit little-endian, mono, 16 kHz. // // Internally the bus runs at 32-bit stereo slots (INMP441 requires it). // readMicChunk() converts the 32-bit left slot down to S16 mono. // writeSpeakerChunk() expands S16 mono to 32-bit stereo frames before // handing them to i2s_write(). #pragma once #include #include #include namespace tipote { class Audio { public: static constexpr int SAMPLE_RATE = 16000; static constexpr int CHANNELS = 1; static constexpr int BYTES_PER_SAMPLE = 2; // S16 // Initialise both I2S ports. Safe to call exactly once from setup(). bool begin(); // Pull whatever the mic DMA has ready. Writes S16 mono little-endian // bytes into `out`, up to `outCapacity` bytes, and returns the number // of bytes actually written (always even, possibly zero). // // Non-blocking (timeout = 0). size_t readMicChunk(uint8_t* out, size_t outCapacity); // Push S16 mono little-endian PCM to the speaker DMA. Blocks up to // ~50 ms waiting for room. Returns bytes actually accepted. size_t writeSpeakerChunk(const uint8_t* data, size_t len); // Drop anything pending in the speaker DMA. Used on shutdown / reset. void flushSpeaker(); // ─── Debug / bring-up ──────────────────────────────────────── // // Stats updated on every readMicChunk() call, covering *this last // batch only*. Handy to confirm the mic is actually clocking data // into the ESP32 without blowing up the main audio path. struct MicStats { int32_t leftRawMin; // raw int32 sample on left I2S slot int32_t leftRawMax; int32_t rightRawMin; // raw int32 sample on right I2S slot int32_t rightRawMax; int16_t s16Min; // post-shift S16 sample (output channel) int16_t s16Max; size_t samples; // sample pairs in the batch }; const MicStats& lastMicStats() const { return lastStats_; } // Which I2S slot to route into the S16 output. Flip at runtime if // the mic's L/R pin doesn't land where we expect. enum class MicChannel { Left, Right }; void setMicChannel(MicChannel ch) { micChannel_ = ch; } MicChannel micChannel() const { return micChannel_; } private: bool micStarted_ = false; bool spkStarted_ = false; MicChannel micChannel_ = MicChannel::Left; MicStats lastStats_ = {0, 0, 0, 0, 0, 0, 0}; }; } // namespace tipote