| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- /**
- * SPDIF audio output via I2S bit-banging — amedes approach
- *
- * Based on the public-domain SPDIF implementation by amedes:
- * https://github.com/amedes/esp_a2dp_sink_spdif
- *
- * The buffer is pre-filled with alternating M (left) / W (right) preambles.
- * Only the audio-data words (odd uint32_t indices) are written during
- * conversion. The B (block-start) preamble is applied by XOR-flipping one
- * byte at the start of each half-block write — the XOR naturally toggles
- * between M and B every 96 stereo frames, placing B exactly once per
- * 192-frame SPDIF block.
- *
- * BCK and WS are not routed to any GPIO — only the DOUT pin carries the
- * SPDIF signal. The internal I2S clock still runs, but no external
- * clocks are emitted.
- *
- * For coax SPDIF output, use this passive circuit:
- *
- * 100nF
- * GPIO ----210R------||---- coax SPDIF signal out
- * |
- * 110R
- * |
- * GND -------------------- coax signal ground
- */
- #include "audio_output.h"
- #include "audio_receiver.h"
- #include "audio_resample.h"
- #include "led.h"
- #include "driver/i2s_std.h"
- #include "esp_check.h"
- #include "soc/soc_caps.h"
- #include "freertos/FreeRTOS.h"
- #include "freertos/task.h"
- #include "rtsp_server.h"
- #include <stdlib.h>
- #include <string.h>
- #define TAG "audio_spdif"
- #define OUTPUT_RATE CONFIG_OUTPUT_SAMPLE_RATE_HZ
- #define FRAME_SAMPLES 352
- /* Max output frames after resampling one input frame */
- #define MAX_RESAMPLE_FRAMES \
- ((size_t)((FRAME_SAMPLES + 2) * ((double)OUTPUT_RATE / 44100) + 16))
- #define SPDIF_DO_PIN CONFIG_SPDIF_DO_IO
- #if CONFIG_FREERTOS_UNICORE
- #define PLAYBACK_CORE 0
- #else
- #define PLAYBACK_CORE 1
- #endif
- /* ── SPDIF framing constants ──────────────────────────────────────────── */
- #define I2S_BITS 32
- #define I2S_CHANNELS 2
- #define BMC_BITS 64 /* bits per SPDIF sub-frame after BMC */
- #define BMC_FACTOR (BMC_BITS / I2S_BITS) /* = 2 */
- #define SPDIF_BLOCK 192 /* sub-frames per SPDIF block (L+R) */
- #define SPDIF_BUF_DIV 2 /* half-block buffering */
- /* DMA: one DMA buffer = one half-block = 96 stereo frames = 192 I2S
- * "pseudo-frames" of 32-bit stereo. Size = 192 × 8 = 1536 bytes. */
- #define DMA_BUF_COUNT 2
- #define DMA_BUF_FRAMES (SPDIF_BLOCK * BMC_BITS / I2S_BITS / SPDIF_BUF_DIV)
- /* Encode buffer (uint32_t array) — one half-block */
- #define SPDIF_BUF_BYTES \
- (SPDIF_BLOCK * (BMC_BITS / 8) * I2S_CHANNELS / SPDIF_BUF_DIV)
- #define SPDIF_BUF_WORDS (SPDIF_BUF_BYTES / sizeof(uint32_t))
- /* ── BMC preambles ─────────────────────────────────────────────────────── */
- #define BMC_B 0x33173333U /* block start (B) */
- #define BMC_M 0x331d3333U /* left channel (M) */
- #define BMC_W 0x331b3333U /* right channel (W) */
- #define BMC_MW_DIF (BMC_M ^ BMC_W)
- /* Byte offset within the first preamble word where M↔B differs */
- #define SYNC_OFFSET 2
- #define SYNC_FLIP ((BMC_B ^ BMC_M) >> (SYNC_OFFSET * 8))
- /* ── BMC lookup table ──────────────────────────────────────────────────
- * 8-bit PCM → 16-bit BMC, LSb first, ending with a "1" level. */
- // NOLINTBEGIN(bugprone-narrowing-conversions)
- static const int16_t bmc_tab[256] = {
- 0x3333, 0xb333, 0xd333, 0x5333, 0xcb33, 0x4b33, 0x2b33, 0xab33, 0xcd33,
- 0x4d33, 0x2d33, 0xad33, 0x3533, 0xb533, 0xd533, 0x5533, 0xccb3, 0x4cb3,
- 0x2cb3, 0xacb3, 0x34b3, 0xb4b3, 0xd4b3, 0x54b3, 0x32b3, 0xb2b3, 0xd2b3,
- 0x52b3, 0xcab3, 0x4ab3, 0x2ab3, 0xaab3, 0xccd3, 0x4cd3, 0x2cd3, 0xacd3,
- 0x34d3, 0xb4d3, 0xd4d3, 0x54d3, 0x32d3, 0xb2d3, 0xd2d3, 0x52d3, 0xcad3,
- 0x4ad3, 0x2ad3, 0xaad3, 0x3353, 0xb353, 0xd353, 0x5353, 0xcb53, 0x4b53,
- 0x2b53, 0xab53, 0xcd53, 0x4d53, 0x2d53, 0xad53, 0x3553, 0xb553, 0xd553,
- 0x5553, 0xcccb, 0x4ccb, 0x2ccb, 0xaccb, 0x34cb, 0xb4cb, 0xd4cb, 0x54cb,
- 0x32cb, 0xb2cb, 0xd2cb, 0x52cb, 0xcacb, 0x4acb, 0x2acb, 0xaacb, 0x334b,
- 0xb34b, 0xd34b, 0x534b, 0xcb4b, 0x4b4b, 0x2b4b, 0xab4b, 0xcd4b, 0x4d4b,
- 0x2d4b, 0xad4b, 0x354b, 0xb54b, 0xd54b, 0x554b, 0x332b, 0xb32b, 0xd32b,
- 0x532b, 0xcb2b, 0x4b2b, 0x2b2b, 0xab2b, 0xcd2b, 0x4d2b, 0x2d2b, 0xad2b,
- 0x352b, 0xb52b, 0xd52b, 0x552b, 0xccab, 0x4cab, 0x2cab, 0xacab, 0x34ab,
- 0xb4ab, 0xd4ab, 0x54ab, 0x32ab, 0xb2ab, 0xd2ab, 0x52ab, 0xcaab, 0x4aab,
- 0x2aab, 0xaaab, 0xcccd, 0x4ccd, 0x2ccd, 0xaccd, 0x34cd, 0xb4cd, 0xd4cd,
- 0x54cd, 0x32cd, 0xb2cd, 0xd2cd, 0x52cd, 0xcacd, 0x4acd, 0x2acd, 0xaacd,
- 0x334d, 0xb34d, 0xd34d, 0x534d, 0xcb4d, 0x4b4d, 0x2b4d, 0xab4d, 0xcd4d,
- 0x4d4d, 0x2d4d, 0xad4d, 0x354d, 0xb54d, 0xd54d, 0x554d, 0x332d, 0xb32d,
- 0xd32d, 0x532d, 0xcb2d, 0x4b2d, 0x2b2d, 0xab2d, 0xcd2d, 0x4d2d, 0x2d2d,
- 0xad2d, 0x352d, 0xb52d, 0xd52d, 0x552d, 0xccad, 0x4cad, 0x2cad, 0xacad,
- 0x34ad, 0xb4ad, 0xd4ad, 0x54ad, 0x32ad, 0xb2ad, 0xd2ad, 0x52ad, 0xcaad,
- 0x4aad, 0x2aad, 0xaaad, 0x3335, 0xb335, 0xd335, 0x5335, 0xcb35, 0x4b35,
- 0x2b35, 0xab35, 0xcd35, 0x4d35, 0x2d35, 0xad35, 0x3535, 0xb535, 0xd535,
- 0x5535, 0xccb5, 0x4cb5, 0x2cb5, 0xacb5, 0x34b5, 0xb4b5, 0xd4b5, 0x54b5,
- 0x32b5, 0xb2b5, 0xd2b5, 0x52b5, 0xcab5, 0x4ab5, 0x2ab5, 0xaab5, 0xccd5,
- 0x4cd5, 0x2cd5, 0xacd5, 0x34d5, 0xb4d5, 0xd4d5, 0x54d5, 0x32d5, 0xb2d5,
- 0xd2d5, 0x52d5, 0xcad5, 0x4ad5, 0x2ad5, 0xaad5, 0x3355, 0xb355, 0xd355,
- 0x5355, 0xcb55, 0x4b55, 0x2b55, 0xab55, 0xcd55, 0x4d55, 0x2d55, 0xad55,
- 0x3555, 0xb555, 0xd555, 0x5555,
- };
- // NOLINTEND(bugprone-narrowing-conversions)
- /* ── I2S handle ────────────────────────────────────────────────────────── */
- static i2s_chan_handle_t tx_handle;
- static volatile bool flush_requested = false;
- static volatile int source_rate = 44100;
- static volatile bool resample_reinit_needed = false;
- /* ── SPDIF encode buffer and write pointer ─────────────────────────────── */
- static uint32_t spdif_buf[SPDIF_BUF_WORDS];
- static uint32_t *spdif_ptr;
- /* ── Volume ────────────────────────────────────────────────────────────── */
- static void apply_volume(int16_t *buf, size_t n) {
- #ifndef CONFIG_DAC_CONTROLS_VOLUME
- int32_t vol = airplay_get_volume_q15();
- for (size_t i = 0; i < n; i++) {
- buf[i] = (int16_t)(((int32_t)buf[i] * vol) >> 15);
- }
- #endif
- }
- /* ── SPDIF buffer init ─────────────────────────────────────────────────
- * Pre-fill even indices with alternating M / W preamble words.
- * Odd indices (audio data) will be overwritten during conversion. */
- static void spdif_buf_init(void) {
- uint32_t bmc_mw = BMC_W;
- for (int i = 0; i < (int)SPDIF_BUF_WORDS; i += 2) {
- spdif_buf[i] = (bmc_mw ^= BMC_MW_DIF);
- }
- }
- /* ── SPDIF write ───────────────────────────────────────────────────────
- * Convert interleaved 16-bit PCM to BMC and push to I2S when the
- * half-block buffer fills.
- *
- * src — pointer to interleaved 16-bit stereo PCM
- * size — total byte count (frames × 2 ch × 2 bytes) */
- static void spdif_write(const void *src, size_t size) {
- const uint8_t *p = src;
- while (p < (const uint8_t *)src + size) {
- /* Each 16-bit sample → one SPDIF sub-frame:
- * bmc_tab[lo_byte] occupies upper 16 bits
- * bmc_tab[hi_byte] occupies lower 16 bits
- * XOR gives differential encoding
- * << 1 >> 1 clears MSB (parity = 0) */
- *(spdif_ptr + 1) =
- (uint32_t)(((bmc_tab[*p] << 16) ^ bmc_tab[*(p + 1)]) << 1) >> 1;
- p += 2;
- spdif_ptr += 2; /* skip preamble word → next slot pair */
- /* Half-block complete → toggle B-preamble and flush to DMA */
- if (spdif_ptr >= &spdif_buf[SPDIF_BUF_WORDS]) {
- size_t written;
- /* XOR toggles byte at SYNC_OFFSET between M and B preamble.
- * Because we always XOR, it alternates: B on even half-blocks,
- * M on odd ones → B appears once every 192 frames. */
- ((uint8_t *)spdif_buf)[SYNC_OFFSET] ^= SYNC_FLIP;
- i2s_channel_write(tx_handle, spdif_buf, sizeof(spdif_buf), &written,
- portMAX_DELAY);
- spdif_ptr = spdif_buf;
- }
- }
- }
- /* ── Playback task ─────────────────────────────────────────────────────── */
- static void playback_task(void *arg) {
- int16_t *pcm = malloc((size_t)(FRAME_SAMPLES + 1) * 2 * sizeof(int16_t));
- int16_t *silence = calloc((size_t)FRAME_SAMPLES * 2, sizeof(int16_t));
- int16_t *resample_buf = malloc(MAX_RESAMPLE_FRAMES * 2 * sizeof(int16_t));
- if (!pcm || !silence || !resample_buf) {
- ESP_LOGE(TAG, "Failed to allocate PCM buffers");
- free(pcm);
- free(silence);
- free(resample_buf);
- vTaskDelete(NULL);
- return;
- }
- while (true) {
- if (resample_reinit_needed) {
- resample_reinit_needed = false;
- audio_resample_init((uint32_t)source_rate, OUTPUT_RATE, 2);
- }
- if (flush_requested) {
- flush_requested = false;
- audio_resample_reset();
- i2s_channel_disable(tx_handle);
- spdif_buf_init();
- spdif_ptr = spdif_buf;
- i2s_channel_enable(tx_handle);
- }
- size_t samples = audio_receiver_read(pcm, FRAME_SAMPLES + 1);
- if (samples > 0) {
- int16_t *play_buf = pcm;
- size_t play_samples = samples;
- if (audio_resample_is_active()) {
- play_samples = audio_resample_process(pcm, samples, resample_buf,
- MAX_RESAMPLE_FRAMES);
- play_buf = resample_buf;
- }
- apply_volume(play_buf, play_samples * 2);
- led_audio_feed(play_buf, play_samples);
- spdif_write(play_buf, play_samples * 2 * sizeof(int16_t));
- taskYIELD();
- } else {
- led_audio_feed(silence, FRAME_SAMPLES);
- spdif_write(silence, (size_t)FRAME_SAMPLES * 2 * sizeof(int16_t));
- vTaskDelay(1);
- }
- }
- }
- /* ── Public API ────────────────────────────────────────────────────────── */
- esp_err_t audio_output_init(void) {
- ESP_LOGI(TAG, "Initialising SPDIF output (amedes) on GPIO %d", SPDIF_DO_PIN);
- /* Pre-fill buffer with alternating M/W preambles */
- spdif_buf_init();
- spdif_ptr = spdif_buf;
- /* ── I2S channel ─────────────────────────────────────────────────── */
- i2s_chan_config_t chan_cfg =
- I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
- chan_cfg.dma_desc_num = DMA_BUF_COUNT;
- chan_cfg.dma_frame_num = DMA_BUF_FRAMES;
- ESP_RETURN_ON_ERROR(i2s_new_channel(&chan_cfg, &tx_handle, NULL), TAG,
- "channel create failed");
- /* SPDIF: I2S at 2× sample rate, 32-bit stereo.
- * Only DOUT carries the SPDIF signal; BCK and WS are internal-only.
- * APLL provides exact audio-rate clocking (ESP32). */
- i2s_std_clk_config_t clk_cfg =
- I2S_STD_CLK_DEFAULT_CONFIG((uint32_t)OUTPUT_RATE * BMC_FACTOR);
- #if SOC_I2S_SUPPORTS_APLL
- clk_cfg.clk_src = I2S_CLK_SRC_APLL;
- #endif
- clk_cfg.mclk_multiple = I2S_MCLK_MULTIPLE_256;
- i2s_std_config_t std_cfg = {
- .clk_cfg = clk_cfg,
- .slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_32BIT,
- I2S_SLOT_MODE_STEREO),
- .gpio_cfg =
- {
- .mclk = I2S_GPIO_UNUSED,
- .bclk = I2S_GPIO_UNUSED,
- .ws = I2S_GPIO_UNUSED,
- .dout = SPDIF_DO_PIN,
- .din = I2S_GPIO_UNUSED,
- },
- };
- ESP_RETURN_ON_ERROR(i2s_channel_init_std_mode(tx_handle, &std_cfg), TAG,
- "std mode init failed");
- ESP_RETURN_ON_ERROR(i2s_channel_enable(tx_handle), TAG,
- "channel enable failed");
- /* Pre-fill DMA with SPDIF-encoded silence so the receiver can lock */
- {
- int16_t silence_pcm[SPDIF_BLOCK * 2];
- memset(silence_pcm, 0, sizeof(silence_pcm));
- for (int i = 0; i < DMA_BUF_COUNT; i++) {
- spdif_write(silence_pcm,
- (size_t)(SPDIF_BLOCK / SPDIF_BUF_DIV) * 2 * sizeof(int16_t));
- }
- }
- audio_resample_init(44100, OUTPUT_RATE, 2);
- ESP_LOGI(TAG, "SPDIF output ready rate=%d×%d dma=%d×%d", OUTPUT_RATE,
- BMC_FACTOR, DMA_BUF_FRAMES, DMA_BUF_COUNT);
- return ESP_OK;
- }
- void audio_output_start(void) {
- xTaskCreatePinnedToCore(playback_task, "spdif_play", 4096, NULL, 7, NULL,
- PLAYBACK_CORE);
- }
- void audio_output_flush(void) {
- flush_requested = true;
- }
- void audio_output_set_source_rate(int rate) {
- if (rate > 0 && rate != source_rate) {
- source_rate = rate;
- resample_reinit_needed = true;
- }
- }
- uint32_t audio_output_get_hardware_latency_us(void) {
- // SPDIF DMA ring: DMA_BUF_COUNT half-blocks, each SPDIF_BLOCK/SPDIF_BUF_DIV
- // audio samples (= 96 stereo frames per buffer).
- const uint32_t audio_samples = DMA_BUF_COUNT * (SPDIF_BLOCK / SPDIF_BUF_DIV);
- return (uint32_t)((uint64_t)audio_samples * 1000000ULL / OUTPUT_RATE);
- }
|