2026-04-09 02:47:53 +02:00

85 lines
3.2 KiB
C++

// 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 <Arduino.h>
#include <stdint.h>
#include <stddef.h>
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