Skip to main content

device_envoy_core/
audio_player.rs

1//! A device abstraction for playing audio clips over I²S hardware.
2//!
3//! Platform-independent types, macros, and helpers for the audio player.
4//! For complete documentation and examples, see the platform-specific crate
5//! (for example `device_envoy_rp::audio_player` or `device_envoy::audio_player`).
6
7// TODO Add a realtime tone Playable (sine + ASR envelope) that uses parameter-only storage and matches ADPCM playback performance.
8
9pub mod adpcm_clip_generated;
10#[cfg(all(test, feature = "host"))]
11mod host_tests;
12pub mod pcm_clip_generated;
13
14// Re-export `paste!` so platform crates can reference it as
15// `__paste!` in their `audio_player!` macro.
16
17use core::ops::ControlFlow;
18use core::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
19use core::sync::atomic::{AtomicI32, Ordering};
20use core::time::Duration;
21
22use embassy_sync::{blocking_mutex::raw::CriticalSectionRawMutex, signal::Signal};
23use heapless::Vec;
24
25const I16_ABS_MAX_I64: i64 = -(i16::MIN as i64);
26const ADPCM_ENCODE_BLOCK_ALIGN: usize = 256;
27
28// Common audio sample-rate constants in hertz.
29
30/// Narrowband telephony sample rate.
31pub const NARROWBAND_8000_HZ: u32 = 8_000;
32/// Wideband voice sample rate.
33pub const VOICE_16000_HZ: u32 = 16_000;
34/// Common low-memory voice/music sample rate.
35///
36/// Convenience constant: any sample rate supported by your hardware setup may
37/// be used.
38pub const VOICE_22050_HZ: u32 = 22_050;
39/// Compact-disc sample rate.
40pub const CD_44100_HZ: u32 = 44_100;
41/// Pro-audio sample rate.
42pub const PRO_48000_HZ: u32 = 48_000;
43
44/// Absolute playback loudness setting for the whole player.
45///
46/// `Volume` is used by the player-level controls
47/// [`max_volume`, `initial_volume`](mod@crate::audio_player), and
48/// [`set_volume`](AudioPlayer::set_volume),
49/// which set the absolute playback loudness behavior for the whole player.
50///
51/// This is different from [`Gain`] and [`PcmClipBuf::with_gain`], which
52/// adjust the relative loudness of individual clips.
53///
54/// See the [audio_player module documentation](mod@crate::audio_player) for
55/// usage examples.
56#[derive(Clone, Copy, Debug, PartialEq, Eq)]
57pub struct Volume(i16);
58
59impl Volume {
60    /// Silence.
61    pub const MUTE: Self = Self(0);
62
63    /// Maximum playback volume.
64    pub const MAX: Self = Self(i16::MAX);
65
66    /// Creates a volume from a percentage of full scale.
67    ///
68    /// Values above `100` are clamped to `100`.
69    ///
70    /// See the [audio_player module documentation](mod@crate::audio_player) for
71    /// usage examples.
72    #[must_use]
73    pub const fn percent(percent: u8) -> Self {
74        let percent = if percent > 100 { 100 } else { percent };
75        let value_i32 = (percent as i32 * i16::MAX as i32) / 100;
76        Self(value_i32 as i16)
77    }
78
79    /// Creates a humorous "goes to 11" demo volume scale.
80    ///
81    /// `0..=11` maps to `0..=100%` using a perceptual curve
82    /// (roughly logarithmic, but not mathematically exact).
83    ///
84    /// Values above `11` clamp to `11`.
85    ///
86    /// See the [audio_player module documentation](mod@crate::audio_player) for
87    /// usage examples.
88    #[must_use]
89    pub const fn spinal_tap(spinal_tap: u8) -> Self {
90        let spinal_tap = if spinal_tap > 11 { 11 } else { spinal_tap };
91        let percent = match spinal_tap {
92            0 => 0,
93            1 => 1,
94            2 => 3,
95            3 => 6,
96            4 => 13,
97            5 => 25,
98            6 => 35,
99            7 => 50,
100            8 => 71,
101            9 => 89,
102            10 => 100,
103            11 => 100,
104            _ => 100,
105        };
106        Self::percent(percent)
107    }
108
109    #[must_use]
110    pub(crate) const fn to_i16(self) -> i16 {
111        self.0
112    }
113
114    #[must_use]
115    pub(crate) const fn from_i16(value_i16: i16) -> Self {
116        Self(value_i16)
117    }
118}
119
120/// Relative loudness adjustment for audio clips.
121///
122/// Use `Gain` with [`PcmClipBuf::with_gain`] to make a clip louder or quieter
123/// before playback.
124///
125/// `with_gain` is intended for const clip definitions, so the adjusted samples
126/// are precomputed at compile time with no extra runtime work.
127///
128/// You can set gain by percent or by dB:
129/// - [`Gain::percent`] where `100` means unchanged and values above `100` are louder.
130/// - [`Gain::db`] where positive dB is louder and negative dB is quieter.
131///
132/// This is different from [`Volume`] used by
133/// [`max_volume`, `initial_volume`](mod@crate::audio_player), and
134/// [`set_volume`](AudioPlayer::set_volume),
135/// which set the absolute playback loudness behavior for the whole player.
136///
137/// See the [audio_player module documentation](mod@crate::audio_player) for
138/// usage examples.
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub struct Gain(i32);
141
142impl Gain {
143    /// Silence.
144    pub const MUTE: Self = Self(0);
145
146    /// Creates a gain from percentage.
147    ///
148    /// `100` is unity gain. Values above `100` boost the signal.
149    ///
150    /// See the [audio_player module documentation](mod@crate::audio_player) for
151    /// usage examples.
152    #[must_use]
153    pub const fn percent(percent: u16) -> Self {
154        let value_i32 = (percent as i32 * i16::MAX as i32) / 100;
155        Self(value_i32)
156    }
157
158    /// Creates gain from dB with a bounded boost range.
159    ///
160    /// Values above `+12 dB` clamp to `+12 dB`.
161    /// Values below `-96 dB` clamp to `-96 dB`.
162    ///
163    /// See [`PcmClipBuf::with_gain`] for usage.
164    #[must_use]
165    pub const fn db(db: i8) -> Self {
166        const DB_UPPER_LIMIT: i8 = 12;
167        const DB_LOWER_LIMIT: i8 = -96;
168        let db = if db > DB_UPPER_LIMIT {
169            DB_UPPER_LIMIT
170        } else if db < DB_LOWER_LIMIT {
171            DB_LOWER_LIMIT
172        } else {
173            db
174        };
175
176        if db == 0 {
177            return Self::percent(100);
178        }
179
180        // Fixed-point multipliers for 10^(+/-1/20) (approximately +/-1 dB in amplitude).
181        const DB_STEP_DOWN_Q15: i32 = 29_205;
182        const DB_STEP_UP_Q15: i32 = 36_781;
183        const ONE_Q15: i32 = 32_768;
184        const ROUND_Q15: i32 = 16_384;
185        let step_q15_i32 = if db > 0 {
186            DB_STEP_UP_Q15
187        } else {
188            DB_STEP_DOWN_Q15
189        };
190        let db_steps_u8 = if db > 0 { db as u8 } else { (-db) as u8 };
191        let mut scale_q15_i32 = ONE_Q15;
192        let mut step_index = 0_u8;
193        // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
194        while step_index < db_steps_u8 {
195            scale_q15_i32 = (scale_q15_i32 * step_q15_i32 + ROUND_Q15) / ONE_Q15;
196            step_index += 1;
197        }
198
199        let gain_i64 = (i16::MAX as i64 * scale_q15_i32 as i64 + ROUND_Q15 as i64) / ONE_Q15 as i64;
200        let gain_i32 = if gain_i64 > i32::MAX as i64 {
201            i32::MAX
202        } else {
203            gain_i64 as i32
204        };
205        Self(gain_i32)
206    }
207
208    #[must_use]
209    const fn linear(self) -> i32 {
210        self.0
211    }
212}
213
214#[must_use]
215#[doc(hidden)]
216/// This uses [`core::time::Duration`] for clip timing.
217pub const fn __samples_for_duration(duration: core::time::Duration, sample_rate_hz: u32) -> usize {
218    assert!(sample_rate_hz > 0, "sample_rate_hz must be > 0");
219    let sample_rate_hz_u64 = sample_rate_hz as u64;
220    let samples_from_seconds_u64 = duration.as_secs() * sample_rate_hz_u64;
221    let samples_from_subsec_nanos_u64 =
222        (duration.subsec_nanos() as u64 * sample_rate_hz_u64) / 1_000_000_000_u64;
223    let total_samples_u64 = samples_from_seconds_u64 + samples_from_subsec_nanos_u64;
224    assert!(
225        total_samples_u64 <= usize::MAX as u64,
226        "duration/sample_rate result must fit usize"
227    );
228    total_samples_u64 as usize
229}
230
231const fn duration_for_sample_count(sample_count: usize, sample_rate_hz: u32) -> Duration {
232    assert!(sample_rate_hz > 0, "sample_rate_hz must be > 0");
233    let sample_rate_hz_usize = sample_rate_hz as usize;
234    let whole_seconds = sample_count / sample_rate_hz_usize;
235    let subsecond_sample_count = sample_count % sample_rate_hz_usize;
236    let subsecond_nanos =
237        ((subsecond_sample_count as u64) * 1_000_000_000_u64) / sample_rate_hz as u64;
238    Duration::new(whole_seconds as u64, subsecond_nanos as u32)
239}
240
241// Must remain `pub` because exported macros (for example `pcm_clip!` and
242// `adpcm_clip!`) expand in downstream crates and reference this helper via
243// `$crate::...`.
244#[doc(hidden)]
245#[must_use]
246pub const fn __resampled_sample_count(
247    source_sample_count: usize,
248    source_sample_rate_hz: u32,
249    destination_sample_rate_hz: u32,
250) -> usize {
251    assert!(source_sample_count > 0, "source_sample_count must be > 0");
252    assert!(
253        source_sample_rate_hz > 0,
254        "source_sample_rate_hz must be > 0"
255    );
256    assert!(
257        destination_sample_rate_hz > 0,
258        "destination_sample_rate_hz must be > 0"
259    );
260    let destination_sample_count = ((source_sample_count as u64
261        * destination_sample_rate_hz as u64)
262        + (source_sample_rate_hz as u64 / 2))
263        / source_sample_rate_hz as u64;
264    assert!(
265        destination_sample_count > 0,
266        "destination sample count must be > 0"
267    );
268    destination_sample_count as usize
269}
270
271#[inline]
272const fn sine_sample_from_phase(phase_u32: u32) -> i16 {
273    let half_cycle_u64 = 1_u64 << 31;
274    let one_q31_u64 = 1_u64 << 31;
275    let phase_u64 = phase_u32 as u64;
276    let (half_phase_u64, sign_i64) = if phase_u64 < half_cycle_u64 {
277        (phase_u64, 1_i64)
278    } else {
279        (phase_u64 - half_cycle_u64, -1_i64)
280    };
281
282    // Bhaskara approximation on a normalized half-cycle:
283    // sin(pi * t) ~= 16 t (1 - t) / (5 - 4 t (1 - t)), for t in [0, 1].
284    let product_q31_u64 = (half_phase_u64 * (one_q31_u64 - half_phase_u64)) >> 31;
285    let denominator_q31_u64 = 5 * one_q31_u64 - 4 * product_q31_u64;
286    let sine_q31_u64 = ((16 * product_q31_u64) << 31) / denominator_q31_u64;
287
288    let sample_i64 = (sine_q31_u64 as i64 * sign_i64) >> 16;
289    clamp_i64_to_i16(sample_i64)
290}
291
292// Must be `pub` because platform-crate `device_loop` helpers call it at
293// runtime (in a different crate from where it is defined).
294#[doc(hidden)]
295#[inline]
296pub const fn scale_sample_with_linear(sample_i16: i16, linear_i32: i32) -> i16 {
297    if linear_i32 == 0 {
298        return 0;
299    }
300    // Use signed full-scale magnitude (32768) so i16::MIN is handled correctly.
301    // Full-scale linear is 32767, so add one to map it to exact unity gain.
302    let unity_scaled_linear_i64 = linear_i32 as i64 + 1;
303    let scaled_i64 = (sample_i16 as i64 * unity_scaled_linear_i64) / I16_ABS_MAX_I64;
304    clamp_i64_to_i16(scaled_i64)
305}
306
307// Must be `pub` because platform-crate `device_loop` helpers call it at
308// runtime (in a different crate from where it is defined).
309#[doc(hidden)]
310#[inline]
311pub const fn scale_sample_with_volume(sample_i16: i16, volume: Volume) -> i16 {
312    scale_sample_with_linear(sample_i16, volume.to_i16() as i32)
313}
314
315#[inline]
316const fn scale_linear(linear_i32: i32, volume: Volume) -> i32 {
317    if volume.to_i16() == 0 || linear_i32 == 0 {
318        return 0;
319    }
320    let unity_scaled_volume_i64 = volume.to_i16() as i64 + 1;
321    ((linear_i32 as i64 * unity_scaled_volume_i64) / I16_ABS_MAX_I64) as i32
322}
323
324#[inline]
325const fn clamp_i64_to_i16(value_i64: i64) -> i16 {
326    if value_i64 > i16::MAX as i64 {
327        i16::MAX
328    } else if value_i64 < i16::MIN as i64 {
329        i16::MIN
330    } else {
331        value_i64 as i16
332    }
333}
334
335/// Platform sink used by shared audio playback routines.
336#[doc(hidden)]
337#[allow(async_fn_in_trait)]
338pub trait AudioOutputSink<const SAMPLE_BUFFER_LEN: usize> {
339    /// Writes `stereo_word_count` samples from `stereo_words`.
340    async fn write_stereo_words(
341        &mut self,
342        stereo_words: &[u32; SAMPLE_BUFFER_LEN],
343        stereo_word_count: usize,
344    ) -> Result<(), ()>;
345
346    /// Optional hook for platform pacing after each successful write.
347    async fn after_write(&mut self) {}
348}
349
350/// Packs a mono sample into a stereo I2S frame word.
351#[doc(hidden)]
352#[inline]
353pub const fn stereo_sample(sample: i16) -> u32 {
354    let sample_bits = sample as u16 as u32;
355    (sample_bits << 16) | sample_bits
356}
357
358// Must be `pub` because platform `device_loop` implementations call this from
359// another crate.
360#[doc(hidden)]
361pub async fn play_clip_sequence_once<
362    Output: AudioOutputSink<SAMPLE_BUFFER_LEN>,
363    const SAMPLE_BUFFER_LEN: usize,
364    const MAX_CLIPS: usize,
365    const SAMPLE_RATE_HZ: u32,
366>(
367    output: &mut Output,
368    audio_clips: &[PlaybackClip<SAMPLE_RATE_HZ>],
369    sample_buffer: &mut [u32; SAMPLE_BUFFER_LEN],
370    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
371) -> Option<AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>> {
372    for audio_clip in audio_clips {
373        match audio_clip {
374            PlaybackClip::Pcm(audio_clip) => {
375                if let ControlFlow::Break(next_audio_command) =
376                    play_full_pcm_clip_once(output, audio_clip, sample_buffer, audio_player_static)
377                        .await
378                {
379                    return Some(next_audio_command);
380                }
381            }
382            PlaybackClip::Adpcm(adpcm_clip) => {
383                if let ControlFlow::Break(next_audio_command) = play_full_adpcm_clip_once(
384                    output,
385                    adpcm_clip,
386                    sample_buffer,
387                    audio_player_static,
388                )
389                .await
390                {
391                    return Some(next_audio_command);
392                }
393            }
394            PlaybackClip::Silence(duration) => {
395                if let ControlFlow::Break(next_audio_command) = play_silence_duration_once(
396                    output,
397                    *duration,
398                    sample_buffer,
399                    audio_player_static,
400                )
401                .await
402                {
403                    return Some(next_audio_command);
404                }
405            }
406        }
407    }
408    None
409}
410
411async fn play_full_pcm_clip_once<
412    Output: AudioOutputSink<SAMPLE_BUFFER_LEN>,
413    const SAMPLE_BUFFER_LEN: usize,
414    const MAX_CLIPS: usize,
415    const SAMPLE_RATE_HZ: u32,
416>(
417    output: &mut Output,
418    audio_clip: &PcmClip<SAMPLE_RATE_HZ>,
419    sample_buffer: &mut [u32; SAMPLE_BUFFER_LEN],
420    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
421) -> ControlFlow<AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>, ()> {
422    for audio_sample_chunk in audio_clip.samples().chunks(SAMPLE_BUFFER_LEN) {
423        let runtime_volume = audio_player_static.effective_runtime_volume();
424        for (sample_buffer_slot, sample_value_ref) in
425            sample_buffer.iter_mut().zip(audio_sample_chunk.iter())
426        {
427            let sample_value = *sample_value_ref;
428            let scaled_sample_value = scale_sample_with_volume(sample_value, runtime_volume);
429            *sample_buffer_slot = stereo_sample(scaled_sample_value);
430        }
431        sample_buffer[audio_sample_chunk.len()..].fill(stereo_sample(0));
432
433        if output
434            .write_stereo_words(sample_buffer, audio_sample_chunk.len())
435            .await
436            .is_err()
437        {
438            return ControlFlow::Continue(());
439        }
440        output.after_write().await;
441
442        if let Some(next_audio_command) = audio_player_static.try_take_command() {
443            return ControlFlow::Break(next_audio_command);
444        }
445    }
446
447    ControlFlow::Continue(())
448}
449
450async fn play_full_adpcm_clip_once<
451    Output: AudioOutputSink<SAMPLE_BUFFER_LEN>,
452    const SAMPLE_BUFFER_LEN: usize,
453    const MAX_CLIPS: usize,
454    const SAMPLE_RATE_HZ: u32,
455>(
456    output: &mut Output,
457    adpcm_clip: &AdpcmClip<SAMPLE_RATE_HZ>,
458    sample_buffer: &mut [u32; SAMPLE_BUFFER_LEN],
459    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
460) -> ControlFlow<AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>, ()> {
461    let mut sample_buffer_len = 0usize;
462
463    let block_align = adpcm_clip.block_align() as usize;
464    for adpcm_block in adpcm_clip.data().chunks_exact(block_align) {
465        if adpcm_block.len() < 4 {
466            return ControlFlow::Continue(());
467        }
468
469        let runtime_volume = audio_player_static.effective_runtime_volume();
470        let mut predictor_i32 = match read_i16_le(adpcm_block, 0) {
471            Some(value) => value as i32,
472            None => return ControlFlow::Continue(()),
473        };
474        let mut step_index_i32 = adpcm_block[2] as i32;
475        if !(0..=88).contains(&step_index_i32) {
476            return ControlFlow::Continue(());
477        }
478
479        sample_buffer[sample_buffer_len] = stereo_sample(scale_sample_with_volume(
480            predictor_i32 as i16,
481            runtime_volume,
482        ));
483        sample_buffer_len += 1;
484        if sample_buffer_len == SAMPLE_BUFFER_LEN {
485            if output
486                .write_stereo_words(sample_buffer, sample_buffer_len)
487                .await
488                .is_err()
489            {
490                return ControlFlow::Continue(());
491            }
492            output.after_write().await;
493            sample_buffer_len = 0;
494            if let Some(next_audio_command) = audio_player_static.try_take_command() {
495                return ControlFlow::Break(next_audio_command);
496            }
497        }
498
499        let mut samples_decoded_in_block = 1usize;
500        let samples_per_block = adpcm_clip.samples_per_block() as usize;
501
502        for adpcm_byte in &adpcm_block[4..] {
503            for adpcm_nibble in [adpcm_byte & 0x0F, adpcm_byte >> 4] {
504                if samples_decoded_in_block >= samples_per_block {
505                    break;
506                }
507
508                let decoded_sample_i16 = decode_adpcm_nibble_const(
509                    adpcm_nibble,
510                    &mut predictor_i32,
511                    &mut step_index_i32,
512                );
513                sample_buffer[sample_buffer_len] =
514                    stereo_sample(scale_sample_with_volume(decoded_sample_i16, runtime_volume));
515                sample_buffer_len += 1;
516                samples_decoded_in_block += 1;
517
518                if sample_buffer_len == SAMPLE_BUFFER_LEN {
519                    if output
520                        .write_stereo_words(sample_buffer, sample_buffer_len)
521                        .await
522                        .is_err()
523                    {
524                        return ControlFlow::Continue(());
525                    }
526                    output.after_write().await;
527                    sample_buffer_len = 0;
528                    if let Some(next_audio_command) = audio_player_static.try_take_command() {
529                        return ControlFlow::Break(next_audio_command);
530                    }
531                }
532            }
533        }
534
535        if let Some(next_audio_command) = audio_player_static.try_take_command() {
536            return ControlFlow::Break(next_audio_command);
537        }
538    }
539
540    if sample_buffer_len != 0 {
541        sample_buffer[sample_buffer_len..].fill(stereo_sample(0));
542        if output
543            .write_stereo_words(sample_buffer, sample_buffer_len)
544            .await
545            .is_err()
546        {
547            return ControlFlow::Continue(());
548        }
549        output.after_write().await;
550        if let Some(next_audio_command) = audio_player_static.try_take_command() {
551            return ControlFlow::Break(next_audio_command);
552        }
553    }
554
555    ControlFlow::Continue(())
556}
557
558async fn play_silence_duration_once<
559    Output: AudioOutputSink<SAMPLE_BUFFER_LEN>,
560    const SAMPLE_BUFFER_LEN: usize,
561    const MAX_CLIPS: usize,
562    const SAMPLE_RATE_HZ: u32,
563>(
564    output: &mut Output,
565    duration: Duration,
566    sample_buffer: &mut [u32; SAMPLE_BUFFER_LEN],
567    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
568) -> ControlFlow<AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>, ()> {
569    let silence_sample_count = __samples_for_duration(duration, SAMPLE_RATE_HZ);
570    let mut remaining_sample_count = silence_sample_count;
571    sample_buffer.fill(stereo_sample(0));
572
573    while remaining_sample_count > 0 {
574        let chunk_sample_count = remaining_sample_count.min(SAMPLE_BUFFER_LEN);
575        if output
576            .write_stereo_words(sample_buffer, chunk_sample_count)
577            .await
578            .is_err()
579        {
580            return ControlFlow::Continue(());
581        }
582        output.after_write().await;
583        remaining_sample_count -= chunk_sample_count;
584        if let Some(next_audio_command) = audio_player_static.try_take_command() {
585            return ControlFlow::Break(next_audio_command);
586        }
587    }
588
589    ControlFlow::Continue(())
590}
591
592#[inline]
593fn read_i16_le(bytes: &[u8], byte_offset: usize) -> Option<i16> {
594    let end_offset = byte_offset.checked_add(2)?;
595    if end_offset > bytes.len() {
596        return None;
597    }
598    Some(i16::from_le_bytes([
599        bytes[byte_offset],
600        bytes[byte_offset + 1],
601    ]))
602}
603
604/// End-of-sequence behavior for playback.
605///
606/// Generated audio player types support looping or stopping at the end of a clip sequence.
607///
608/// See the [audio_player module documentation](mod@crate::audio_player) for
609/// usage examples.
610pub enum AtEnd {
611    /// Repeat the full clip sequence forever.
612    Loop,
613    /// Stop after one full clip sequence pass.
614    Stop,
615}
616
617/// Unsized view of static compressed (ADPCM) clip data.
618///
619/// For fixed-size, const-friendly storage, see [`AdpcmClipBuf`].
620pub struct AdpcmClip<const SAMPLE_RATE_HZ: u32, T: ?Sized = [u8]> {
621    block_align: u16,
622    samples_per_block: u16,
623    data: T,
624}
625
626/// Sized, const-friendly storage for compressed (ADPCM) clip data.
627pub type AdpcmClipBuf<const SAMPLE_RATE_HZ: u32, const DATA_LEN: usize> =
628    AdpcmClip<SAMPLE_RATE_HZ, [u8; DATA_LEN]>;
629
630impl<const SAMPLE_RATE_HZ: u32, T: ?Sized> AdpcmClip<SAMPLE_RATE_HZ, T> {
631    /// Returns the ADPCM block size in bytes.
632    #[must_use]
633    pub fn block_align(&self) -> u16 {
634        self.block_align
635    }
636
637    /// Returns the number of decoded samples per ADPCM block.
638    #[must_use]
639    pub fn samples_per_block(&self) -> u16 {
640        self.samples_per_block
641    }
642
643    /// Returns a reference to the raw ADPCM byte data.
644    #[must_use]
645    pub fn data(&self) -> &T {
646        &self.data
647    }
648}
649
650/// **Implementation for fixed-size clips (`AdpcmClipBuf`).**
651///
652/// This impl applies to [`AdpcmClip`] with array-backed storage:
653/// `AdpcmClip<SAMPLE_RATE_HZ, [u8; DATA_LEN]>`
654/// (which is what [`AdpcmClipBuf`] aliases).
655impl<const SAMPLE_RATE_HZ: u32, const DATA_LEN: usize> AdpcmClip<SAMPLE_RATE_HZ, [u8; DATA_LEN]> {
656    /// Creates a fixed-size ADPCM clip.
657    #[must_use]
658    pub(crate) const fn new(
659        block_align: u16,
660        samples_per_block: u16,
661        data: [u8; DATA_LEN],
662    ) -> Self {
663        assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
664        assert!(block_align >= 5, "block_align must be >= 5");
665        assert!(samples_per_block > 0, "samples_per_block must be > 0");
666        assert!(
667            DATA_LEN % block_align as usize == 0,
668            "adpcm data length must be block aligned"
669        );
670        Self {
671            block_align,
672            samples_per_block,
673            data,
674        }
675    }
676
677    /// Returns the uncompressed (PCM) version of this clip.
678    ///
679    /// `SAMPLE_COUNT` is the number of samples in the resulting PCM clip.
680    /// Typically, use the generated clip-module constant:
681    /// [`AdpcmClipGenerated::PCM_SAMPLE_COUNT`](crate::audio_player::adpcm_clip_generated::AdpcmClipGenerated::PCM_SAMPLE_COUNT).
682    #[must_use]
683    pub const fn with_pcm<const SAMPLE_COUNT: usize>(
684        &self,
685    ) -> PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT> {
686        let block_align = self.block_align as usize;
687        assert!(block_align >= 5, "block_align must be >= 5");
688        assert!(
689            DATA_LEN % block_align == 0,
690            "adpcm data length must be block aligned"
691        );
692
693        let samples_per_block = self.samples_per_block as usize;
694        assert!(samples_per_block > 0, "samples_per_block must be > 0");
695        let expected_sample_count = (DATA_LEN / block_align) * samples_per_block;
696        assert!(
697            SAMPLE_COUNT == expected_sample_count,
698            "sample count must match decoded ADPCM length"
699        );
700
701        let mut samples = [0_i16; SAMPLE_COUNT];
702        if SAMPLE_COUNT == 0 {
703            assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
704            return PcmClip { samples };
705        }
706
707        let mut sample_index = 0usize;
708        let mut block_start = 0usize;
709        // TODO_NIGHTLY When nightly feature const_for becomes stable, replace these while loops with for loops.
710        while block_start < DATA_LEN {
711            let mut predictor_i32 = read_i16_le_const(&self.data, block_start) as i32;
712            let mut step_index_i32 = self.data[block_start + 2] as i32;
713            assert!(step_index_i32 >= 0, "ADPCM step_index must be >= 0");
714            assert!(step_index_i32 <= 88, "ADPCM step_index must be <= 88");
715
716            samples[sample_index] = predictor_i32 as i16;
717            sample_index += 1;
718            let mut decoded_in_block = 1usize;
719
720            let mut adpcm_byte_offset = block_start + 4;
721            let adpcm_block_end = block_start + block_align;
722            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
723            while adpcm_byte_offset < adpcm_block_end {
724                let adpcm_byte = self.data[adpcm_byte_offset];
725                let adpcm_nibble_low = adpcm_byte & 0x0F;
726                let adpcm_nibble_high = adpcm_byte >> 4;
727
728                if decoded_in_block < samples_per_block {
729                    samples[sample_index] = decode_adpcm_nibble_const(
730                        adpcm_nibble_low,
731                        &mut predictor_i32,
732                        &mut step_index_i32,
733                    );
734                    sample_index += 1;
735                    decoded_in_block += 1;
736                }
737                if decoded_in_block < samples_per_block {
738                    samples[sample_index] = decode_adpcm_nibble_const(
739                        adpcm_nibble_high,
740                        &mut predictor_i32,
741                        &mut step_index_i32,
742                    );
743                    sample_index += 1;
744                    decoded_in_block += 1;
745                }
746
747                adpcm_byte_offset += 1;
748            }
749
750            block_start += block_align;
751        }
752
753        assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
754        PcmClip { samples }
755    }
756
757    /// Returns this fixed-size ADPCM clip with linear sample gain applied.
758    ///
759    /// This operation decodes ADPCM to PCM, applies gain, then re-encodes ADPCM.
760    /// The extra ADPCM encode pass can be more lossy than applying gain once on
761    /// PCM before a single ADPCM encode.
762    #[must_use]
763    pub const fn with_gain(self, gain: Gain) -> Self {
764        let block_align = self.block_align as usize;
765        assert!(block_align >= 5, "block_align must be >= 5");
766        assert!(
767            DATA_LEN % block_align == 0,
768            "adpcm data length must be block aligned"
769        );
770
771        let samples_per_block = self.samples_per_block as usize;
772        assert!(samples_per_block > 0, "samples_per_block must be > 0");
773        let max_samples_per_block = __adpcm_samples_per_block(block_align);
774        assert!(
775            samples_per_block <= max_samples_per_block,
776            "samples_per_block exceeds block_align capacity"
777        );
778
779        let mut gained_data = [0_u8; DATA_LEN];
780        let mut block_start = 0usize;
781        // TODO_NIGHTLY When nightly feature const_for becomes stable, replace these while loops with for loops.
782        while block_start < DATA_LEN {
783            let mut source_predictor_i32 = read_i16_le_const(&self.data, block_start) as i32;
784            let mut source_step_index_i32 = self.data[block_start + 2] as i32;
785            assert!(
786                source_step_index_i32 >= 0 && source_step_index_i32 <= 88,
787                "ADPCM step_index must be in 0..=88"
788            );
789
790            let scaled_first_sample_i16 =
791                scale_sample_with_linear(source_predictor_i32 as i16, gain.linear());
792            let mut destination_predictor_i32 = scaled_first_sample_i16 as i32;
793            let mut destination_step_index_i32 = source_step_index_i32;
794
795            let scaled_first_sample_bytes = scaled_first_sample_i16.to_le_bytes();
796            gained_data[block_start] = scaled_first_sample_bytes[0];
797            gained_data[block_start + 1] = scaled_first_sample_bytes[1];
798            gained_data[block_start + 2] = destination_step_index_i32 as u8;
799            gained_data[block_start + 3] = 0;
800
801            let mut decoded_in_block = 1usize;
802            let mut source_byte_offset = block_start + 4;
803            let mut destination_byte_offset = block_start + 4;
804            let block_end = block_start + block_align;
805
806            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
807            while source_byte_offset < block_end {
808                let source_byte = self.data[source_byte_offset];
809                let mut destination_byte = 0_u8;
810
811                let mut nibble_index = 0usize;
812                // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
813                while nibble_index < 2 {
814                    if decoded_in_block < samples_per_block {
815                        let source_nibble = if nibble_index == 0 {
816                            source_byte & 0x0F
817                        } else {
818                            source_byte >> 4
819                        };
820                        let decoded_sample_i16 = decode_adpcm_nibble_const(
821                            source_nibble,
822                            &mut source_predictor_i32,
823                            &mut source_step_index_i32,
824                        );
825                        let scaled_sample_i32 =
826                            scale_sample_with_linear(decoded_sample_i16, gain.linear()) as i32;
827                        let destination_nibble = encode_adpcm_nibble(
828                            scaled_sample_i32,
829                            &mut destination_predictor_i32,
830                            &mut destination_step_index_i32,
831                        );
832                        destination_byte |= destination_nibble << (nibble_index * 4);
833                        decoded_in_block += 1;
834                    }
835                    nibble_index += 1;
836                }
837
838                gained_data[destination_byte_offset] = destination_byte;
839                source_byte_offset += 1;
840                destination_byte_offset += 1;
841            }
842
843            block_start += block_align;
844        }
845
846        Self::new(self.block_align, self.samples_per_block, gained_data)
847    }
848}
849
850/// Parsed ADPCM WAV metadata used by [`adpcm_clip!`](macro@crate::audio_player::adpcm_clip).
851#[derive(Clone, Copy)]
852#[doc(hidden)]
853pub struct ParsedAdpcmWavHeader {
854    /// WAV sample rate.
855    pub sample_rate_hz: u32,
856    /// ADPCM block size in bytes.
857    pub block_align: usize,
858    /// Decoded samples per ADPCM block.
859    pub samples_per_block: usize,
860    /// Byte offset of the `data` chunk payload.
861    pub data_chunk_start: usize,
862    /// Byte length of the `data` chunk payload.
863    pub data_chunk_len: usize,
864    /// Total decoded sample count from all ADPCM blocks.
865    pub sample_count: usize,
866}
867
868/// Parses ADPCM WAV header metadata in a `const` context.
869#[must_use]
870#[doc(hidden)]
871pub const fn __parse_adpcm_wav_header(wav_bytes: &[u8]) -> ParsedAdpcmWavHeader {
872    if wav_bytes.len() < 12 {
873        panic!("WAV file too small");
874    }
875    if !wav_tag_eq(wav_bytes, 0, *b"RIFF") {
876        panic!("Missing RIFF header");
877    }
878    if !wav_tag_eq(wav_bytes, 8, *b"WAVE") {
879        panic!("Missing WAVE header");
880    }
881
882    let mut chunk_offset = 12usize;
883    let mut sample_rate_hz = 0u32;
884    let mut block_align = 0usize;
885    let mut samples_per_block = 0usize;
886    let mut fmt_found = false;
887    let mut data_chunk_start = 0usize;
888    let mut data_chunk_end = 0usize;
889    let mut data_found = false;
890
891    // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
892    while chunk_offset + 8 <= wav_bytes.len() {
893        let chunk_size = read_u32_le_const(wav_bytes, chunk_offset + 4) as usize;
894        let chunk_data_start = chunk_offset + 8;
895        if chunk_data_start > wav_bytes.len() || chunk_size > wav_bytes.len() - chunk_data_start {
896            panic!("WAV chunk overruns file");
897        }
898        let chunk_data_end = chunk_data_start + chunk_size;
899
900        if wav_tag_eq(wav_bytes, chunk_offset, *b"fmt ") {
901            if chunk_size < 16 {
902                panic!("fmt chunk too small");
903            }
904
905            let audio_format = read_u16_le_const(wav_bytes, chunk_data_start);
906            let channels = read_u16_le_const(wav_bytes, chunk_data_start + 2);
907            sample_rate_hz = read_u32_le_const(wav_bytes, chunk_data_start + 4);
908            block_align = read_u16_le_const(wav_bytes, chunk_data_start + 12) as usize;
909            let bits_per_sample = read_u16_le_const(wav_bytes, chunk_data_start + 14);
910
911            if audio_format != 0x0011 {
912                panic!("Expected ADPCM WAV format");
913            }
914            if channels != 1 {
915                panic!("Expected mono ADPCM WAV");
916            }
917            if bits_per_sample != 4 {
918                panic!("Expected 4-bit ADPCM");
919            }
920            if block_align < 5 {
921                panic!("ADPCM block_align too small");
922            }
923
924            let derived_samples_per_block = derive_samples_per_block_const(block_align);
925            samples_per_block = if chunk_size >= 22 {
926                read_u16_le_const(wav_bytes, chunk_data_start + 18) as usize
927            } else {
928                derived_samples_per_block
929            };
930            if samples_per_block != derived_samples_per_block {
931                panic!("Unexpected ADPCM samples_per_block");
932            }
933            fmt_found = true;
934        } else if wav_tag_eq(wav_bytes, chunk_offset, *b"data") {
935            data_chunk_start = chunk_data_start;
936            data_chunk_end = chunk_data_end;
937            data_found = true;
938        }
939
940        let padded_chunk_size = chunk_size + (chunk_size & 1);
941        if chunk_data_start > usize::MAX - padded_chunk_size {
942            panic!("WAV chunk traversal overflow");
943        }
944        chunk_offset = chunk_data_start + padded_chunk_size;
945    }
946
947    if !fmt_found {
948        panic!("Missing fmt chunk");
949    }
950    if !data_found {
951        panic!("Missing data chunk");
952    }
953    let data_chunk_len = data_chunk_end - data_chunk_start;
954    if data_chunk_len % block_align != 0 {
955        panic!("data chunk is not block aligned");
956    }
957
958    ParsedAdpcmWavHeader {
959        sample_rate_hz,
960        block_align,
961        samples_per_block,
962        data_chunk_start,
963        data_chunk_len,
964        sample_count: (data_chunk_len / block_align) * samples_per_block,
965    }
966}
967
968const fn wav_tag_eq(wav_bytes: &[u8], byte_offset: usize, tag_bytes: [u8; 4]) -> bool {
969    if byte_offset > wav_bytes.len().saturating_sub(4) {
970        return false;
971    }
972    wav_bytes[byte_offset] == tag_bytes[0]
973        && wav_bytes[byte_offset + 1] == tag_bytes[1]
974        && wav_bytes[byte_offset + 2] == tag_bytes[2]
975        && wav_bytes[byte_offset + 3] == tag_bytes[3]
976}
977
978const fn derive_samples_per_block_const(block_align: usize) -> usize {
979    if block_align < 4 {
980        panic!("ADPCM block_align underflow");
981    }
982    ((block_align - 4) * 2) + 1
983}
984
985const ADPCM_INDEX_TABLE: [i32; 16] = [-1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8];
986const ADPCM_STEP_TABLE: [i32; 89] = [
987    7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, 50, 55, 60, 66,
988    73, 80, 88, 97, 107, 118, 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, 337, 371, 408, 449,
989    494, 544, 598, 658, 724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, 2272,
990    2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, 7132, 7845, 8630, 9493,
991    10442, 11487, 12635, 13899, 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767,
992];
993
994/// Returns decoded samples per IMA ADPCM block for mono 4-bit data.
995// Must remain `pub` because exported macros/constants can reference this via
996// `$crate::audio_player::...` in downstream crates.
997#[doc(hidden)]
998#[must_use]
999pub const fn __adpcm_samples_per_block(block_align: usize) -> usize {
1000    if block_align < 5 {
1001        panic!("block_align must be >= 5 for ADPCM");
1002    }
1003    derive_samples_per_block_const(block_align)
1004}
1005
1006/// Returns ADPCM byte length needed to encode `sample_count` mono PCM samples.
1007#[doc(hidden)]
1008#[must_use]
1009pub const fn __adpcm_data_len_for_pcm_samples(sample_count: usize) -> usize {
1010    __adpcm_data_len_for_pcm_samples_with_block_align(sample_count, ADPCM_ENCODE_BLOCK_ALIGN)
1011}
1012
1013/// Returns ADPCM byte length needed to encode `sample_count` mono PCM samples
1014/// with a specific ADPCM `block_align`.
1015#[doc(hidden)]
1016#[must_use]
1017pub const fn __adpcm_data_len_for_pcm_samples_with_block_align(
1018    sample_count: usize,
1019    block_align: usize,
1020) -> usize {
1021    let samples_per_block = __adpcm_samples_per_block(block_align);
1022    let block_count = if sample_count == 0 {
1023        0
1024    } else {
1025        ((sample_count - 1) / samples_per_block) + 1
1026    };
1027    block_count * block_align
1028}
1029
1030const fn read_u16_le_const(bytes: &[u8], byte_offset: usize) -> u16 {
1031    if byte_offset > bytes.len().saturating_sub(2) {
1032        panic!("read_u16_le_const out of bounds");
1033    }
1034    u16::from_le_bytes([bytes[byte_offset], bytes[byte_offset + 1]])
1035}
1036
1037const fn read_i16_le_const(bytes: &[u8], byte_offset: usize) -> i16 {
1038    if byte_offset > bytes.len().saturating_sub(2) {
1039        panic!("read_i16_le_const out of bounds");
1040    }
1041    i16::from_le_bytes([bytes[byte_offset], bytes[byte_offset + 1]])
1042}
1043
1044const fn read_u32_le_const(bytes: &[u8], byte_offset: usize) -> u32 {
1045    if byte_offset > bytes.len().saturating_sub(4) {
1046        panic!("read_u32_le_const out of bounds");
1047    }
1048    u32::from_le_bytes([
1049        bytes[byte_offset],
1050        bytes[byte_offset + 1],
1051        bytes[byte_offset + 2],
1052        bytes[byte_offset + 3],
1053    ])
1054}
1055
1056// Must be `pub` so platform-crate `device_loop` and play functions can match
1057// on variants across the crate boundary.
1058#[doc(hidden)]
1059pub enum PlaybackClip<const SAMPLE_RATE_HZ: u32> {
1060    Pcm(&'static PcmClip<SAMPLE_RATE_HZ>),
1061    Adpcm(&'static AdpcmClip<SAMPLE_RATE_HZ>),
1062    Silence(Duration),
1063}
1064
1065/// An audio clip of silence for a specific duration. Memory-efficient because it stores no audio sample data.
1066///
1067/// This clip type is sample-rate agnostic. It can be used with any generated
1068/// player sample rate because silence is rendered at playback time.
1069///
1070/// See the [audio_player module documentation](mod@crate::audio_player) for
1071/// usage examples.
1072#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1073pub struct SilenceClip {
1074    duration: Duration,
1075}
1076
1077impl SilenceClip {
1078    /// Creates a silence clip for a specific duration.
1079    /// This uses [`core::time::Duration`].
1080    #[must_use]
1081    pub const fn new(duration: core::time::Duration) -> Self {
1082        Self { duration }
1083    }
1084
1085    /// Returns the silence duration.
1086    /// This returns [`core::time::Duration`].
1087    #[must_use]
1088    pub const fn duration(self) -> core::time::Duration {
1089        self.duration
1090    }
1091}
1092
1093/// A clip source trait for generated audio player `play(...)` methods.
1094///
1095/// This trait let's us pass audio clips of different types (PCM, ADPCM, or silence) in a single heterogeneous sequence to `play`.
1096///
1097/// This trait is object-safe, so mixed clips are passed as:
1098/// `&'static dyn Playable<SAMPLE_RATE_HZ>`.
1099#[allow(private_bounds)]
1100pub trait Playable<const SAMPLE_RATE_HZ: u32>: sealed::PlayableSealed<SAMPLE_RATE_HZ> {}
1101
1102impl<const SAMPLE_RATE_HZ: u32, T: ?Sized> Playable<SAMPLE_RATE_HZ> for T where
1103    T: sealed::PlayableSealed<SAMPLE_RATE_HZ>
1104{
1105}
1106
1107/// Platform-agnostic audio player device contract.
1108///
1109/// Platform crates implement this trait for generated audio player types so
1110/// playback operations resolve through trait methods instead of inherent methods.
1111///
1112/// # Example: Play "Mary Had a Little Lamb" (Phrase) Once
1113///
1114/// This example plays the opening phrase (`E D C D E E E`) and then stops.
1115///
1116/// ```rust,no_run
1117/// use device_envoy_core::audio_player::{
1118///     AtEnd, AudioPlayer, Playable, SilenceClip, VOICE_22050_HZ, Volume, tone,
1119/// };
1120/// use core::time::Duration as StdDuration;
1121///
1122/// const SAMPLE_RATE_HZ: u32 = VOICE_22050_HZ;
1123///
1124/// fn play_mary_phrase(audio_player: &impl AudioPlayer<SAMPLE_RATE_HZ>) {
1125///     type PlayableRef = &'static dyn Playable<SAMPLE_RATE_HZ>;
1126///
1127///     const REST: PlayableRef = &SilenceClip::new(StdDuration::from_millis(80));
1128///     const NOTE_DURATION: StdDuration = StdDuration::from_millis(220);
1129///     const NOTE_E4: PlayableRef = &tone!(330, SAMPLE_RATE_HZ, NOTE_DURATION);
1130///     const NOTE_D4: PlayableRef = &tone!(294, SAMPLE_RATE_HZ, NOTE_DURATION);
1131///     const NOTE_C4: PlayableRef = &tone!(262, SAMPLE_RATE_HZ, NOTE_DURATION);
1132///
1133///     audio_player.play(
1134///         [
1135///             NOTE_E4, REST, NOTE_D4, REST, NOTE_C4, REST, NOTE_D4, REST, NOTE_E4, REST,
1136///             NOTE_E4, REST, NOTE_E4,
1137///         ],
1138///         AtEnd::Stop,
1139///     );
1140/// }
1141///
1142/// # struct DemoAudioPlayer;
1143/// # impl AudioPlayer<SAMPLE_RATE_HZ> for DemoAudioPlayer {
1144/// #     const SAMPLE_RATE_HZ: u32 = SAMPLE_RATE_HZ;
1145/// #     const MAX_CLIPS: usize = 16;
1146/// #     const INITIAL_VOLUME: Volume = Volume::MAX;
1147/// #     const MAX_VOLUME: Volume = Volume::MAX;
1148/// #     fn play<I>(&self, _audio_clips: I, _at_end: AtEnd)
1149/// #     where
1150/// #         I: IntoIterator<Item = &'static dyn Playable<SAMPLE_RATE_HZ>>,
1151/// #     {
1152/// #     }
1153/// #     fn stop(&self) {}
1154/// #     async fn wait_until_stopped(&self) {}
1155/// #     fn set_volume(&self, _volume: Volume) {}
1156/// #     fn volume(&self) -> Volume {
1157/// #         Self::INITIAL_VOLUME
1158/// #     }
1159/// # }
1160/// # let audio_player = DemoAudioPlayer;
1161/// # play_mary_phrase(&audio_player);
1162/// ```
1163///
1164/// # Example: Compiling in an External Audio Clip and Runtime Volume Changes
1165///
1166/// This example shows how to compile in an external clip, play it in a loop,
1167/// change volume at runtime, and then stop/reset playback settings.
1168///
1169/// ```rust,no_run,standalone_crate
1170/// use device_envoy_core::audio_player::{
1171///     AtEnd, AudioPlayer, Gain, Playable, SilenceClip, VOICE_22050_HZ, Volume, pcm_clip, tone,
1172/// };
1173/// use device_envoy_core::button::Button;
1174/// use core::time::Duration as StdDuration;
1175/// use embassy_futures::select::{Either, select};
1176/// use embassy_time::{Duration, Timer};
1177///
1178/// pcm_clip! {
1179///     Nasa {
1180///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/nasa_22k.s16"),
1181///         source_sample_rate_hz: VOICE_22050_HZ,
1182///     }
1183/// }
1184///
1185/// async fn play_nasa_with_runtime_volume(
1186///     audio_player: &impl AudioPlayer<VOICE_22050_HZ>,
1187///     button: &mut impl Button,
1188/// ) -> ! {
1189///     type PlayableRef = &'static dyn Playable<VOICE_22050_HZ>;
1190///
1191///     const fn ms(milliseconds: u64) -> StdDuration {
1192///         StdDuration::from_millis(milliseconds)
1193///     }
1194///
1195///     const NASA: PlayableRef = &Nasa::adpcm_clip();
1196///     const GAP: PlayableRef = &SilenceClip::new(ms(80));
1197///     const CHIME: PlayableRef = &tone!(880, VOICE_22050_HZ, ms(100)).with_gain(Gain::percent(20));
1198///     const VOLUME_STEPS_PERCENT: [u8; 7] = [50, 25, 12, 6, 3, 1, 0];
1199///     let initial_volume = audio_player.volume();
1200///
1201///     loop {
1202///         audio_player.play([CHIME, NASA, GAP], AtEnd::Loop);
1203///
1204///         for volume_percent in VOLUME_STEPS_PERCENT {
1205///             match select(button.wait_for_press(), Timer::after(Duration::from_secs(1))).await {
1206///                 Either::First(()) => break,
1207///                 Either::Second(()) => audio_player.set_volume(Volume::percent(volume_percent)),
1208///             }
1209///         }
1210///
1211///         audio_player.stop();
1212///         audio_player.set_volume(initial_volume);
1213///         button.wait_for_press().await;
1214///     }
1215/// }
1216///
1217/// # struct DemoAudioPlayer;
1218/// # impl AudioPlayer<VOICE_22050_HZ> for DemoAudioPlayer {
1219/// #     const SAMPLE_RATE_HZ: u32 = VOICE_22050_HZ;
1220/// #     const MAX_CLIPS: usize = 8;
1221/// #     const INITIAL_VOLUME: Volume = Volume::spinal_tap(5);
1222/// #     const MAX_VOLUME: Volume = Volume::spinal_tap(11);
1223/// #     fn play<I>(&self, _audio_clips: I, _at_end: AtEnd)
1224/// #     where
1225/// #         I: IntoIterator<Item = &'static dyn Playable<VOICE_22050_HZ>>,
1226/// #     {
1227/// #     }
1228/// #     fn stop(&self) {}
1229/// #     async fn wait_until_stopped(&self) {}
1230/// #     fn set_volume(&self, _volume: Volume) {}
1231/// #     fn volume(&self) -> Volume {
1232/// #         Self::INITIAL_VOLUME
1233/// #     }
1234/// # }
1235/// # struct ButtonMock;
1236/// # impl device_envoy_core::button::__ButtonMonitor for ButtonMock {
1237/// #     fn is_pressed_raw(&self) -> bool { false }
1238/// #     async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
1239/// # }
1240/// # impl Button for ButtonMock {}
1241/// fn main() {
1242///     let mut button = ButtonMock;
1243///     let audio_player = DemoAudioPlayer;
1244///     let _future = play_nasa_with_runtime_volume(&audio_player, &mut button);
1245/// }
1246/// ```
1247///
1248/// # Example: Resample and Play Countdown Once
1249///
1250/// This example compiles in `2`, `1`, `0`, and NASA at `22.05 kHz`, resamples
1251/// them to narrowband (`8 kHz`) at compile time, and then plays the sequence.
1252///
1253/// ```rust,no_run,standalone_crate
1254/// use device_envoy_core::audio_player::{
1255///     AtEnd, AudioPlayer, Gain, NARROWBAND_8000_HZ, Playable, VOICE_22050_HZ, Volume, pcm_clip,
1256/// };
1257/// use device_envoy_core::button::Button;
1258///
1259/// pcm_clip! {
1260///     Digit0 {
1261///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/0_22050.s16"),
1262///         source_sample_rate_hz: VOICE_22050_HZ,
1263///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1264///     }
1265/// }
1266///
1267/// pcm_clip! {
1268///     Digit1 {
1269///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/1_22050.s16"),
1270///         source_sample_rate_hz: VOICE_22050_HZ,
1271///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1272///     }
1273/// }
1274///
1275/// pcm_clip! {
1276///     Digit2 {
1277///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/2_22050.s16"),
1278///         source_sample_rate_hz: VOICE_22050_HZ,
1279///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1280///     }
1281/// }
1282///
1283/// pcm_clip! {
1284///     Nasa {
1285///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/nasa_22k.s16"),
1286///         source_sample_rate_hz: VOICE_22050_HZ,
1287///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1288///     }
1289/// }
1290///
1291/// fn play_resampled_countdown(audio_player: &impl AudioPlayer<NARROWBAND_8000_HZ>) {
1292///     type PlayableRef = &'static dyn Playable<NARROWBAND_8000_HZ>;
1293///
1294///     const DIGITS: [PlayableRef; 3] =
1295///         [&Digit0::adpcm_clip(), &Digit1::adpcm_clip(), &Digit2::adpcm_clip()];
1296///     const NASA: PlayableRef = &Nasa::pcm_clip()
1297///         .with_gain(Gain::percent(25))
1298///         .with_adpcm::<{ Nasa::ADPCM_DATA_LEN }>();
1299///
1300///     audio_player.play([DIGITS[2], DIGITS[1], DIGITS[0], NASA], AtEnd::Stop);
1301/// }
1302///
1303/// # struct DemoAudioPlayer;
1304/// # impl AudioPlayer<NARROWBAND_8000_HZ> for DemoAudioPlayer {
1305/// #     const SAMPLE_RATE_HZ: u32 = NARROWBAND_8000_HZ;
1306/// #     const MAX_CLIPS: usize = 16;
1307/// #     const INITIAL_VOLUME: Volume = Volume::MAX;
1308/// #     const MAX_VOLUME: Volume = Volume::percent(50);
1309/// #     fn play<I>(&self, _audio_clips: I, _at_end: AtEnd)
1310/// #     where
1311/// #         I: IntoIterator<Item = &'static dyn Playable<NARROWBAND_8000_HZ>>,
1312/// #     {
1313/// #     }
1314/// #     fn stop(&self) {}
1315/// #     async fn wait_until_stopped(&self) {}
1316/// #     fn set_volume(&self, _volume: Volume) {}
1317/// #     fn volume(&self) -> Volume {
1318/// #         Self::INITIAL_VOLUME
1319/// #     }
1320/// # }
1321/// # struct ButtonMock;
1322/// # impl device_envoy_core::button::__ButtonMonitor for ButtonMock {
1323/// #     fn is_pressed_raw(&self) -> bool { false }
1324/// #     async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
1325/// # }
1326/// # impl Button for ButtonMock {}
1327/// fn main() {
1328///     let mut button = ButtonMock;
1329///     let audio_player = DemoAudioPlayer;
1330///     play_resampled_countdown(&audio_player);
1331///     let _future = button.wait_for_press();
1332/// }
1333/// ```
1334#[allow(async_fn_in_trait)]
1335pub trait AudioPlayer<const SAMPLE_RATE_HZ: u32> {
1336    /// Sample rate in hertz for this generated player type.
1337    const SAMPLE_RATE_HZ: u32;
1338    /// Maximum number of clips accepted by `play(...)` for this generated type.
1339    const MAX_CLIPS: usize;
1340    /// Initial runtime volume relative to [`Self::MAX_VOLUME`].
1341    const INITIAL_VOLUME: Volume;
1342    /// Runtime volume ceiling for this generated player type.
1343    const MAX_VOLUME: Volume;
1344
1345    /// Starts playback of one or more static audio clips.
1346    ///
1347    /// Accepts any array-like or iterator input. The maximum number of clips
1348    /// is determined by the generated type configuration.
1349    ///
1350    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1351    fn play<I>(&self, audio_clips: I, at_end: AtEnd)
1352    where
1353        I: IntoIterator<Item = &'static dyn Playable<SAMPLE_RATE_HZ>>;
1354
1355    /// Stops current playback as soon as possible.
1356    ///
1357    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1358    fn stop(&self);
1359
1360    /// Waits until playback is stopped.
1361    ///
1362    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1363    async fn wait_until_stopped(&self);
1364
1365    /// Sets runtime playback volume relative to the generated player's max volume.
1366    ///
1367    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1368    fn set_volume(&self, volume: Volume);
1369
1370    /// Returns the current runtime playback volume relative to max volume.
1371    ///
1372    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1373    fn volume(&self) -> Volume;
1374}
1375
1376mod sealed {
1377    use super::{AdpcmClip, PcmClip, PlaybackClip, SilenceClip};
1378
1379    pub(crate) trait PlayableSealed<const SAMPLE_RATE_HZ: u32> {
1380        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ>;
1381    }
1382
1383    impl<const SAMPLE_RATE_HZ: u32> PlayableSealed<SAMPLE_RATE_HZ> for PcmClip<SAMPLE_RATE_HZ> {
1384        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1385            PlaybackClip::Pcm(self)
1386        }
1387    }
1388
1389    impl<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize> PlayableSealed<SAMPLE_RATE_HZ>
1390        for PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>
1391    {
1392        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1393            PlaybackClip::Pcm(self)
1394        }
1395    }
1396
1397    impl<const SAMPLE_RATE_HZ: u32> PlayableSealed<SAMPLE_RATE_HZ> for AdpcmClip<SAMPLE_RATE_HZ> {
1398        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1399            PlaybackClip::Adpcm(self)
1400        }
1401    }
1402
1403    impl<const SAMPLE_RATE_HZ: u32, const DATA_LEN: usize> PlayableSealed<SAMPLE_RATE_HZ>
1404        for AdpcmClip<SAMPLE_RATE_HZ, [u8; DATA_LEN]>
1405    {
1406        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1407            PlaybackClip::Adpcm(self)
1408        }
1409    }
1410
1411    impl<const SAMPLE_RATE_HZ: u32> PlayableSealed<SAMPLE_RATE_HZ> for SilenceClip {
1412        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1413            PlaybackClip::Silence(self.duration())
1414        }
1415    }
1416}
1417
1418/// Unsized view of static uncompressed (PCM) audio clip data.
1419///
1420/// For fixed-size, const-friendly storage, see [`PcmClipBuf`].
1421///
1422/// See the [audio_player module documentation](mod@crate::audio_player) for
1423/// usage examples.
1424pub struct PcmClip<const SAMPLE_RATE_HZ: u32, T: ?Sized = [i16]> {
1425    samples: T,
1426}
1427
1428impl<const SAMPLE_RATE_HZ: u32, T: ?Sized> PcmClip<SAMPLE_RATE_HZ, T> {
1429    /// Returns a reference to the raw PCM sample data.
1430    #[must_use]
1431    pub fn samples(&self) -> &T {
1432        &self.samples
1433    }
1434}
1435
1436/// Sized, const-friendly storage for uncompressed (PCM) audio clip data.
1437///
1438/// For unsized clip references, see [`PcmClip`].
1439///
1440/// Sample rate is part of the type, so clips with different sample rates are
1441/// not assignment-compatible.
1442///
1443/// See the [audio_player module documentation](mod@crate::audio_player) for
1444/// usage examples.
1445pub type PcmClipBuf<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize> =
1446    PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>;
1447
1448/// **Implementation for fixed-size clips (`PcmClipBuf`).**
1449///
1450/// This impl applies to [`PcmClip`] with array-backed storage:
1451/// `PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>`
1452/// (which is what [`PcmClipBuf`] aliases).
1453impl<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>
1454    PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>
1455{
1456    /// Returns a new clip with linear sample gain applied.
1457    ///
1458    /// This is intended to be used in const clip definitions so the adjusted
1459    /// samples are computed ahead of time.
1460    ///
1461    /// Gain multiplication uses i32 math and saturates to i16 sample bounds.
1462    /// Large boosts can hard-clip peaks and introduce distortion.
1463    ///
1464    /// See the [audio_player module documentation](mod@crate::audio_player) for
1465    /// usage examples.
1466    #[must_use]
1467    pub const fn with_gain(self, gain: Gain) -> Self {
1468        assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1469        let mut scaled_samples = [0_i16; SAMPLE_COUNT];
1470        let mut sample_index = 0_usize;
1471        // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1472        while sample_index < SAMPLE_COUNT {
1473            scaled_samples[sample_index] =
1474                scale_sample_with_linear(self.samples[sample_index], gain.linear());
1475            sample_index += 1;
1476        }
1477        Self {
1478            samples: scaled_samples,
1479        }
1480    }
1481
1482    /// Returns this clip with a linear attack and release envelope.
1483    ///
1484    /// This is useful when you want explicit control over click reduction at
1485    /// the start/end of generated tones.
1486    ///
1487    /// See the [audio_player module documentation](mod@crate::audio_player) for
1488    /// usage examples.
1489    #[must_use]
1490    pub(crate) const fn with_attack_release(self, attack: Duration, release: Duration) -> Self {
1491        assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1492        let attack_sample_count = __samples_for_duration(attack, SAMPLE_RATE_HZ);
1493        let release_sample_count = __samples_for_duration(release, SAMPLE_RATE_HZ);
1494        self.with_attack_release_sample_count(attack_sample_count, release_sample_count)
1495    }
1496
1497    #[must_use]
1498    const fn with_attack_release_sample_count(
1499        self,
1500        attack_sample_count: usize,
1501        release_sample_count: usize,
1502    ) -> Self {
1503        assert!(
1504            attack_sample_count <= SAMPLE_COUNT,
1505            "attack duration must fit within clip duration"
1506        );
1507        assert!(
1508            release_sample_count <= SAMPLE_COUNT,
1509            "release duration must fit within clip duration"
1510        );
1511        assert!(
1512            attack_sample_count + release_sample_count <= SAMPLE_COUNT,
1513            "attack + release must fit within clip duration"
1514        );
1515
1516        let mut shaped_samples = self.samples;
1517
1518        if attack_sample_count > 0 {
1519            let attack_sample_count_i32 = attack_sample_count as i32;
1520            let mut sample_index = 0usize;
1521            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1522            while sample_index < attack_sample_count {
1523                let envelope_numerator_i32 = sample_index as i32;
1524                shaped_samples[sample_index] = scale_sample_with_linear(
1525                    shaped_samples[sample_index],
1526                    (envelope_numerator_i32 * i16::MAX as i32) / attack_sample_count_i32,
1527                );
1528                sample_index += 1;
1529            }
1530        }
1531
1532        if release_sample_count > 0 {
1533            let release_sample_count_i32 = release_sample_count as i32;
1534            let release_start_index = SAMPLE_COUNT - release_sample_count;
1535            let mut release_index = 0usize;
1536            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1537            while release_index < release_sample_count {
1538                let sample_index = release_start_index + release_index;
1539                let envelope_numerator_i32 = (release_sample_count - release_index) as i32;
1540                shaped_samples[sample_index] = scale_sample_with_linear(
1541                    shaped_samples[sample_index],
1542                    (envelope_numerator_i32 * i16::MAX as i32) / release_sample_count_i32,
1543                );
1544                release_index += 1;
1545            }
1546        }
1547
1548        Self {
1549            samples: shaped_samples,
1550        }
1551    }
1552
1553    /// Returns the compressed (ADPCM) encoding for this clip.
1554    ///
1555    /// See the [audio_player module documentation](mod@crate::audio_player) for
1556    /// usage examples.
1557    #[must_use]
1558    pub const fn with_adpcm<const DATA_LEN: usize>(
1559        &self,
1560    ) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1561        self.with_adpcm_block_align::<DATA_LEN>(ADPCM_ENCODE_BLOCK_ALIGN)
1562    }
1563
1564    #[must_use]
1565    pub(crate) const fn with_adpcm_block_align<const DATA_LEN: usize>(
1566        &self,
1567        block_align: usize,
1568    ) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1569        assert!(block_align >= 5, "block_align must be >= 5");
1570        assert!(
1571            block_align <= u16::MAX as usize,
1572            "block_align must fit in u16"
1573        );
1574        let samples_per_block = __adpcm_samples_per_block(block_align);
1575        assert!(
1576            samples_per_block <= u16::MAX as usize,
1577            "samples_per_block must fit in u16"
1578        );
1579        assert!(
1580            DATA_LEN
1581                == __adpcm_data_len_for_pcm_samples_with_block_align(SAMPLE_COUNT, block_align),
1582            "adpcm data length must match sample count and block_align"
1583        );
1584        if SAMPLE_COUNT == 0 {
1585            return AdpcmClip::new(block_align as u16, samples_per_block as u16, [0; DATA_LEN]);
1586        }
1587
1588        let mut adpcm_data = [0_u8; DATA_LEN];
1589        let mut sample_index = 0usize;
1590        let mut data_index = 0usize;
1591        let payload_len_per_block = block_align - 4;
1592
1593        // TODO_NIGHTLY When nightly feature const_for becomes stable, replace these while loops with for loops.
1594        while sample_index < SAMPLE_COUNT {
1595            let mut predictor_i32 = self.samples[sample_index] as i32;
1596            let mut step_index_i32 = 0_i32;
1597
1598            let predictor_i16 = predictor_i32 as i16;
1599            let predictor_bytes = predictor_i16.to_le_bytes();
1600            adpcm_data[data_index] = predictor_bytes[0];
1601            adpcm_data[data_index + 1] = predictor_bytes[1];
1602            adpcm_data[data_index + 2] = step_index_i32 as u8;
1603            adpcm_data[data_index + 3] = 0;
1604            data_index += 4;
1605            sample_index += 1;
1606
1607            let mut payload_byte_index = 0usize;
1608            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1609            while payload_byte_index < payload_len_per_block {
1610                let mut adpcm_byte = 0_u8;
1611
1612                let mut nibble_index = 0usize;
1613                // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1614                while nibble_index < 2 {
1615                    let target_sample_i32 = if sample_index < SAMPLE_COUNT {
1616                        self.samples[sample_index] as i32
1617                    } else {
1618                        predictor_i32
1619                    };
1620                    let adpcm_nibble = encode_adpcm_nibble(
1621                        target_sample_i32,
1622                        &mut predictor_i32,
1623                        &mut step_index_i32,
1624                    );
1625                    adpcm_byte |= adpcm_nibble << (nibble_index * 4);
1626                    sample_index += 1;
1627                    nibble_index += 1;
1628                }
1629
1630                adpcm_data[data_index] = adpcm_byte;
1631                data_index += 1;
1632                payload_byte_index += 1;
1633            }
1634        }
1635
1636        AdpcmClip::new(block_align as u16, samples_per_block as u16, adpcm_data)
1637    }
1638}
1639
1640// Must be `pub` so platform-crate `device_loop` and play functions can access
1641// these across the crate boundary.
1642#[doc(hidden)]
1643pub enum AudioCommand<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32> {
1644    Play {
1645        audio_clips: Vec<PlaybackClip<SAMPLE_RATE_HZ>, MAX_CLIPS>,
1646        at_end: AtEnd,
1647    },
1648    Stop,
1649}
1650
1651/// Static resources for audio player runtime handles.
1652// Must be `pub` so `audio_player!` expansions in downstream crates can reference this type.
1653#[doc(hidden)]
1654pub struct AudioPlayerStatic<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32> {
1655    command_signal: Signal<CriticalSectionRawMutex, AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>>,
1656    stopped_signal: Signal<CriticalSectionRawMutex, ()>,
1657    is_playing: AtomicBool,
1658    has_pending_play: AtomicBool,
1659    max_volume_linear: i32,
1660    runtime_volume_relative_linear: AtomicI32,
1661}
1662
1663impl<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>
1664    AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>
1665{
1666    /// Creates static resources for a player.
1667    #[must_use]
1668    pub const fn new_static() -> Self {
1669        Self::new_static_with_max_volume_and_initial_volume(Volume::MAX, Volume::MAX)
1670    }
1671
1672    /// Creates static resources for a player with a runtime volume ceiling.
1673    #[must_use]
1674    pub const fn new_static_with_max_volume(max_volume: Volume) -> Self {
1675        Self::new_static_with_max_volume_and_initial_volume(max_volume, Volume::MAX)
1676    }
1677
1678    /// Creates static resources for a player with a runtime volume ceiling
1679    /// and an initial runtime volume relative to that ceiling.
1680    #[must_use]
1681    pub const fn new_static_with_max_volume_and_initial_volume(
1682        max_volume: Volume,
1683        initial_volume: Volume,
1684    ) -> Self {
1685        Self {
1686            command_signal: Signal::new(),
1687            stopped_signal: Signal::new(),
1688            is_playing: AtomicBool::new(false),
1689            has_pending_play: AtomicBool::new(false),
1690            max_volume_linear: max_volume.to_i16() as i32,
1691            runtime_volume_relative_linear: AtomicI32::new(initial_volume.to_i16() as i32),
1692        }
1693    }
1694
1695    fn signal(&self, audio_command: AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>) {
1696        self.command_signal.signal(audio_command);
1697    }
1698
1699    fn mark_pending_play(&self) {
1700        self.has_pending_play.store(true, AtomicOrdering::Relaxed);
1701    }
1702
1703    /// Marks the player as currently playing. Called by platform-crate `device_loop`.
1704    #[doc(hidden)]
1705    pub fn mark_playing(&self) {
1706        self.has_pending_play.store(false, AtomicOrdering::Relaxed);
1707        self.is_playing.store(true, AtomicOrdering::Relaxed);
1708    }
1709
1710    /// Marks the player as stopped. Called by platform-crate `device_loop`.
1711    #[doc(hidden)]
1712    pub fn mark_stopped(&self) {
1713        self.has_pending_play.store(false, AtomicOrdering::Relaxed);
1714        self.is_playing.store(false, AtomicOrdering::Relaxed);
1715        self.stopped_signal.signal(());
1716    }
1717
1718    fn is_idle(&self) -> bool {
1719        !self.has_pending_play.load(AtomicOrdering::Relaxed)
1720            && !self.is_playing.load(AtomicOrdering::Relaxed)
1721    }
1722
1723    async fn wait_until_stopped(&self) {
1724        while !self.is_idle() {
1725            self.stopped_signal.wait().await;
1726        }
1727    }
1728
1729    fn set_runtime_volume(&self, volume: Volume) {
1730        self.runtime_volume_relative_linear
1731            .store(volume.to_i16() as i32, Ordering::Relaxed);
1732    }
1733
1734    fn runtime_volume(&self) -> Volume {
1735        Volume::from_i16(self.runtime_volume_relative_linear.load(Ordering::Relaxed) as i16)
1736    }
1737
1738    /// Returns the effective runtime volume after applying max-volume scaling.
1739    /// Called by platform-crate play functions.
1740    #[doc(hidden)]
1741    pub fn effective_runtime_volume(&self) -> Volume {
1742        let runtime_volume_relative = self.runtime_volume();
1743        Volume::from_i16(scale_linear(self.max_volume_linear, runtime_volume_relative) as i16)
1744    }
1745
1746    /// Awaits the next audio command. Used by RP-style `device_loop`.
1747    #[doc(hidden)]
1748    pub async fn wait(&self) -> AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ> {
1749        self.command_signal.wait().await
1750    }
1751
1752    /// Tries to take a pending audio command without waiting. Used by
1753    /// ESP-style play functions and both platforms' `device_loop`.
1754    #[doc(hidden)]
1755    pub fn try_take_command(&self) -> Option<AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>> {
1756        self.command_signal.try_take()
1757    }
1758}
1759
1760// Must be `pub` so platform runtime handles can call this from another crate.
1761#[doc(hidden)]
1762pub fn __audio_player_play<I, const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1763    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1764    audio_clips: I,
1765    at_end: AtEnd,
1766) where
1767    I: IntoIterator<Item = &'static dyn Playable<SAMPLE_RATE_HZ>>,
1768{
1769    assert!(MAX_CLIPS > 0, "play disabled: max_clips is 0");
1770    let mut audio_clip_sequence: Vec<PlaybackClip<SAMPLE_RATE_HZ>, MAX_CLIPS> = Vec::new();
1771    for audio_clip in audio_clips {
1772        assert!(
1773            audio_clip_sequence
1774                .push(sealed::PlayableSealed::playback_clip(audio_clip))
1775                .is_ok(),
1776            "play sequence fits within max_clips"
1777        );
1778    }
1779    assert!(
1780        !audio_clip_sequence.is_empty(),
1781        "play requires at least one clip"
1782    );
1783
1784    audio_player_static.mark_pending_play();
1785    audio_player_static.signal(AudioCommand::Play {
1786        audio_clips: audio_clip_sequence,
1787        at_end,
1788    });
1789}
1790
1791// Must be `pub` so platform runtime handles can call this from another crate.
1792#[doc(hidden)]
1793pub fn __audio_player_stop<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1794    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1795) {
1796    audio_player_static.signal(AudioCommand::Stop);
1797}
1798
1799// Must be `pub` so platform runtime handles can call this from another crate.
1800#[doc(hidden)]
1801pub async fn __audio_player_wait_until_stopped<
1802    const MAX_CLIPS: usize,
1803    const SAMPLE_RATE_HZ: u32,
1804>(
1805    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1806) {
1807    audio_player_static.wait_until_stopped().await;
1808}
1809
1810// Must be `pub` so platform runtime handles can call this from another crate.
1811#[doc(hidden)]
1812pub fn __audio_player_set_volume<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1813    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1814    volume: Volume,
1815) {
1816    audio_player_static.set_runtime_volume(volume);
1817}
1818
1819// Must be `pub` so platform runtime handles can call this from another crate.
1820#[doc(hidden)]
1821#[must_use]
1822pub fn __audio_player_volume<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1823    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1824) -> Volume {
1825    audio_player_static.runtime_volume()
1826}
1827
1828/// Const backend helper that creates a PCM sine-wave clip.
1829///
1830/// This is intentionally `#[doc(hidden)]` because user-facing construction
1831/// should prefer [`tone!`](macro@crate::tone).
1832#[must_use]
1833#[doc(hidden)]
1834pub const fn __tone_pcm_clip<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>(
1835    frequency_hz: u32,
1836) -> PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT> {
1837    __tone_pcm_clip_with_duration::<SAMPLE_RATE_HZ, SAMPLE_COUNT>(
1838        frequency_hz,
1839        duration_for_sample_count(SAMPLE_COUNT, SAMPLE_RATE_HZ),
1840    )
1841}
1842
1843/// Const backend helper that creates a PCM sine-wave clip with explicit
1844/// duration metadata used for built-in shaping.
1845/// This uses [`core::time::Duration`] for tone duration.
1846#[must_use]
1847#[doc(hidden)]
1848pub const fn __tone_pcm_clip_with_duration<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>(
1849    frequency_hz: u32,
1850    duration: core::time::Duration,
1851) -> PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT> {
1852    assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1853    let mut samples = [0_i16; SAMPLE_COUNT];
1854    let phase_step_u64 = ((frequency_hz as u64) << 32) / SAMPLE_RATE_HZ as u64;
1855    let phase_step_u32 = phase_step_u64 as u32;
1856    let mut phase_u32 = 0_u32;
1857
1858    let mut sample_index = 0usize;
1859    // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1860    while sample_index < SAMPLE_COUNT {
1861        samples[sample_index] = sine_sample_from_phase(phase_u32);
1862        phase_u32 = phase_u32.wrapping_add(phase_step_u32);
1863        sample_index += 1;
1864    }
1865
1866    // Add an attack and release duration to reduce clicks. It is min(50ms, 1/4 of total duration)
1867    let max_duration = Duration::from_millis(50);
1868    assert!(max_duration.as_secs() == 0, "50ms cap must be sub-second");
1869    let attack_release_duration = match (duration.as_secs(), duration.subsec_nanos()) {
1870        (0, nanos) if nanos / 4 < max_duration.subsec_nanos() => Duration::new(0, nanos / 4),
1871        (_, _) => max_duration,
1872    };
1873    PcmClip { samples }.with_attack_release(attack_release_duration, attack_release_duration)
1874}
1875
1876/// Builds a fixed-size PCM clip from samples.
1877///
1878/// This is intentionally `#[doc(hidden)]` because user-facing clip
1879/// construction should prefer `pcm_clip!`, `adpcm_clip!`, and `tone!`.
1880#[must_use]
1881#[doc(hidden)]
1882pub const fn __pcm_clip_from_samples<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>(
1883    samples: [i16; SAMPLE_COUNT],
1884) -> PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT> {
1885    assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1886    PcmClip { samples }
1887}
1888
1889/// Const backend helper that builds a fixed-size ADPCM clip from parts.
1890///
1891/// This is intentionally `#[doc(hidden)]` because user-facing clip
1892/// construction should prefer `adpcm_clip!` and conversion helpers.
1893#[must_use]
1894#[doc(hidden)]
1895pub const fn __adpcm_clip_from_parts<const SAMPLE_RATE_HZ: u32, const DATA_LEN: usize>(
1896    block_align: u16,
1897    samples_per_block: u16,
1898    data: [u8; DATA_LEN],
1899) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1900    AdpcmClip::new(block_align, samples_per_block, data)
1901}
1902
1903/// Const backend helper that encodes PCM into ADPCM with an explicit block size.
1904///
1905/// This helper must be `pub` because macro expansions in downstream crates call
1906/// it at the call site, but it is not a user-facing API.
1907#[must_use]
1908#[doc(hidden)]
1909pub const fn __pcm_with_adpcm_block_align<
1910    const SAMPLE_RATE_HZ: u32,
1911    const SAMPLE_COUNT: usize,
1912    const DATA_LEN: usize,
1913>(
1914    source_pcm_clip: &PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT>,
1915    block_align: usize,
1916) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1917    source_pcm_clip.with_adpcm_block_align::<DATA_LEN>(block_align)
1918}
1919
1920/// Const backend helper that resamples a PCM clip to a destination timeline.
1921///
1922/// This is intentionally `#[doc(hidden)]` because resampling is configured by
1923/// `pcm_clip!`/`adpcm_clip!` inputs (`target_sample_rate_hz`) rather than by a
1924/// direct clip method.
1925#[must_use]
1926#[doc(hidden)]
1927pub const fn __resample_pcm_clip<
1928    const SOURCE_HZ: u32,
1929    const SOURCE_COUNT: usize,
1930    const TARGET_HZ: u32,
1931    const TARGET_COUNT: usize,
1932>(
1933    source_pcm_clip: PcmClipBuf<SOURCE_HZ, SOURCE_COUNT>,
1934) -> PcmClipBuf<TARGET_HZ, TARGET_COUNT> {
1935    assert!(SOURCE_COUNT > 0, "source sample count must be > 0");
1936    assert!(TARGET_HZ > 0, "destination sample_rate_hz must be > 0");
1937    let expected_destination_sample_count =
1938        __resampled_sample_count(SOURCE_COUNT, SOURCE_HZ, TARGET_HZ);
1939    assert!(
1940        TARGET_COUNT == expected_destination_sample_count,
1941        "destination sample count must preserve duration"
1942    );
1943
1944    let source_samples = source_pcm_clip.samples;
1945    let mut resampled_samples = [0_i16; TARGET_COUNT];
1946    let mut sample_index = 0_usize;
1947
1948    // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1949    while sample_index < TARGET_COUNT {
1950        let source_position_numerator_u128 = sample_index as u128 * SOURCE_HZ as u128;
1951        let source_index_u128 = source_position_numerator_u128 / TARGET_HZ as u128;
1952        let source_fraction_numerator_u128 = source_position_numerator_u128 % TARGET_HZ as u128;
1953        let source_index = source_index_u128 as usize;
1954
1955        resampled_samples[sample_index] = if source_index + 1 >= SOURCE_COUNT {
1956            source_samples[SOURCE_COUNT - 1]
1957        } else if source_fraction_numerator_u128 == 0 {
1958            source_samples[source_index]
1959        } else {
1960            let left_sample_i128 = source_samples[source_index] as i128;
1961            let right_sample_i128 = source_samples[source_index + 1] as i128;
1962            let sample_delta_i128 = right_sample_i128 - left_sample_i128;
1963            let denom_i128 = TARGET_HZ as i128;
1964            let numerator_i128 = sample_delta_i128 * source_fraction_numerator_u128 as i128;
1965            let rounded_i128 = if numerator_i128 >= 0 {
1966                (numerator_i128 + (denom_i128 / 2)) / denom_i128
1967            } else {
1968                (numerator_i128 - (denom_i128 / 2)) / denom_i128
1969            };
1970            clamp_i64_to_i16((left_sample_i128 + rounded_i128) as i64)
1971        };
1972
1973        sample_index += 1;
1974    }
1975
1976    PcmClip {
1977        samples: resampled_samples,
1978    }
1979}
1980
1981// Must be `pub` because platform-crate `device_loop` and play functions call
1982// this directly.
1983#[doc(hidden)]
1984pub const fn decode_adpcm_nibble_const(
1985    adpcm_nibble: u8,
1986    predictor_i32: &mut i32,
1987    step_index_i32: &mut i32,
1988) -> i16 {
1989    let step = ADPCM_STEP_TABLE[*step_index_i32 as usize];
1990    let mut delta = step >> 3;
1991
1992    if (adpcm_nibble & 0x01) != 0 {
1993        delta += step >> 2;
1994    }
1995    if (adpcm_nibble & 0x02) != 0 {
1996        delta += step >> 1;
1997    }
1998    if (adpcm_nibble & 0x04) != 0 {
1999        delta += step;
2000    }
2001
2002    if (adpcm_nibble & 0x08) != 0 {
2003        *predictor_i32 -= delta;
2004    } else {
2005        *predictor_i32 += delta;
2006    }
2007
2008    if *predictor_i32 < i16::MIN as i32 {
2009        *predictor_i32 = i16::MIN as i32;
2010    } else if *predictor_i32 > i16::MAX as i32 {
2011        *predictor_i32 = i16::MAX as i32;
2012    }
2013    *step_index_i32 += ADPCM_INDEX_TABLE[adpcm_nibble as usize];
2014    if *step_index_i32 < 0 {
2015        *step_index_i32 = 0;
2016    } else if *step_index_i32 > 88 {
2017        *step_index_i32 = 88;
2018    }
2019
2020    *predictor_i32 as i16
2021}
2022
2023const fn encode_adpcm_nibble(
2024    target_sample_i32: i32,
2025    predictor_i32: &mut i32,
2026    step_index_i32: &mut i32,
2027) -> u8 {
2028    let step = ADPCM_STEP_TABLE[*step_index_i32 as usize];
2029    let mut diff = target_sample_i32 - *predictor_i32;
2030    let mut adpcm_nibble = 0_u8;
2031    if diff < 0 {
2032        adpcm_nibble |= 0x08;
2033        diff = -diff;
2034    }
2035
2036    let mut delta = step >> 3;
2037    if diff >= step {
2038        adpcm_nibble |= 0x04;
2039        diff -= step;
2040        delta += step;
2041    }
2042    if diff >= (step >> 1) {
2043        adpcm_nibble |= 0x02;
2044        diff -= step >> 1;
2045        delta += step >> 1;
2046    }
2047    if diff >= (step >> 2) {
2048        adpcm_nibble |= 0x01;
2049        delta += step >> 2;
2050    }
2051
2052    if (adpcm_nibble & 0x08) != 0 {
2053        *predictor_i32 -= delta;
2054    } else {
2055        *predictor_i32 += delta;
2056    }
2057
2058    if *predictor_i32 < i16::MIN as i32 {
2059        *predictor_i32 = i16::MIN as i32;
2060    } else if *predictor_i32 > i16::MAX as i32 {
2061        *predictor_i32 = i16::MAX as i32;
2062    }
2063    *step_index_i32 += ADPCM_INDEX_TABLE[adpcm_nibble as usize];
2064    if *step_index_i32 < 0 {
2065        *step_index_i32 = 0;
2066    } else if *step_index_i32 > 88 {
2067        *step_index_i32 = 88;
2068    }
2069
2070    adpcm_nibble
2071}
2072
2073// ============================================================================
2074// Macros
2075// ============================================================================
2076
2077#[doc(hidden)]
2078#[macro_export]
2079macro_rules! pcm_clip {
2080    // TODO_NIGHTLY When nightly feature `decl_macro` becomes stable, change this
2081    // code by replacing `#[macro_export] macro_rules!` with module-scoped `pub macro`
2082    // so macro visibility and helper exposure can be controlled more precisely.
2083    ($($tt:tt)*) => { $crate::__audio_clip_parse! { $($tt)* } };
2084}
2085
2086#[doc(hidden)]
2087#[macro_export]
2088macro_rules! __audio_clip_parse {
2089    (
2090        $vis:vis $name:ident {
2091            file: $file:expr,
2092            sample_rate_hz: $source_sample_rate_hz:expr,
2093            target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2094        }
2095    ) => {
2096        $crate::__audio_clip_dispatch! {
2097            vis: $vis,
2098            name: $name,
2099            file: $file,
2100            source_sample_rate_hz: $source_sample_rate_hz,
2101            target_sample_rate_hz: $target_sample_rate_hz,
2102        }
2103    };
2104    (
2105        $vis:vis $name:ident {
2106            file: $file:expr,
2107            sample_rate_hz: $source_sample_rate_hz:expr,
2108            target_sample_rate_hz: $target_sample_rate_hz:expr,
2109            $(,)?
2110        }
2111    ) => {
2112        $crate::__audio_clip_dispatch! {
2113            vis: $vis,
2114            name: $name,
2115            file: $file,
2116            source_sample_rate_hz: $source_sample_rate_hz,
2117            target_sample_rate_hz: $target_sample_rate_hz,
2118        }
2119    };
2120    (
2121        $vis:vis $name:ident {
2122            file: $file:expr,
2123            sample_rate_hz: $sample_rate_hz:expr $(,)?
2124        }
2125    ) => {
2126        $crate::__audio_clip_dispatch! {
2127            vis: $vis,
2128            name: $name,
2129            file: $file,
2130            source_sample_rate_hz: $sample_rate_hz,
2131            target_sample_rate_hz: $sample_rate_hz,
2132        }
2133    };
2134    (
2135        $vis:vis $name:ident {
2136            file: $file:expr,
2137            sample_rate_hz: $sample_rate_hz:expr,
2138            $(,)?
2139        }
2140    ) => {
2141        $crate::__audio_clip_dispatch! {
2142            vis: $vis,
2143            name: $name,
2144            file: $file,
2145            source_sample_rate_hz: $sample_rate_hz,
2146            target_sample_rate_hz: $sample_rate_hz,
2147        }
2148    };
2149    (
2150        $vis:vis $name:ident {
2151            file: $file:expr,
2152            sample_rate_hz: $sample_rate_hz:expr,
2153            $(,)?
2154        }
2155    ) => {
2156        $crate::__audio_clip_dispatch! {
2157            vis: $vis,
2158            name: $name,
2159            file: $file,
2160            source_sample_rate_hz: $sample_rate_hz,
2161            target_sample_rate_hz: $sample_rate_hz,
2162        }
2163    };
2164    // Alias: `source_sample_rate_hz:` is accepted as a synonym for `sample_rate_hz:`.
2165    (
2166        $vis:vis $name:ident {
2167            file: $file:expr,
2168            source_sample_rate_hz: $source_sample_rate_hz:expr,
2169            target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2170        }
2171    ) => {
2172        $crate::__audio_clip_dispatch! {
2173            vis: $vis,
2174            name: $name,
2175            file: $file,
2176            source_sample_rate_hz: $source_sample_rate_hz,
2177            target_sample_rate_hz: $target_sample_rate_hz,
2178        }
2179    };
2180    (
2181        $vis:vis $name:ident {
2182            file: $file:expr,
2183            source_sample_rate_hz: $sample_rate_hz:expr $(,)?
2184        }
2185    ) => {
2186        $crate::__audio_clip_dispatch! {
2187            vis: $vis,
2188            name: $name,
2189            file: $file,
2190            source_sample_rate_hz: $sample_rate_hz,
2191            target_sample_rate_hz: $sample_rate_hz,
2192        }
2193    };
2194}
2195
2196#[doc(hidden)]
2197#[macro_export]
2198macro_rules! __audio_clip_dispatch {
2199    (
2200        vis: $vis:vis,
2201        name: $name:ident,
2202        file: $file:expr,
2203        source_sample_rate_hz: $source_sample_rate_hz:expr,
2204        target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2205    ) => {
2206        $crate::__audio_clip_impl! {
2207            vis: $vis,
2208            name: $name,
2209            file: $file,
2210            source_sample_rate_hz: $source_sample_rate_hz,
2211            target_sample_rate_hz: $target_sample_rate_hz,
2212        }
2213    };
2214}
2215
2216#[doc(hidden)]
2217#[macro_export]
2218macro_rules! __audio_clip_impl {
2219    (
2220        vis: $vis:vis,
2221        name: $name:ident,
2222        file: $file:expr,
2223        source_sample_rate_hz: $source_sample_rate_hz:expr,
2224        target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2225    ) => {
2226        $crate::__paste! {
2227            const [<$name:upper _SOURCE_SAMPLE_RATE_HZ>]: u32 = $source_sample_rate_hz;
2228            const [<$name:upper _TARGET_SAMPLE_RATE_HZ>]: u32 = $target_sample_rate_hz;
2229
2230            #[allow(non_snake_case)]
2231            #[doc = concat!(
2232                "Audio clip module generated by [`pcm_clip!`](macro@crate::audio_player::pcm_clip).\n\n",
2233                "[`SAMPLE_RATE_HZ`](Self::SAMPLE_RATE_HZ), ",
2234                "[`PCM_SAMPLE_COUNT`](Self::PCM_SAMPLE_COUNT), ",
2235                "[`ADPCM_DATA_LEN`](Self::ADPCM_DATA_LEN), ",
2236                "[`pcm_clip`](Self::pcm_clip), ",
2237                "and [`adpcm_clip`](Self::adpcm_clip)."
2238            )]
2239            $vis mod $name {
2240                // TODO_NIGHTLY When nightly feature inherent_associated_types becomes stable,
2241                // change generated clip items from a module to inherent associated items on a struct.
2242                const SOURCE_SAMPLE_RATE_HZ: u32 = super::[<$name:upper _SOURCE_SAMPLE_RATE_HZ>];
2243                const TARGET_SAMPLE_RATE_HZ: u32 = super::[<$name:upper _TARGET_SAMPLE_RATE_HZ>];
2244                #[doc = "Sample rate in hertz for this generated clip output."]
2245                pub const SAMPLE_RATE_HZ: u32 = TARGET_SAMPLE_RATE_HZ;
2246                const AUDIO_SAMPLE_BYTES_LEN: usize = include_bytes!($file).len();
2247                const SOURCE_SAMPLE_COUNT: usize = AUDIO_SAMPLE_BYTES_LEN / 2;
2248                #[doc = "Number of samples for uncompressed (PCM) version of this clip."]
2249                pub const PCM_SAMPLE_COUNT: usize = $crate::audio_player::__resampled_sample_count(
2250                    SOURCE_SAMPLE_COUNT,
2251                    SOURCE_SAMPLE_RATE_HZ,
2252                    TARGET_SAMPLE_RATE_HZ,
2253                );
2254                #[doc = "Byte length for compressed (ADPCM) encoding this clip."]
2255                pub const ADPCM_DATA_LEN: usize =
2256                    $crate::audio_player::__adpcm_data_len_for_pcm_samples(PCM_SAMPLE_COUNT);
2257
2258                #[allow(dead_code)]
2259                type SourcePcmClip = $crate::audio_player::PcmClipBuf<
2260                    { SOURCE_SAMPLE_RATE_HZ },
2261                    { SOURCE_SAMPLE_COUNT },
2262                >;
2263
2264                #[doc = "`const` function that returns the uncompressed (PCM) version of this clip."]
2265                #[must_use]
2266                pub const fn pcm_clip() -> $crate::audio_player::PcmClipBuf<
2267                    { SAMPLE_RATE_HZ },
2268                    { PCM_SAMPLE_COUNT },
2269                > {
2270                    assert!(
2271                        AUDIO_SAMPLE_BYTES_LEN % 2 == 0,
2272                        "audio byte length must be even for s16le"
2273                    );
2274
2275                    let audio_sample_s16le: &[u8; AUDIO_SAMPLE_BYTES_LEN] = include_bytes!($file);
2276                    let mut samples = [0_i16; SOURCE_SAMPLE_COUNT];
2277                    let mut sample_index = 0_usize;
2278                    while sample_index < SOURCE_SAMPLE_COUNT {
2279                        let byte_index = sample_index * 2;
2280                        samples[sample_index] = i16::from_le_bytes([
2281                            audio_sample_s16le[byte_index],
2282                            audio_sample_s16le[byte_index + 1],
2283                        ]);
2284                        sample_index += 1;
2285                    }
2286                    $crate::audio_player::__resample_pcm_clip::<
2287                        SOURCE_SAMPLE_RATE_HZ,
2288                        SOURCE_SAMPLE_COUNT,
2289                        TARGET_SAMPLE_RATE_HZ,
2290                        PCM_SAMPLE_COUNT,
2291                    >($crate::audio_player::__pcm_clip_from_samples::<
2292                        SOURCE_SAMPLE_RATE_HZ,
2293                        SOURCE_SAMPLE_COUNT,
2294                    >(samples))
2295                }
2296
2297                #[doc = "`const` function that returns the compressed (ADPCM) encoding for this clip."]
2298                #[must_use]
2299                pub const fn adpcm_clip() -> $crate::audio_player::AdpcmClipBuf<
2300                    { SAMPLE_RATE_HZ },
2301                    { ADPCM_DATA_LEN },
2302                > {
2303                    pcm_clip().with_adpcm::<ADPCM_DATA_LEN>()
2304                }
2305
2306            }
2307        }
2308    };
2309}
2310
2311#[doc = "Macro to \"compile in\" a compressed (ADPCM) WAV clip from an external file (includes syntax details)."]
2312#[doc = include_str!("audio_player/adpcm_clip_docs.md")]
2313#[doc = include_str!("audio_player/audio_prep_steps_1_2.md")]
2314#[doc = include_str!("audio_player/adpcm_clip_step_3.md")]
2315#[doc(inline)]
2316pub use crate::adpcm_clip;
2317
2318#[doc(hidden)]
2319#[macro_export]
2320macro_rules! adpcm_clip {
2321    ($($tt:tt)*) => { $crate::__adpcm_clip_parse! { $($tt)* } };
2322}
2323
2324#[doc(hidden)]
2325#[macro_export]
2326macro_rules! __adpcm_clip_parse {
2327    (
2328        $vis:vis $name:ident {
2329            file: $file:expr,
2330            target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2331        }
2332    ) => {
2333        $crate::__paste! {
2334            const [<$name:upper _TARGET_SAMPLE_RATE_HZ>]: u32 = $target_sample_rate_hz;
2335
2336            #[allow(non_snake_case)]
2337            #[allow(missing_docs)]
2338            $vis mod $name {
2339                const PARSED_WAV: $crate::audio_player::ParsedAdpcmWavHeader =
2340                    $crate::audio_player::__parse_adpcm_wav_header(include_bytes!($file));
2341                const SOURCE_SAMPLE_RATE_HZ: u32 = PARSED_WAV.sample_rate_hz;
2342                const TARGET_SAMPLE_RATE_HZ: u32 = super::[<$name:upper _TARGET_SAMPLE_RATE_HZ>];
2343                pub const SAMPLE_RATE_HZ: u32 = TARGET_SAMPLE_RATE_HZ;
2344
2345                const SOURCE_SAMPLE_COUNT: usize = PARSED_WAV.sample_count;
2346                #[doc = "Number of samples for uncompressed (PCM) version of this clip."]
2347                pub const PCM_SAMPLE_COUNT: usize = $crate::audio_player::__resampled_sample_count(
2348                    SOURCE_SAMPLE_COUNT,
2349                    SOURCE_SAMPLE_RATE_HZ,
2350                    TARGET_SAMPLE_RATE_HZ,
2351                );
2352                const BLOCK_ALIGN: usize = PARSED_WAV.block_align;
2353                const SOURCE_DATA_LEN: usize = PARSED_WAV.data_chunk_len;
2354                #[doc = "Byte length for compressed (ADPCM) encoding this clip."]
2355                pub const ADPCM_DATA_LEN: usize = if TARGET_SAMPLE_RATE_HZ == SOURCE_SAMPLE_RATE_HZ {
2356                    SOURCE_DATA_LEN
2357                } else {
2358                    $crate::audio_player::__adpcm_data_len_for_pcm_samples_with_block_align(
2359                        PCM_SAMPLE_COUNT,
2360                        BLOCK_ALIGN,
2361                    )
2362                };
2363                type SourceAdpcmClip = $crate::audio_player::AdpcmClipBuf<SOURCE_SAMPLE_RATE_HZ, SOURCE_DATA_LEN>;
2364
2365                #[must_use]
2366                const fn source_adpcm_clip() -> SourceAdpcmClip {
2367                    let wav_bytes = include_bytes!($file);
2368                    let parsed_wav = $crate::audio_player::__parse_adpcm_wav_header(wav_bytes);
2369                    assert!(parsed_wav.block_align <= u16::MAX as usize, "block_align too large");
2370                    assert!(
2371                        parsed_wav.samples_per_block <= u16::MAX as usize,
2372                        "samples_per_block too large"
2373                    );
2374
2375                    let mut adpcm_data = [0_u8; SOURCE_DATA_LEN];
2376                    let mut data_index = 0usize;
2377                    while data_index < SOURCE_DATA_LEN {
2378                        adpcm_data[data_index] = wav_bytes[parsed_wav.data_chunk_start + data_index];
2379                        data_index += 1;
2380                    }
2381
2382                    $crate::audio_player::__adpcm_clip_from_parts(
2383                        parsed_wav.block_align as u16,
2384                        parsed_wav.samples_per_block as u16,
2385                        adpcm_data,
2386                    )
2387                }
2388
2389                #[doc = "`const` function that returns the uncompressed (PCM) version of this clip."]
2390                #[must_use]
2391                pub const fn pcm_clip() -> $crate::audio_player::PcmClipBuf<SAMPLE_RATE_HZ, PCM_SAMPLE_COUNT> {
2392                    $crate::audio_player::__resample_pcm_clip::<
2393                        SOURCE_SAMPLE_RATE_HZ,
2394                        SOURCE_SAMPLE_COUNT,
2395                        TARGET_SAMPLE_RATE_HZ,
2396                        PCM_SAMPLE_COUNT,
2397                    >(source_adpcm_clip().with_pcm::<SOURCE_SAMPLE_COUNT>())
2398                }
2399
2400                #[doc = "`const` function that returns the compressed (ADPCM) encoding for this clip."]
2401                #[must_use]
2402                pub const fn adpcm_clip() -> $crate::audio_player::AdpcmClipBuf<SAMPLE_RATE_HZ, ADPCM_DATA_LEN> {
2403                    if TARGET_SAMPLE_RATE_HZ == SOURCE_SAMPLE_RATE_HZ {
2404                        let wav_bytes = include_bytes!($file);
2405                        let parsed_wav = $crate::audio_player::__parse_adpcm_wav_header(wav_bytes);
2406                        assert!(parsed_wav.block_align <= u16::MAX as usize, "block_align too large");
2407                        assert!(
2408                            parsed_wav.samples_per_block <= u16::MAX as usize,
2409                            "samples_per_block too large"
2410                        );
2411                        let mut adpcm_data = [0_u8; ADPCM_DATA_LEN];
2412                        let mut data_index = 0usize;
2413                        while data_index < ADPCM_DATA_LEN {
2414                            adpcm_data[data_index] =
2415                                wav_bytes[parsed_wav.data_chunk_start + data_index];
2416                            data_index += 1;
2417                        }
2418                        $crate::audio_player::__adpcm_clip_from_parts(
2419                            parsed_wav.block_align as u16,
2420                            parsed_wav.samples_per_block as u16,
2421                            adpcm_data,
2422                        )
2423                    } else {
2424                        $crate::audio_player::__pcm_with_adpcm_block_align::<
2425                            SAMPLE_RATE_HZ,
2426                            PCM_SAMPLE_COUNT,
2427                            ADPCM_DATA_LEN,
2428                        >(&pcm_clip(), BLOCK_ALIGN)
2429                    }
2430                }
2431
2432            }
2433        }
2434    };
2435
2436    (
2437        $vis:vis $name:ident {
2438            file: $file:expr $(,)?
2439        }
2440    ) => {
2441        $crate::__adpcm_clip_parse! {
2442            $vis $name {
2443                file: $file,
2444                target_sample_rate_hz: $crate::audio_player::__parse_adpcm_wav_header(include_bytes!($file)).sample_rate_hz,
2445            }
2446        }
2447    };
2448}
2449
2450/// Macro to create an audio clip of a musical tone.
2451///
2452/// Examples:
2453/// - `tone!(440, VOICE_22050_HZ, Duration::from_millis(500))`
2454/// - `tone!(440, AudioPlayer8::SAMPLE_RATE_HZ, Duration::from_millis(500))`
2455///
2456/// The result is an uncompressed (PCM) clip.
2457/// (It does not use compressed because ADPCM sounds poor for pure sine tones.)
2458///
2459/// See the [audio_player module documentation](mod@crate::audio_player) for
2460/// usage examples.
2461#[doc(hidden)]
2462#[macro_export]
2463macro_rules! tone {
2464    ($frequency_hz:expr, $sample_rate_hz:expr, $duration:expr) => {
2465        $crate::audio_player::__tone_pcm_clip_with_duration::<
2466            { $sample_rate_hz },
2467            { $crate::audio_player::__samples_for_duration($duration, $sample_rate_hz) },
2468        >($frequency_hz, $duration)
2469    };
2470}
2471
2472#[doc = "Macro to \"compile in\" an uncompressed (PCM) clip from an external file (includes syntax details)."]
2473#[doc = include_str!("audio_player/pcm_clip_docs.md")]
2474#[doc = include_str!("audio_player/audio_prep_steps_1_2.md")]
2475#[doc = include_str!("audio_player/pcm_clip_step_3.md")]
2476#[doc(inline)]
2477pub use crate::pcm_clip;
2478#[doc(inline)]
2479pub use crate::tone;