85 lines
3.2 KiB
C++
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
|