ESPHome 2026.5.1
Loading...
Searching...
No Matches
spdif_encoder.cpp
Go to the documentation of this file.
1#include "spdif_encoder.h"
2
3#if defined(USE_ESP32) && defined(USE_I2S_AUDIO_SPDIF_MODE)
4
5#include "esphome/core/log.h"
6
7namespace esphome::i2s_audio {
8
9static const char *const TAG = "i2s_audio.spdif_encoder";
10
11// S/PDIF preamble patterns (8 BMC bits each)
12// These are the BMC-encoded sync patterns that violate normal BMC rules for easy detection.
13// All preambles end at phase HIGH (last bit = 1), enabling consistent data encoding.
14// Preamble is placed at bits 24-31 of word[0] for MSB-first transmission.
15static constexpr uint8_t PREAMBLE_B = 0x17; // Block start (left channel, frame 0)
16static constexpr uint8_t PREAMBLE_M = 0x1d; // Left channel (not block start)
17static constexpr uint8_t PREAMBLE_W = 0x1b; // Right channel
18
19// BMC encoding of 4 zero bits starting at phase HIGH: 00_11_00_11 = 0x33
20// Used as a constant in the 16-bit subframe path, where bits 4-11 are always zero.
21static constexpr uint32_t BMC_ZERO_NIBBLE = 0x33;
22
23// Constexpr BMC encoder for compile-time LUT generation.
24// Encodes with start phase=true (HIGH). The complement property allows phase=false
25// via XOR: bmc_encode(v, N, false) == bmc_encode(v, N, true) ^ mask
26static constexpr uint16_t bmc_lut_encode(uint32_t data, uint8_t num_bits) {
27 uint16_t bmc = 0;
28 bool phase = true;
29 for (uint8_t i = 0; i < num_bits; i++) {
30 bool bit = (data >> i) & 1;
31 uint8_t bmc_pair = phase ? (bit ? 0b01 : 0b00) : (bit ? 0b10 : 0b11);
32 bmc |= static_cast<uint16_t>(bmc_pair) << ((num_bits - 1 - i) * 2);
33 if (!bit)
34 phase = !phase;
35 }
36 return bmc;
37}
38
39// Compile-time parity helper (constexpr-friendly, runs only at LUT build time).
40static constexpr uint32_t bmc_lut_parity(uint32_t value, uint32_t num_bits) {
41 uint32_t p = 0;
42 for (uint32_t b = 0; b < num_bits; b++)
43 p ^= (value >> b) & 1u;
44 return p;
45}
46
47// Combined BMC + phase-delta lookup tables.
48// Each entry packs the BMC pattern (lower bits, phase=high start) together with
49// a phase-mask delta in bits 16-31 (0xFFFF if the input has odd parity, else 0).
50// XORing the delta into the running phase mask propagates parity across chunks
51// without an explicit popcount.
52
53// 4-bit BMC lookup table: 16 entries x uint32_t = 64 bytes in flash.
54// Bits 0-7 : 8-bit BMC pattern (phase=high start)
55// Bits 16-31 : phase-mask delta (0xFFFFu if odd parity, else 0)
56static constexpr auto BMC_LUT_4 = [] {
57 std::array<uint32_t, 16> t{};
58 for (uint32_t i = 0; i < 16; i++) {
59 uint32_t bmc = bmc_lut_encode(i, 4);
60 uint32_t delta = bmc_lut_parity(i, 4) ? 0xFFFF0000u : 0u;
61 t[i] = bmc | delta;
62 }
63 return t;
64}();
65
66// 8-bit BMC lookup table: 256 entries x uint32_t = 1024 bytes in flash.
67// Bits 0-15 : 16-bit BMC pattern (phase=high start)
68// Bits 16-31 : phase-mask delta (0xFFFFu if odd parity, else 0)
69static constexpr auto BMC_LUT_8 = [] {
70 std::array<uint32_t, 256> t{};
71 for (uint32_t i = 0; i < 256; i++) {
72 uint32_t bmc = bmc_lut_encode(i, 8);
73 uint32_t delta = bmc_lut_parity(i, 8) ? 0xFFFF0000u : 0u;
74 t[i] = bmc | delta;
75 }
76 return t;
77}();
78
79// Initialize S/PDIF buffer
81 this->spdif_block_buf_ = std::make_unique<uint32_t[]>(SPDIF_BLOCK_SIZE_U32);
82 if (!this->spdif_block_buf_) {
83 ESP_LOGE(TAG, "Buffer allocation failed (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES);
84 return false;
85 }
86 ESP_LOGV(TAG, "Buffer allocated (%zu bytes)", SPDIF_BLOCK_SIZE_BYTES);
87
88 // Build initial channel status block with default sample rate and width
90
91 this->reset();
92 return true;
93}
94
96 this->spdif_block_ptr_ = this->spdif_block_buf_.get();
97 this->frame_in_block_ = 0;
98 this->block_buf_is_silence_block_ = false;
99}
100
102 if (this->sample_rate_ != sample_rate) {
103 this->sample_rate_ = sample_rate;
104 this->build_channel_status_();
105 ESP_LOGD(TAG, "Sample rate set to %lu Hz", (unsigned long) sample_rate);
106 }
107}
108
109void SPDIFEncoder::set_bytes_per_sample(uint8_t bytes_per_sample) {
110 if (bytes_per_sample != 2 && bytes_per_sample != 3 && bytes_per_sample != 4) {
111 ESP_LOGE(TAG, "Unsupported bytes per sample: %u", (unsigned) bytes_per_sample);
112 return;
113 }
114 if (this->bytes_per_sample_ != bytes_per_sample) {
115 this->bytes_per_sample_ = bytes_per_sample;
116 this->build_channel_status_();
117 // Discard any partial block built at the previous width so we never mix widths on the wire.
118 this->reset();
119 ESP_LOGD(TAG, "Input width set to %u-bit", (unsigned) bytes_per_sample * 8);
120 }
121}
122
124 // IEC 60958-3 Consumer Channel Status Block (192 bits = 24 bytes)
125 // Transmitted LSB-first within each byte, one bit per frame via C bit.
126
127 // Any cached silence block was built for the previous channel status; it is now stale.
128 this->block_buf_is_silence_block_ = false;
129
130 this->channel_status_.fill(0);
131
132 // Byte 0: Consumer, PCM audio, no copyright, no pre-emphasis, Mode 0
133 // All bits are 0, which is already set
134
135 // Byte 1: Category code = 0x00 (general)
136 // Already 0
137
138 // Byte 2: Source/channel unspecified
139 // Already 0
140
141 // Byte 3: Sample frequency code (bits 0-3) + clock accuracy (bits 4-5)
142 // Clock accuracy = 00 (Level II, ±1000 ppm) - appropriate for ESP32
143 uint8_t freq_code;
144 switch (this->sample_rate_) {
145 case 44100:
146 freq_code = 0x0; // 0000
147 break;
148 case 48000:
149 freq_code = 0x2; // 0010
150 break;
151 default:
152 // Other values are possible but they're not supported by ESPHome
153 freq_code = 0x1; // 0001 = not indicated
154 ESP_LOGW(TAG, "Unsupported sample rate %lu Hz, channel status will indicate 'not specified'",
155 (unsigned long) this->sample_rate_);
156 break;
157 }
158 // Byte 3: freq_code in bits 0-3, clock accuracy (00) in bits 4-5
159 this->channel_status_[3] = freq_code; // Clock accuracy bits 4-5 are already 0
160
161 // Byte 4: Word length encoding (IEC 60958-3 consumer)
162 // bit 0: max length flag (0 = max 20 bits, 1 = max 24 bits)
163 // bits 1-3: word length code relative to the max
164 // For our supported widths:
165 // 16-bit (max 20): 0b0010 = 0x02 -- "16 bits, max 20"
166 // 24-bit (max 24): 0b1101 = 0x0D -- "24 bits, max 24"
167 // 32-bit input is truncated to 24-bit on the wire, so use the 24-bit code.
168 uint8_t word_length_code;
169 switch (this->bytes_per_sample_) {
170 case 2:
171 word_length_code = 0x02;
172 break;
173 case 3: // Shared case
174 case 4:
175 word_length_code = 0x0D;
176 break;
177 default:
178 word_length_code = 0x00; // not specified
179 break;
180 }
181 this->channel_status_[4] = word_length_code;
182}
183
184// Extract the C bit for the given frame from channel_status_ and shift it into bit 30
185// so it can be OR'd directly into a raw subframe.
186ESPHOME_ALWAYS_INLINE static inline uint32_t c_bit_for_frame(const std::array<uint8_t, 24> &channel_status,
187 uint32_t frame) {
188 return static_cast<uint32_t>((channel_status[frame >> 3] >> (frame & 7)) & 1u) << 30;
189}
190
191// ============================================================================
192// IEC 60958 subframe bit layout
193// ============================================================================
194// Bits 0-3: Preamble (handled separately, not in raw_subframe)
195// Bits 4-7: Auxiliary audio data / 24-bit audio LSB
196// Bits 8-11: Audio LSB extension (zero for 16-bit, low nibble of audio for 24-bit)
197// Bits 12-27: Audio sample (16 high bits in 16-bit mode, mid 16 bits in 24-bit mode)
198// Bit 28: V (Validity) - 0 = valid audio
199// Bit 29: U (User data) - 0
200// Bit 30: C (Channel status) - from channel status block
201// Bit 31: P (Parity) - even parity over bits 4-31
202// ============================================================================
203
204// Build a raw IEC 60958 subframe from PCM little-endian input of width Bps bytes.
205// Caller is responsible for OR-ing in the C bit and parity.
206template<uint8_t Bps> ESPHOME_ALWAYS_INLINE static inline uint32_t build_raw_subframe(const uint8_t *pcm_sample) {
207 static_assert(Bps == 2 || Bps == 3 || Bps == 4, "Unsupported bytes per sample");
208 if constexpr (Bps == 2) {
209 // 16-bit input: MSB-aligned in the 20-bit audio field, bits 12-27.
210 return (static_cast<uint32_t>(pcm_sample[1]) << 20) | (static_cast<uint32_t>(pcm_sample[0]) << 12);
211 } else if constexpr (Bps == 3) {
212 // 24-bit input: full 24-bit audio field, bits 4-27.
213 return (static_cast<uint32_t>(pcm_sample[2]) << 20) | (static_cast<uint32_t>(pcm_sample[1]) << 12) |
214 (static_cast<uint32_t>(pcm_sample[0]) << 4);
215 } else { // Bps == 4
216 // 32-bit input truncated to 24-bit: drop the lowest byte.
217 return (static_cast<uint32_t>(pcm_sample[3]) << 20) | (static_cast<uint32_t>(pcm_sample[2]) << 12) |
218 (static_cast<uint32_t>(pcm_sample[1]) << 4);
219 }
220}
221
222// BMC-encode a subframe and write the two output uint32 words to dst. Caller passes
223// raw_subframe with the C bit set (bit 30) and the P bit cleared (bit 31 = 0). P is
224// derived from the cumulative parity-mask delta of the per-byte LUT lookups.
225//
226// I2S halfword swap means word[0] transmits as: bits 24-31, 16-23, 8-15, 0-7.
227// word[1] transmits as: bits 16-31, 0-15. Within each halfword, MSB-first.
228// All preambles end at phase HIGH, so phase=true at the start of bit 4.
229//
230// P-bit derivation: BMC_LUT_*'s upper half encodes the parity of the input chunk. Each
231// chunk's parity delta is shifted down (`lut >> 16`) into a phase_mask that lives in the
232// low 16 bits, so the same value can also be XORed against subsequent BMC patterns to
233// invert phase. XOR'ing those deltas through all chunks (with bit 31 = 0) yields the
234// parity of bits 4-30 in the low bits of phase_mask -- the required value of the P bit
235// for even total parity. The BMC of bit 31 lives in bit 0 of the high-byte BMC output
236// (i = 7 maps to position (8-1-7)*2 = 0); flipping the source bit flips only the lower
237// BMC bit (= phase XOR bit), so applying P is `bmc_24_31 ^= phase_mask & 1u`.
238template<uint8_t Bps>
239ESPHOME_ALWAYS_INLINE static inline void bmc_encode_subframe(uint32_t raw_subframe, uint8_t preamble, uint32_t *dst) {
240 if constexpr (Bps == 2) {
241 // 16-bit path: bits 4-11 are zero, encoded inline as BMC_ZERO_NIBBLE constants.
242 // Eight zero source bits with start phase=HIGH end at phase=HIGH (popcount of zeros is even),
243 // so encoding of bits 12-15 starts at phase=true. Zeros contribute 0 to parity.
244 uint32_t nibble = (raw_subframe >> 12) & 0xF;
245 uint32_t lut_n = BMC_LUT_4[nibble];
246 uint32_t bmc_12_15 = lut_n & 0xFFu;
247 uint32_t phase_mask = lut_n >> 16; // 0xFFFFu if odd parity, else 0
248
249 uint32_t byte_mid = (raw_subframe >> 16) & 0xFF;
250 uint32_t lut_m = BMC_LUT_8[byte_mid];
251 uint32_t bmc_16_23 = (lut_m & 0xFFFFu) ^ phase_mask;
252 phase_mask ^= lut_m >> 16;
253
254 uint32_t byte_hi = (raw_subframe >> 24) & 0xFF; // bit 7 (= P) is 0 by precondition
255 uint32_t lut_h = BMC_LUT_8[byte_hi];
256 uint32_t bmc_24_31 = (lut_h & 0xFFFFu) ^ phase_mask;
257 phase_mask ^= lut_h >> 16;
258 // phase_mask now reflects parity of bits 4-30. Apply P by flipping bit 0 of bmc_24_31.
259 bmc_24_31 ^= phase_mask & 1u;
260
261 dst[0] = bmc_12_15 | (BMC_ZERO_NIBBLE << 8) | (BMC_ZERO_NIBBLE << 16) | (static_cast<uint32_t>(preamble) << 24);
262 dst[1] = bmc_24_31 | (bmc_16_23 << 16);
263 } else {
264 // 24-bit (and 32-bit truncated) path: bits 4-11 are live audio.
265 uint32_t byte_lo = (raw_subframe >> 4) & 0xFF;
266 uint32_t lut_l = BMC_LUT_8[byte_lo];
267 uint32_t bmc_4_11 = lut_l & 0xFFFFu;
268 uint32_t phase_mask = lut_l >> 16; // 0xFFFFu if odd parity, else 0
269
270 uint32_t nibble = (raw_subframe >> 12) & 0xF;
271 uint32_t lut_n = BMC_LUT_4[nibble];
272 uint32_t bmc_12_15 = (lut_n & 0xFFu) ^ (phase_mask & 0xFFu);
273 phase_mask ^= lut_n >> 16;
274
275 uint32_t byte_mid = (raw_subframe >> 16) & 0xFF;
276 uint32_t lut_m = BMC_LUT_8[byte_mid];
277 uint32_t bmc_16_23 = (lut_m & 0xFFFFu) ^ phase_mask;
278 phase_mask ^= lut_m >> 16;
279
280 uint32_t byte_hi = (raw_subframe >> 24) & 0xFF; // bit 7 (= P) is 0 by precondition
281 uint32_t lut_h = BMC_LUT_8[byte_hi];
282 uint32_t bmc_24_31 = (lut_h & 0xFFFFu) ^ phase_mask;
283 phase_mask ^= lut_h >> 16;
284 bmc_24_31 ^= phase_mask & 1u;
285
286 // word[0]: bits 24-31 = preamble, bits 8-23 = bmc(4-11), bits 0-7 = bmc(12-15)
287 // word[1]: bits 16-31 = bmc(16-23), bits 0-15 = bmc(24-31)
288 dst[0] = bmc_12_15 | (bmc_4_11 << 8) | (static_cast<uint32_t>(preamble) << 24);
289 dst[1] = bmc_24_31 | (bmc_16_23 << 16);
290 }
291}
292
293template<uint8_t Bps> void SPDIFEncoder::encode_silence_frame_() {
294 static constexpr uint8_t SILENCE[4] = {0, 0, 0, 0};
295 uint32_t raw = build_raw_subframe<Bps>(SILENCE) | c_bit_for_frame(this->channel_status_, this->frame_in_block_);
296 uint8_t preamble_l = (this->frame_in_block_ == 0) ? PREAMBLE_B : PREAMBLE_M;
297 bmc_encode_subframe<Bps>(raw, preamble_l, this->spdif_block_ptr_);
298 bmc_encode_subframe<Bps>(raw, PREAMBLE_W, this->spdif_block_ptr_ + 2);
299 this->spdif_block_ptr_ += 4;
300 if (++this->frame_in_block_ >= SPDIF_BLOCK_SAMPLES) {
301 this->frame_in_block_ = 0;
302 }
303}
304
305esp_err_t SPDIFEncoder::send_block_(TickType_t ticks_to_wait) {
306 // Use the appropriate callback and context based on preload mode
307 SPDIFBlockCallback callback;
308 void *ctx;
309
310 if (this->preload_mode_) {
311 callback = this->preload_callback_;
312 ctx = this->preload_callback_ctx_;
313 } else {
314 callback = this->write_callback_;
315 ctx = this->write_callback_ctx_;
316 }
317
318 if (callback == nullptr) {
319 return ESP_ERR_INVALID_STATE;
320 }
321
322 esp_err_t err = callback(ctx, this->spdif_block_buf_.get(), SPDIF_BLOCK_SIZE_BYTES, ticks_to_wait);
323
324 if (err == ESP_OK) {
325 // Reset pointer for next block; position tracking continues from where it left off
326 this->spdif_block_ptr_ = this->spdif_block_buf_.get();
327 }
328
329 return err;
330}
331
332template<uint8_t Bps>
333HOT esp_err_t SPDIFEncoder::write_typed_(const uint8_t *src, size_t size, TickType_t ticks_to_wait,
334 uint32_t *blocks_sent, size_t *bytes_consumed) {
335 const uint8_t *pcm_data = src;
336 const uint8_t *const pcm_end = src + size;
337 uint32_t block_count = 0;
338
339 // Hot state lives in locals so the compiler can keep it in registers across the
340 // per-frame encoding work; byte writes through block_ptr may alias the member fields,
341 // which would block register allocation if the encoding read them directly from this->*.
342 uint32_t *block_ptr = this->spdif_block_ptr_;
343 uint32_t *const block_buf = this->spdif_block_buf_.get();
344 uint32_t *const block_end = block_buf + SPDIF_BLOCK_SIZE_U32;
345 uint32_t frame = this->frame_in_block_;
346 const std::array<uint8_t, 24> &channel_status = this->channel_status_;
347
348 auto save_state = [&]() {
349 this->spdif_block_ptr_ = block_ptr;
350 this->frame_in_block_ = static_cast<uint8_t>(frame);
351 };
352
353 auto report_out_params = [&]() {
354 if (blocks_sent != nullptr)
355 *blocks_sent = block_count;
356 if (bytes_consumed != nullptr)
357 *bytes_consumed = pcm_data - src;
358 };
359
360 // Send a completed block if the buffer is full, propagating any error.
361 // send_block_ resets this->spdif_block_ptr_ to block_buf on success and leaves it
362 // unchanged on error -- mirror both behaviors in our local block_ptr.
363 auto maybe_send = [&]() -> esp_err_t {
364 if (block_ptr >= block_end) {
365 esp_err_t err = this->send_block_(ticks_to_wait);
366 if (err != ESP_OK) {
367 save_state();
368 report_out_params();
369 return err;
370 }
371 block_ptr = block_buf;
372 ++block_count;
373 }
374 return ESP_OK;
375 };
376
377 // Hot path: encode L+R pairs in two peeled sub-loops. Frame 0 carries the only
378 // buffer-full check and uses PREAMBLE_B (a block fills exactly when frame wraps from
379 // 191 back to 0). Frames 1..191 use PREAMBLE_M and need no buffer-full check or
380 // preamble branch. The encoding body is inlined here so block_ptr lives in a register
381 // for the duration of the loop.
382 while (pcm_data + 2 * Bps <= pcm_end) {
383 if (frame == 0) {
384 esp_err_t err = maybe_send();
385 if (err != ESP_OK)
386 return err;
387
388 uint32_t c_bit = c_bit_for_frame(channel_status, 0);
389 uint32_t raw_l = build_raw_subframe<Bps>(pcm_data) | c_bit;
390 uint32_t raw_r = build_raw_subframe<Bps>(pcm_data + Bps) | c_bit;
391 bmc_encode_subframe<Bps>(raw_l, PREAMBLE_B, block_ptr);
392 bmc_encode_subframe<Bps>(raw_r, PREAMBLE_W, block_ptr + 2);
393 block_ptr += 4;
394 frame = 1;
395 pcm_data += 2 * Bps;
396 }
397
398 // The inner loop runs until min(SPDIF_BLOCK_SAMPLES, frame + input_frames). The
399 // input-size bound is folded into end_frame so a single `frame < end_frame` test
400 // governs termination.
401 uint32_t input_frames = static_cast<uint32_t>(pcm_end - pcm_data) / (2u * Bps);
402 uint32_t end_frame = SPDIF_BLOCK_SAMPLES;
403 if (frame + input_frames < end_frame)
404 end_frame = frame + input_frames;
405
406 while (frame < end_frame) {
407 uint32_t c_bit = c_bit_for_frame(channel_status, frame);
408 uint32_t raw_l = build_raw_subframe<Bps>(pcm_data) | c_bit;
409 uint32_t raw_r = build_raw_subframe<Bps>(pcm_data + Bps) | c_bit;
410 bmc_encode_subframe<Bps>(raw_l, PREAMBLE_M, block_ptr);
411 bmc_encode_subframe<Bps>(raw_r, PREAMBLE_W, block_ptr + 2);
412 block_ptr += 4;
413 ++frame;
414 pcm_data += 2 * Bps;
415 }
416 if (frame >= SPDIF_BLOCK_SAMPLES)
417 frame = 0;
418 }
419
420 // Send any complete block that was just finished.
421 if (block_ptr >= block_end) {
422 esp_err_t err = this->send_block_(ticks_to_wait);
423 if (err != ESP_OK) {
424 save_state();
425 report_out_params();
426 return err;
427 }
428 block_ptr = block_buf;
429 ++block_count;
430 }
431
432 save_state();
433 report_out_params();
434 return ESP_OK;
435}
436
437HOT esp_err_t SPDIFEncoder::write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent,
438 size_t *bytes_consumed) {
439 if (size > 0) {
440 // Real PCM is about to be encoded into the buffer, so it is no longer a full-silence block.
441 this->block_buf_is_silence_block_ = false;
442 }
443 switch (this->bytes_per_sample_) {
444 case 2:
445 return this->write_typed_<2>(src, size, ticks_to_wait, blocks_sent, bytes_consumed);
446 case 3:
447 return this->write_typed_<3>(src, size, ticks_to_wait, blocks_sent, bytes_consumed);
448 case 4:
449 return this->write_typed_<4>(src, size, ticks_to_wait, blocks_sent, bytes_consumed);
450 default:
451 return ESP_ERR_INVALID_STATE;
452 }
453}
454
455template<uint8_t Bps> esp_err_t SPDIFEncoder::flush_with_silence_typed_(TickType_t ticks_to_wait) {
456 // If a complete block is already pending (from a previous failed send), emit just that block.
457 // Otherwise pad the partial block with silence (or generate a full silence block if empty) and
458 // send. Always emits exactly one block on success.
459 if (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
460 const bool was_empty = (this->spdif_block_ptr_ == this->spdif_block_buf_.get());
461 // Continuous-silence idle case: a full silence block is byte-identical every time for the
462 // active channel status, so when the buffer already holds one, re-send it as-is.
463 if (was_empty && this->block_buf_is_silence_block_) {
464 return this->send_block_(ticks_to_wait);
465 }
466 // Pad with silence frames at the configured width.
467 while (this->spdif_block_ptr_ < &this->spdif_block_buf_[SPDIF_BLOCK_SIZE_U32]) {
469 }
470 // The buffer is a reusable full-silence block only if it was built entirely from silence; a
471 // partial real-audio block padded out with silence is not.
472 this->block_buf_is_silence_block_ = was_empty;
473 }
474 return this->send_block_(ticks_to_wait);
475}
476
477esp_err_t SPDIFEncoder::flush_with_silence(TickType_t ticks_to_wait) {
478 switch (this->bytes_per_sample_) {
479 case 2:
480 return this->flush_with_silence_typed_<2>(ticks_to_wait);
481 case 3:
482 return this->flush_with_silence_typed_<3>(ticks_to_wait);
483 case 4:
484 return this->flush_with_silence_typed_<4>(ticks_to_wait);
485 default:
486 return ESP_ERR_INVALID_STATE;
487 }
488}
489
490} // namespace esphome::i2s_audio
491
492#endif // USE_I2S_AUDIO_SPDIF_MODE
uint8_t raw[35]
Definition bl0939.h:0
std::unique_ptr< uint32_t[]> spdif_block_buf_
esp_err_t send_block_(TickType_t ticks_to_wait)
Send the completed block via the appropriate callback.
void set_bytes_per_sample(uint8_t bytes_per_sample)
Set input PCM width: 2 = 16-bit, 3 = 24-bit, 4 = 32-bit (truncated to 24-bit on the wire).
std::array< uint8_t, 24 > channel_status_
esp_err_t write(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent=nullptr, size_t *bytes_consumed=nullptr)
Convert PCM audio data to SPDIF BMC encoded data.
void set_sample_rate(uint32_t sample_rate)
Set the sample rate for Channel Status Block encoding.
void encode_silence_frame_()
Encode a single stereo silence frame at the current block position.
esp_err_t flush_with_silence_typed_(TickType_t ticks_to_wait)
Templated flush-with-silence.
SPDIFBlockCallback preload_callback_
void reset()
Reset the SPDIF block buffer and position tracking, discarding any partial block.
void build_channel_status_()
Build the channel status block from current configuration.
HOT esp_err_t write_typed_(const uint8_t *src, size_t size, TickType_t ticks_to_wait, uint32_t *blocks_sent, size_t *bytes_consumed)
Templated write loop. Called from the public write() via runtime dispatch on bytes_per_sample_.
esp_err_t flush_with_silence(TickType_t ticks_to_wait)
Emit one complete SPDIF block: pad any pending partial block with silence and send,...
bool setup()
Initialize the SPDIF working buffer.
esp_err_t(*)(void *user_ctx, uint32_t *data, size_t size, TickType_t ticks_to_wait) SPDIFBlockCallback
Callback signature for block completion (raw function pointer for minimal overhead)
static float float float t
static float float b
const std::vector< uint8_t > & data
uint16_t size
Definition helpers.cpp:25
static void uint32_t