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