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 % block_align as usize == 0,
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 % block_align == 0,
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 % block_align == 0,
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 % block_align != 0 {
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#[allow(private_bounds)]
1146pub trait Playable<const SAMPLE_RATE_HZ: u32>: sealed::PlayableSealed<SAMPLE_RATE_HZ> {}
1147
1148impl<const SAMPLE_RATE_HZ: u32, T: ?Sized> Playable<SAMPLE_RATE_HZ> for T where
1149    T: sealed::PlayableSealed<SAMPLE_RATE_HZ>
1150{
1151}
1152
1153/// Platform-agnostic audio player device contract.
1154///
1155/// Platform crates implement this trait for generated audio player types so
1156/// playback operations resolve through trait methods instead of inherent methods.
1157///
1158/// # Example: Play "Mary Had a Little Lamb" (Phrase) Once
1159///
1160/// This example plays the opening phrase (`E D C D E E E`) and then stops.
1161///
1162/// ```rust,no_run
1163/// use device_envoy_core::audio_player::{
1164///     AtEnd, AudioPlayer, Playable, SilenceClip, VOICE_22050_HZ, Volume, tone,
1165/// };
1166/// use core::time::Duration as StdDuration;
1167///
1168/// const SAMPLE_RATE_HZ: u32 = VOICE_22050_HZ;
1169///
1170/// fn play_mary_phrase(audio_player: &impl AudioPlayer<SAMPLE_RATE_HZ>) {
1171///     type PlayableRef = &'static dyn Playable<SAMPLE_RATE_HZ>;
1172///
1173///     const REST: PlayableRef = &SilenceClip::new(StdDuration::from_millis(80));
1174///     const NOTE_DURATION: StdDuration = StdDuration::from_millis(220);
1175///     const NOTE_E4: PlayableRef = &tone!(330, SAMPLE_RATE_HZ, NOTE_DURATION);
1176///     const NOTE_D4: PlayableRef = &tone!(294, SAMPLE_RATE_HZ, NOTE_DURATION);
1177///     const NOTE_C4: PlayableRef = &tone!(262, SAMPLE_RATE_HZ, NOTE_DURATION);
1178///
1179///     audio_player.play(
1180///         [
1181///             NOTE_E4, REST, NOTE_D4, REST, NOTE_C4, REST, NOTE_D4, REST, NOTE_E4, REST,
1182///             NOTE_E4, REST, NOTE_E4,
1183///         ],
1184///         AtEnd::Stop,
1185///     );
1186/// }
1187///
1188/// # struct DemoAudioPlayer;
1189/// # impl AudioPlayer<SAMPLE_RATE_HZ> for DemoAudioPlayer {
1190/// #     const SAMPLE_RATE_HZ: u32 = SAMPLE_RATE_HZ;
1191/// #     const MAX_CLIPS: usize = 16;
1192/// #     const INITIAL_VOLUME: Volume = Volume::MAX;
1193/// #     const MAX_VOLUME: Volume = Volume::MAX;
1194/// #     fn play<I>(&self, _audio_clips: I, _at_end: AtEnd)
1195/// #     where
1196/// #         I: IntoIterator<Item = &'static dyn Playable<SAMPLE_RATE_HZ>>,
1197/// #     {
1198/// #     }
1199/// #     fn stop(&self) {}
1200/// #     async fn wait_until_stopped(&self) {}
1201/// #     fn set_volume(&self, _volume: Volume) {}
1202/// #     fn volume(&self) -> Volume {
1203/// #         Self::INITIAL_VOLUME
1204/// #     }
1205/// # }
1206/// # let audio_player = DemoAudioPlayer;
1207/// # play_mary_phrase(&audio_player);
1208/// ```
1209///
1210/// # Example: Compiling in an External Audio Clip and Runtime Volume Changes
1211///
1212/// This example shows how to compile in an external clip, play it in a loop,
1213/// change volume at runtime, and then stop/reset playback settings.
1214///
1215/// ```rust,no_run,standalone_crate
1216/// use device_envoy_core::audio_player::{
1217///     AtEnd, AudioPlayer, Gain, Playable, SilenceClip, VOICE_22050_HZ, Volume, pcm_clip, tone,
1218/// };
1219/// use device_envoy_core::button::Button;
1220/// use core::time::Duration as StdDuration;
1221/// use embassy_futures::select::{Either, select};
1222/// use embassy_time::{Duration, Timer};
1223///
1224/// pcm_clip! {
1225///     Nasa {
1226///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/nasa_22k.s16"),
1227///         source_sample_rate_hz: VOICE_22050_HZ,
1228///     }
1229/// }
1230///
1231/// async fn play_nasa_with_runtime_volume(
1232///     audio_player: &impl AudioPlayer<VOICE_22050_HZ>,
1233///     button: &mut impl Button,
1234/// ) -> ! {
1235///     type PlayableRef = &'static dyn Playable<VOICE_22050_HZ>;
1236///
1237///     const fn ms(milliseconds: u64) -> StdDuration {
1238///         StdDuration::from_millis(milliseconds)
1239///     }
1240///
1241///     const NASA: PlayableRef = &Nasa::adpcm_clip();
1242///     const GAP: PlayableRef = &SilenceClip::new(ms(80));
1243///     const CHIME: PlayableRef = &tone!(880, VOICE_22050_HZ, ms(100)).with_gain(Gain::percent(20));
1244///     const VOLUME_STEPS_PERCENT: [u8; 7] = [50, 25, 12, 6, 3, 1, 0];
1245///     let initial_volume = audio_player.volume();
1246///
1247///     loop {
1248///         audio_player.play([CHIME, NASA, GAP], AtEnd::Loop);
1249///
1250///         for volume_percent in VOLUME_STEPS_PERCENT {
1251///             match select(button.wait_for_press(), Timer::after(Duration::from_secs(1))).await {
1252///                 Either::First(()) => break,
1253///                 Either::Second(()) => audio_player.set_volume(Volume::percent(volume_percent)),
1254///             }
1255///         }
1256///
1257///         audio_player.stop();
1258///         audio_player.set_volume(initial_volume);
1259///         button.wait_for_press().await;
1260///     }
1261/// }
1262///
1263/// # struct DemoAudioPlayer;
1264/// # impl AudioPlayer<VOICE_22050_HZ> for DemoAudioPlayer {
1265/// #     const SAMPLE_RATE_HZ: u32 = VOICE_22050_HZ;
1266/// #     const MAX_CLIPS: usize = 8;
1267/// #     const INITIAL_VOLUME: Volume = Volume::spinal_tap(5);
1268/// #     const MAX_VOLUME: Volume = Volume::spinal_tap(11);
1269/// #     fn play<I>(&self, _audio_clips: I, _at_end: AtEnd)
1270/// #     where
1271/// #         I: IntoIterator<Item = &'static dyn Playable<VOICE_22050_HZ>>,
1272/// #     {
1273/// #     }
1274/// #     fn stop(&self) {}
1275/// #     async fn wait_until_stopped(&self) {}
1276/// #     fn set_volume(&self, _volume: Volume) {}
1277/// #     fn volume(&self) -> Volume {
1278/// #         Self::INITIAL_VOLUME
1279/// #     }
1280/// # }
1281/// # struct ButtonMock;
1282/// # impl device_envoy_core::button::__ButtonMonitor for ButtonMock {
1283/// #     fn is_pressed_raw(&self) -> bool { false }
1284/// #     async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
1285/// # }
1286/// # impl Button for ButtonMock {}
1287/// fn main() {
1288///     let mut button = ButtonMock;
1289///     let audio_player = DemoAudioPlayer;
1290///     let _future = play_nasa_with_runtime_volume(&audio_player, &mut button);
1291/// }
1292/// ```
1293///
1294/// # Example: Resample and Play Countdown Once
1295///
1296/// This example compiles in `2`, `1`, `0`, and NASA at `22.05 kHz`, resamples
1297/// them to narrowband (`8 kHz`) at compile time, and then plays the sequence.
1298///
1299/// ```rust,no_run,standalone_crate
1300/// use device_envoy_core::audio_player::{
1301///     AtEnd, AudioPlayer, Gain, NARROWBAND_8000_HZ, Playable, VOICE_22050_HZ, Volume, pcm_clip,
1302/// };
1303/// use device_envoy_core::button::Button;
1304///
1305/// pcm_clip! {
1306///     Digit0 {
1307///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/0_22050.s16"),
1308///         source_sample_rate_hz: VOICE_22050_HZ,
1309///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1310///     }
1311/// }
1312///
1313/// pcm_clip! {
1314///     Digit1 {
1315///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/1_22050.s16"),
1316///         source_sample_rate_hz: VOICE_22050_HZ,
1317///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1318///     }
1319/// }
1320///
1321/// pcm_clip! {
1322///     Digit2 {
1323///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/2_22050.s16"),
1324///         source_sample_rate_hz: VOICE_22050_HZ,
1325///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1326///     }
1327/// }
1328///
1329/// pcm_clip! {
1330///     Nasa {
1331///         file: concat!(env!("CARGO_MANIFEST_DIR"), "/examples/data/audio/nasa_22k.s16"),
1332///         source_sample_rate_hz: VOICE_22050_HZ,
1333///         target_sample_rate_hz: NARROWBAND_8000_HZ,
1334///     }
1335/// }
1336///
1337/// fn play_resampled_countdown(audio_player: &impl AudioPlayer<NARROWBAND_8000_HZ>) {
1338///     type PlayableRef = &'static dyn Playable<NARROWBAND_8000_HZ>;
1339///
1340///     const DIGITS: [PlayableRef; 3] =
1341///         [&Digit0::adpcm_clip(), &Digit1::adpcm_clip(), &Digit2::adpcm_clip()];
1342///     const NASA: PlayableRef = &Nasa::pcm_clip()
1343///         .with_gain(Gain::percent(25))
1344///         .with_adpcm::<{ Nasa::ADPCM_DATA_LEN }>();
1345///
1346///     audio_player.play([DIGITS[2], DIGITS[1], DIGITS[0], NASA], AtEnd::Stop);
1347/// }
1348///
1349/// # struct DemoAudioPlayer;
1350/// # impl AudioPlayer<NARROWBAND_8000_HZ> for DemoAudioPlayer {
1351/// #     const SAMPLE_RATE_HZ: u32 = NARROWBAND_8000_HZ;
1352/// #     const MAX_CLIPS: usize = 16;
1353/// #     const INITIAL_VOLUME: Volume = Volume::MAX;
1354/// #     const MAX_VOLUME: Volume = Volume::percent(50);
1355/// #     fn play<I>(&self, _audio_clips: I, _at_end: AtEnd)
1356/// #     where
1357/// #         I: IntoIterator<Item = &'static dyn Playable<NARROWBAND_8000_HZ>>,
1358/// #     {
1359/// #     }
1360/// #     fn stop(&self) {}
1361/// #     async fn wait_until_stopped(&self) {}
1362/// #     fn set_volume(&self, _volume: Volume) {}
1363/// #     fn volume(&self) -> Volume {
1364/// #         Self::INITIAL_VOLUME
1365/// #     }
1366/// # }
1367/// # struct ButtonMock;
1368/// # impl device_envoy_core::button::__ButtonMonitor for ButtonMock {
1369/// #     fn is_pressed_raw(&self) -> bool { false }
1370/// #     async fn wait_until_pressed_state(&mut self, _pressed: bool) {}
1371/// # }
1372/// # impl Button for ButtonMock {}
1373/// fn main() {
1374///     let mut button = ButtonMock;
1375///     let audio_player = DemoAudioPlayer;
1376///     play_resampled_countdown(&audio_player);
1377///     let _future = button.wait_for_press();
1378/// }
1379/// ```
1380#[allow(async_fn_in_trait)]
1381pub trait AudioPlayer<const SAMPLE_RATE_HZ: u32> {
1382    /// Sample rate in hertz for this generated player type.
1383    const SAMPLE_RATE_HZ: u32;
1384    /// Maximum number of clips accepted by `play(...)` for this generated type.
1385    const MAX_CLIPS: usize;
1386    /// Initial runtime volume relative to [`Self::MAX_VOLUME`].
1387    const INITIAL_VOLUME: Volume;
1388    /// Runtime volume ceiling for this generated player type.
1389    const MAX_VOLUME: Volume;
1390
1391    /// Starts playback of one or more static audio clips.
1392    ///
1393    /// Accepts any array-like or iterator input. The maximum number of clips
1394    /// is determined by the generated type configuration.
1395    ///
1396    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1397    fn play<I>(&self, audio_clips: I, at_end: AtEnd)
1398    where
1399        I: IntoIterator<Item = &'static dyn Playable<SAMPLE_RATE_HZ>>;
1400
1401    /// Stops current playback as soon as possible.
1402    ///
1403    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1404    fn stop(&self);
1405
1406    /// Waits until playback is stopped.
1407    ///
1408    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1409    async fn wait_until_stopped(&self);
1410
1411    /// Sets runtime playback volume relative to the generated player's max volume.
1412    ///
1413    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1414    fn set_volume(&self, volume: Volume);
1415
1416    /// Returns the current runtime playback volume relative to max volume.
1417    ///
1418    /// See the [AudioPlayer trait documentation](Self) for usage examples.
1419    fn volume(&self) -> Volume;
1420}
1421
1422mod sealed {
1423    use super::{AdpcmClip, PcmClip, PlaybackClip, SilenceClip};
1424
1425    pub(crate) trait PlayableSealed<const SAMPLE_RATE_HZ: u32> {
1426        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ>;
1427    }
1428
1429    impl<const SAMPLE_RATE_HZ: u32> PlayableSealed<SAMPLE_RATE_HZ> for PcmClip<SAMPLE_RATE_HZ> {
1430        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1431            PlaybackClip::Pcm(self)
1432        }
1433    }
1434
1435    impl<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize> PlayableSealed<SAMPLE_RATE_HZ>
1436        for PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>
1437    {
1438        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1439            PlaybackClip::Pcm(self)
1440        }
1441    }
1442
1443    impl<const SAMPLE_RATE_HZ: u32> PlayableSealed<SAMPLE_RATE_HZ> for AdpcmClip<SAMPLE_RATE_HZ> {
1444        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1445            PlaybackClip::Adpcm(self)
1446        }
1447    }
1448
1449    impl<const SAMPLE_RATE_HZ: u32, const DATA_LEN: usize> PlayableSealed<SAMPLE_RATE_HZ>
1450        for AdpcmClip<SAMPLE_RATE_HZ, [u8; DATA_LEN]>
1451    {
1452        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1453            PlaybackClip::Adpcm(self)
1454        }
1455    }
1456
1457    impl<const SAMPLE_RATE_HZ: u32> PlayableSealed<SAMPLE_RATE_HZ> for SilenceClip {
1458        fn playback_clip(&'static self) -> PlaybackClip<SAMPLE_RATE_HZ> {
1459            PlaybackClip::Silence(self.duration())
1460        }
1461    }
1462}
1463
1464/// Unsized view of static uncompressed (PCM) audio clip data.
1465///
1466/// For fixed-size, const-friendly storage, see [`PcmClipBuf`].
1467///
1468/// See the [audio_player module documentation](mod@crate::audio_player) for
1469/// usage examples.
1470pub struct PcmClip<const SAMPLE_RATE_HZ: u32, T: ?Sized = [i16]> {
1471    samples: T,
1472}
1473
1474impl<const SAMPLE_RATE_HZ: u32, T: ?Sized> PcmClip<SAMPLE_RATE_HZ, T> {
1475    /// Returns a reference to the raw PCM sample data.
1476    #[must_use]
1477    pub fn samples(&self) -> &T {
1478        &self.samples
1479    }
1480}
1481
1482/// Sized, const-friendly storage for uncompressed (PCM) audio clip data.
1483///
1484/// For unsized clip references, see [`PcmClip`].
1485///
1486/// Sample rate is part of the type, so clips with different sample rates are
1487/// not assignment-compatible.
1488///
1489/// See the [audio_player module documentation](mod@crate::audio_player) for
1490/// usage examples.
1491pub type PcmClipBuf<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize> =
1492    PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>;
1493
1494/// **Implementation for fixed-size clips (`PcmClipBuf`).**
1495///
1496/// This impl applies to [`PcmClip`] with array-backed storage:
1497/// `PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>`
1498/// (which is what [`PcmClipBuf`] aliases).
1499impl<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>
1500    PcmClip<SAMPLE_RATE_HZ, [i16; SAMPLE_COUNT]>
1501{
1502    /// Returns a new clip with linear sample gain applied.
1503    ///
1504    /// This is intended to be used in const clip definitions so the adjusted
1505    /// samples are computed ahead of time.
1506    ///
1507    /// Gain multiplication uses i32 math and saturates to i16 sample bounds.
1508    /// Large boosts can hard-clip peaks and introduce distortion.
1509    ///
1510    /// See the [audio_player module documentation](mod@crate::audio_player) for
1511    /// usage examples.
1512    #[must_use]
1513    pub const fn with_gain(self, gain: Gain) -> Self {
1514        assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1515        let mut scaled_samples = [0_i16; SAMPLE_COUNT];
1516        let mut sample_index = 0_usize;
1517        // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1518        while sample_index < SAMPLE_COUNT {
1519            scaled_samples[sample_index] =
1520                scale_sample_with_linear(self.samples[sample_index], gain.linear());
1521            sample_index += 1;
1522        }
1523        Self {
1524            samples: scaled_samples,
1525        }
1526    }
1527
1528    /// Returns this clip with a linear attack and release envelope.
1529    ///
1530    /// This is useful when you want explicit control over click reduction at
1531    /// the start/end of generated tones.
1532    ///
1533    /// See the [audio_player module documentation](mod@crate::audio_player) for
1534    /// usage examples.
1535    #[must_use]
1536    pub(crate) const fn with_attack_release(self, attack: Duration, release: Duration) -> Self {
1537        assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1538        let attack_sample_count = __samples_for_duration(attack, SAMPLE_RATE_HZ);
1539        let release_sample_count = __samples_for_duration(release, SAMPLE_RATE_HZ);
1540        self.with_attack_release_sample_count(attack_sample_count, release_sample_count)
1541    }
1542
1543    #[must_use]
1544    const fn with_attack_release_sample_count(
1545        self,
1546        attack_sample_count: usize,
1547        release_sample_count: usize,
1548    ) -> Self {
1549        assert!(
1550            attack_sample_count <= SAMPLE_COUNT,
1551            "attack duration must fit within clip duration"
1552        );
1553        assert!(
1554            release_sample_count <= SAMPLE_COUNT,
1555            "release duration must fit within clip duration"
1556        );
1557        assert!(
1558            attack_sample_count + release_sample_count <= SAMPLE_COUNT,
1559            "attack + release must fit within clip duration"
1560        );
1561
1562        let mut shaped_samples = self.samples;
1563
1564        if attack_sample_count > 0 {
1565            let attack_sample_count_i32 = attack_sample_count as i32;
1566            let mut sample_index = 0usize;
1567            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1568            while sample_index < attack_sample_count {
1569                let envelope_numerator_i32 = sample_index as i32;
1570                shaped_samples[sample_index] = scale_sample_with_linear(
1571                    shaped_samples[sample_index],
1572                    (envelope_numerator_i32 * i16::MAX as i32) / attack_sample_count_i32,
1573                );
1574                sample_index += 1;
1575            }
1576        }
1577
1578        if release_sample_count > 0 {
1579            let release_sample_count_i32 = release_sample_count as i32;
1580            let release_start_index = SAMPLE_COUNT - release_sample_count;
1581            let mut release_index = 0usize;
1582            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1583            while release_index < release_sample_count {
1584                let sample_index = release_start_index + release_index;
1585                let envelope_numerator_i32 = (release_sample_count - release_index) as i32;
1586                shaped_samples[sample_index] = scale_sample_with_linear(
1587                    shaped_samples[sample_index],
1588                    (envelope_numerator_i32 * i16::MAX as i32) / release_sample_count_i32,
1589                );
1590                release_index += 1;
1591            }
1592        }
1593
1594        Self {
1595            samples: shaped_samples,
1596        }
1597    }
1598
1599    /// Returns the compressed (ADPCM) encoding for this clip.
1600    ///
1601    /// See the [audio_player module documentation](mod@crate::audio_player) for
1602    /// usage examples.
1603    #[must_use]
1604    pub const fn with_adpcm<const DATA_LEN: usize>(
1605        &self,
1606    ) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1607        self.with_adpcm_block_align::<DATA_LEN>(ADPCM_ENCODE_BLOCK_ALIGN)
1608    }
1609
1610    #[must_use]
1611    pub(crate) const fn with_adpcm_block_align<const DATA_LEN: usize>(
1612        &self,
1613        block_align: usize,
1614    ) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1615        assert!(block_align >= 5, "block_align must be >= 5");
1616        assert!(
1617            block_align <= u16::MAX as usize,
1618            "block_align must fit in u16"
1619        );
1620        let samples_per_block = __adpcm_samples_per_block(block_align);
1621        assert!(
1622            samples_per_block <= u16::MAX as usize,
1623            "samples_per_block must fit in u16"
1624        );
1625        assert!(
1626            DATA_LEN
1627                == __adpcm_data_len_for_pcm_samples_with_block_align(SAMPLE_COUNT, block_align),
1628            "adpcm data length must match sample count and block_align"
1629        );
1630        if SAMPLE_COUNT == 0 {
1631            return AdpcmClip::new(
1632                block_align as u16,
1633                samples_per_block as u16,
1634                SAMPLE_COUNT,
1635                [0; DATA_LEN],
1636            );
1637        }
1638
1639        let mut adpcm_data = [0_u8; DATA_LEN];
1640        let mut sample_index = 0usize;
1641        let mut data_index = 0usize;
1642        let payload_len_per_block = block_align - 4;
1643
1644        // TODO_NIGHTLY When nightly feature const_for becomes stable, replace these while loops with for loops.
1645        while sample_index < SAMPLE_COUNT {
1646            let mut predictor_i32 = self.samples[sample_index] as i32;
1647            let mut step_index_i32 = 0_i32;
1648
1649            let predictor_i16 = predictor_i32 as i16;
1650            let predictor_bytes = predictor_i16.to_le_bytes();
1651            adpcm_data[data_index] = predictor_bytes[0];
1652            adpcm_data[data_index + 1] = predictor_bytes[1];
1653            adpcm_data[data_index + 2] = step_index_i32 as u8;
1654            adpcm_data[data_index + 3] = 0;
1655            data_index += 4;
1656            sample_index += 1;
1657
1658            let mut payload_byte_index = 0usize;
1659            // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1660            while payload_byte_index < payload_len_per_block {
1661                let mut adpcm_byte = 0_u8;
1662
1663                let mut nibble_index = 0usize;
1664                // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1665                while nibble_index < 2 {
1666                    let target_sample_i32 = if sample_index < SAMPLE_COUNT {
1667                        self.samples[sample_index] as i32
1668                    } else {
1669                        predictor_i32
1670                    };
1671                    let adpcm_nibble = encode_adpcm_nibble(
1672                        target_sample_i32,
1673                        &mut predictor_i32,
1674                        &mut step_index_i32,
1675                    );
1676                    adpcm_byte |= adpcm_nibble << (nibble_index * 4);
1677                    sample_index += 1;
1678                    nibble_index += 1;
1679                }
1680
1681                adpcm_data[data_index] = adpcm_byte;
1682                data_index += 1;
1683                payload_byte_index += 1;
1684            }
1685        }
1686
1687        AdpcmClip::new(
1688            block_align as u16,
1689            samples_per_block as u16,
1690            SAMPLE_COUNT,
1691            adpcm_data,
1692        )
1693    }
1694}
1695
1696// Must be `pub` so platform-crate `device_loop` and play functions can access
1697// these across the crate boundary.
1698#[doc(hidden)]
1699pub enum AudioCommand<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32> {
1700    Play {
1701        audio_clips: Vec<PlaybackClip<SAMPLE_RATE_HZ>, MAX_CLIPS>,
1702        at_end: AtEnd,
1703    },
1704    Stop,
1705}
1706
1707/// Static resources for audio player runtime handles.
1708// Must be `pub` so `audio_player!` expansions in downstream crates can reference this type.
1709#[doc(hidden)]
1710pub struct AudioPlayerStatic<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32> {
1711    command_signal: Signal<CriticalSectionRawMutex, AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>>,
1712    stopped_signal: Signal<CriticalSectionRawMutex, ()>,
1713    is_playing: AtomicBool,
1714    has_pending_play: AtomicBool,
1715    max_volume_linear: i32,
1716    runtime_volume_relative_linear: AtomicI32,
1717}
1718
1719impl<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>
1720    AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>
1721{
1722    /// Creates static resources for a player.
1723    #[must_use]
1724    pub const fn new_static() -> Self {
1725        Self::new_static_with_max_volume_and_initial_volume(Volume::MAX, Volume::MAX)
1726    }
1727
1728    /// Creates static resources for a player with a runtime volume ceiling.
1729    #[must_use]
1730    pub const fn new_static_with_max_volume(max_volume: Volume) -> Self {
1731        Self::new_static_with_max_volume_and_initial_volume(max_volume, Volume::MAX)
1732    }
1733
1734    /// Creates static resources for a player with a runtime volume ceiling
1735    /// and an initial runtime volume relative to that ceiling.
1736    #[must_use]
1737    pub const fn new_static_with_max_volume_and_initial_volume(
1738        max_volume: Volume,
1739        initial_volume: Volume,
1740    ) -> Self {
1741        Self {
1742            command_signal: Signal::new(),
1743            stopped_signal: Signal::new(),
1744            is_playing: AtomicBool::new(false),
1745            has_pending_play: AtomicBool::new(false),
1746            max_volume_linear: max_volume.to_i16() as i32,
1747            runtime_volume_relative_linear: AtomicI32::new(initial_volume.to_i16() as i32),
1748        }
1749    }
1750
1751    fn signal(&self, audio_command: AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>) {
1752        self.command_signal.signal(audio_command);
1753    }
1754
1755    fn mark_pending_play(&self) {
1756        self.has_pending_play.store(true, AtomicOrdering::Relaxed);
1757    }
1758
1759    /// Marks the player as currently playing. Called by platform-crate `device_loop`.
1760    #[doc(hidden)]
1761    pub fn mark_playing(&self) {
1762        self.has_pending_play.store(false, AtomicOrdering::Relaxed);
1763        self.is_playing.store(true, AtomicOrdering::Relaxed);
1764    }
1765
1766    /// Marks the player as stopped. Called by platform-crate `device_loop`.
1767    #[doc(hidden)]
1768    pub fn mark_stopped(&self) {
1769        self.has_pending_play.store(false, AtomicOrdering::Relaxed);
1770        self.is_playing.store(false, AtomicOrdering::Relaxed);
1771        self.stopped_signal.signal(());
1772    }
1773
1774    fn is_idle(&self) -> bool {
1775        !self.has_pending_play.load(AtomicOrdering::Relaxed)
1776            && !self.is_playing.load(AtomicOrdering::Relaxed)
1777    }
1778
1779    async fn wait_until_stopped(&self) {
1780        while !self.is_idle() {
1781            self.stopped_signal.wait().await;
1782        }
1783    }
1784
1785    fn set_runtime_volume(&self, volume: Volume) {
1786        self.runtime_volume_relative_linear
1787            .store(volume.to_i16() as i32, Ordering::Relaxed);
1788    }
1789
1790    fn runtime_volume(&self) -> Volume {
1791        Volume::from_i16(self.runtime_volume_relative_linear.load(Ordering::Relaxed) as i16)
1792    }
1793
1794    /// Returns the effective runtime volume after applying max-volume scaling.
1795    /// Called by platform-crate play functions.
1796    #[doc(hidden)]
1797    pub fn effective_runtime_volume(&self) -> Volume {
1798        let runtime_volume_relative = self.runtime_volume();
1799        Volume::from_i16(scale_linear(self.max_volume_linear, runtime_volume_relative) as i16)
1800    }
1801
1802    /// Awaits the next audio command. Used by RP-style `device_loop`.
1803    #[doc(hidden)]
1804    pub async fn wait(&self) -> AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ> {
1805        self.command_signal.wait().await
1806    }
1807
1808    /// Tries to take a pending audio command without waiting. Used by
1809    /// ESP-style play functions and both platforms' `device_loop`.
1810    #[doc(hidden)]
1811    pub fn try_take_command(&self) -> Option<AudioCommand<MAX_CLIPS, SAMPLE_RATE_HZ>> {
1812        self.command_signal.try_take()
1813    }
1814}
1815
1816// Must be `pub` so platform runtime handles can call this from another crate.
1817#[doc(hidden)]
1818pub fn __audio_player_play<I, const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1819    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1820    audio_clips: I,
1821    at_end: AtEnd,
1822) where
1823    I: IntoIterator<Item = &'static dyn Playable<SAMPLE_RATE_HZ>>,
1824{
1825    assert!(MAX_CLIPS > 0, "play disabled: max_clips is 0");
1826    let mut audio_clip_sequence: Vec<PlaybackClip<SAMPLE_RATE_HZ>, MAX_CLIPS> = Vec::new();
1827    for audio_clip in audio_clips {
1828        assert!(
1829            audio_clip_sequence
1830                .push(sealed::PlayableSealed::playback_clip(audio_clip))
1831                .is_ok(),
1832            "play sequence fits within max_clips"
1833        );
1834    }
1835    assert!(
1836        !audio_clip_sequence.is_empty(),
1837        "play requires at least one clip"
1838    );
1839
1840    audio_player_static.mark_pending_play();
1841    audio_player_static.signal(AudioCommand::Play {
1842        audio_clips: audio_clip_sequence,
1843        at_end,
1844    });
1845}
1846
1847// Must be `pub` so platform runtime handles can call this from another crate.
1848#[doc(hidden)]
1849pub fn __audio_player_stop<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1850    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1851) {
1852    audio_player_static.signal(AudioCommand::Stop);
1853}
1854
1855// Must be `pub` so platform runtime handles can call this from another crate.
1856#[doc(hidden)]
1857pub async fn __audio_player_wait_until_stopped<
1858    const MAX_CLIPS: usize,
1859    const SAMPLE_RATE_HZ: u32,
1860>(
1861    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1862) {
1863    audio_player_static.wait_until_stopped().await;
1864}
1865
1866// Must be `pub` so platform runtime handles can call this from another crate.
1867#[doc(hidden)]
1868pub fn __audio_player_set_volume<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1869    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1870    volume: Volume,
1871) {
1872    audio_player_static.set_runtime_volume(volume);
1873}
1874
1875// Must be `pub` so platform runtime handles can call this from another crate.
1876#[doc(hidden)]
1877#[must_use]
1878pub fn __audio_player_volume<const MAX_CLIPS: usize, const SAMPLE_RATE_HZ: u32>(
1879    audio_player_static: &'static AudioPlayerStatic<MAX_CLIPS, SAMPLE_RATE_HZ>,
1880) -> Volume {
1881    audio_player_static.runtime_volume()
1882}
1883
1884/// Const backend helper that creates a PCM sine-wave clip.
1885///
1886/// This is intentionally `#[doc(hidden)]` because user-facing construction
1887/// should prefer [`tone!`](macro@crate::tone).
1888#[must_use]
1889#[doc(hidden)]
1890pub const fn __tone_pcm_clip<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>(
1891    frequency_hz: u32,
1892) -> PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT> {
1893    __tone_pcm_clip_with_duration::<SAMPLE_RATE_HZ, SAMPLE_COUNT>(
1894        frequency_hz,
1895        duration_for_sample_count(SAMPLE_COUNT, SAMPLE_RATE_HZ),
1896    )
1897}
1898
1899/// Const backend helper that creates a PCM sine-wave clip with explicit
1900/// duration metadata used for built-in shaping.
1901/// This uses [`core::time::Duration`] for tone duration.
1902#[must_use]
1903#[doc(hidden)]
1904pub const fn __tone_pcm_clip_with_duration<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>(
1905    frequency_hz: u32,
1906    duration: core::time::Duration,
1907) -> PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT> {
1908    assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1909    let mut samples = [0_i16; SAMPLE_COUNT];
1910    let phase_step_u64 = ((frequency_hz as u64) << 32) / SAMPLE_RATE_HZ as u64;
1911    let phase_step_u32 = phase_step_u64 as u32;
1912    let mut phase_u32 = 0_u32;
1913
1914    let mut sample_index = 0usize;
1915    // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
1916    while sample_index < SAMPLE_COUNT {
1917        samples[sample_index] = sine_sample_from_phase(phase_u32);
1918        phase_u32 = phase_u32.wrapping_add(phase_step_u32);
1919        sample_index += 1;
1920    }
1921
1922    // Add an attack and release duration to reduce clicks. It is min(50ms, 1/4 of total duration)
1923    let max_duration = Duration::from_millis(50);
1924    assert!(max_duration.as_secs() == 0, "50ms cap must be sub-second");
1925    let attack_release_duration = match (duration.as_secs(), duration.subsec_nanos()) {
1926        (0, nanos) if nanos / 4 < max_duration.subsec_nanos() => Duration::new(0, nanos / 4),
1927        (_, _) => max_duration,
1928    };
1929    PcmClip { samples }.with_attack_release(attack_release_duration, attack_release_duration)
1930}
1931
1932/// Builds a fixed-size PCM clip from samples.
1933///
1934/// This is intentionally `#[doc(hidden)]` because user-facing clip
1935/// construction should prefer `pcm_clip!`, `adpcm_clip!`, and `tone!`.
1936#[must_use]
1937#[doc(hidden)]
1938pub const fn __pcm_clip_from_samples<const SAMPLE_RATE_HZ: u32, const SAMPLE_COUNT: usize>(
1939    samples: [i16; SAMPLE_COUNT],
1940) -> PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT> {
1941    assert!(SAMPLE_RATE_HZ > 0, "sample_rate_hz must be > 0");
1942    PcmClip { samples }
1943}
1944
1945/// Const backend helper that builds a fixed-size ADPCM clip from parts.
1946///
1947/// This is intentionally `#[doc(hidden)]` because user-facing clip
1948/// construction should prefer `adpcm_clip!` and conversion helpers.
1949#[must_use]
1950#[doc(hidden)]
1951pub const fn __adpcm_clip_from_parts<const SAMPLE_RATE_HZ: u32, const DATA_LEN: usize>(
1952    block_align: u16,
1953    samples_per_block: u16,
1954    pcm_sample_count: usize,
1955    data: [u8; DATA_LEN],
1956) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1957    AdpcmClip::new(block_align, samples_per_block, pcm_sample_count, data)
1958}
1959
1960/// Const backend helper that encodes PCM into ADPCM with an explicit block size.
1961///
1962/// This helper must be `pub` because macro expansions in downstream crates call
1963/// it at the call site, but it is not a user-facing API.
1964#[must_use]
1965#[doc(hidden)]
1966pub const fn __pcm_with_adpcm_block_align<
1967    const SAMPLE_RATE_HZ: u32,
1968    const SAMPLE_COUNT: usize,
1969    const DATA_LEN: usize,
1970>(
1971    source_pcm_clip: &PcmClipBuf<SAMPLE_RATE_HZ, SAMPLE_COUNT>,
1972    block_align: usize,
1973) -> AdpcmClipBuf<SAMPLE_RATE_HZ, DATA_LEN> {
1974    source_pcm_clip.with_adpcm_block_align::<DATA_LEN>(block_align)
1975}
1976
1977/// Const backend helper that resamples a PCM clip to a destination timeline.
1978///
1979/// This is intentionally `#[doc(hidden)]` because resampling is configured by
1980/// `pcm_clip!`/`adpcm_clip!` inputs (`target_sample_rate_hz`) rather than by a
1981/// direct clip method.
1982#[must_use]
1983#[doc(hidden)]
1984pub const fn __resample_pcm_clip<
1985    const SOURCE_HZ: u32,
1986    const SOURCE_COUNT: usize,
1987    const TARGET_HZ: u32,
1988    const TARGET_COUNT: usize,
1989>(
1990    source_pcm_clip: PcmClipBuf<SOURCE_HZ, SOURCE_COUNT>,
1991) -> PcmClipBuf<TARGET_HZ, TARGET_COUNT> {
1992    assert!(SOURCE_COUNT > 0, "source sample count must be > 0");
1993    assert!(TARGET_HZ > 0, "destination sample_rate_hz must be > 0");
1994    let expected_destination_sample_count =
1995        __resampled_sample_count(SOURCE_COUNT, SOURCE_HZ, TARGET_HZ);
1996    assert!(
1997        TARGET_COUNT == expected_destination_sample_count,
1998        "destination sample count must preserve duration"
1999    );
2000
2001    let source_samples = source_pcm_clip.samples;
2002    let mut resampled_samples = [0_i16; TARGET_COUNT];
2003    let mut sample_index = 0_usize;
2004
2005    // TODO_NIGHTLY When nightly feature const_for becomes stable, replace this while loop with a for loop.
2006    while sample_index < TARGET_COUNT {
2007        let source_position_numerator_u128 = sample_index as u128 * SOURCE_HZ as u128;
2008        let source_index_u128 = source_position_numerator_u128 / TARGET_HZ as u128;
2009        let source_fraction_numerator_u128 = source_position_numerator_u128 % TARGET_HZ as u128;
2010        let source_index = source_index_u128 as usize;
2011
2012        resampled_samples[sample_index] = if source_index + 1 >= SOURCE_COUNT {
2013            source_samples[SOURCE_COUNT - 1]
2014        } else if source_fraction_numerator_u128 == 0 {
2015            source_samples[source_index]
2016        } else {
2017            let left_sample_i128 = source_samples[source_index] as i128;
2018            let right_sample_i128 = source_samples[source_index + 1] as i128;
2019            let sample_delta_i128 = right_sample_i128 - left_sample_i128;
2020            let denom_i128 = TARGET_HZ as i128;
2021            let numerator_i128 = sample_delta_i128 * source_fraction_numerator_u128 as i128;
2022            let rounded_i128 = if numerator_i128 >= 0 {
2023                (numerator_i128 + (denom_i128 / 2)) / denom_i128
2024            } else {
2025                (numerator_i128 - (denom_i128 / 2)) / denom_i128
2026            };
2027            clamp_i64_to_i16((left_sample_i128 + rounded_i128) as i64)
2028        };
2029
2030        sample_index += 1;
2031    }
2032
2033    PcmClip {
2034        samples: resampled_samples,
2035    }
2036}
2037
2038// Must be `pub` because platform-crate `device_loop` and play functions call
2039// this directly.
2040#[doc(hidden)]
2041pub const fn decode_adpcm_nibble_const(
2042    adpcm_nibble: u8,
2043    predictor_i32: &mut i32,
2044    step_index_i32: &mut i32,
2045) -> i16 {
2046    let step = ADPCM_STEP_TABLE[*step_index_i32 as usize];
2047    let mut delta = step >> 3;
2048
2049    if (adpcm_nibble & 0x01) != 0 {
2050        delta += step >> 2;
2051    }
2052    if (adpcm_nibble & 0x02) != 0 {
2053        delta += step >> 1;
2054    }
2055    if (adpcm_nibble & 0x04) != 0 {
2056        delta += step;
2057    }
2058
2059    if (adpcm_nibble & 0x08) != 0 {
2060        *predictor_i32 -= delta;
2061    } else {
2062        *predictor_i32 += delta;
2063    }
2064
2065    if *predictor_i32 < i16::MIN as i32 {
2066        *predictor_i32 = i16::MIN as i32;
2067    } else if *predictor_i32 > i16::MAX as i32 {
2068        *predictor_i32 = i16::MAX as i32;
2069    }
2070    *step_index_i32 += ADPCM_INDEX_TABLE[adpcm_nibble as usize];
2071    if *step_index_i32 < 0 {
2072        *step_index_i32 = 0;
2073    } else if *step_index_i32 > 88 {
2074        *step_index_i32 = 88;
2075    }
2076
2077    *predictor_i32 as i16
2078}
2079
2080const fn encode_adpcm_nibble(
2081    target_sample_i32: i32,
2082    predictor_i32: &mut i32,
2083    step_index_i32: &mut i32,
2084) -> u8 {
2085    let step = ADPCM_STEP_TABLE[*step_index_i32 as usize];
2086    let mut diff = target_sample_i32 - *predictor_i32;
2087    let mut adpcm_nibble = 0_u8;
2088    if diff < 0 {
2089        adpcm_nibble |= 0x08;
2090        diff = -diff;
2091    }
2092
2093    let mut delta = step >> 3;
2094    if diff >= step {
2095        adpcm_nibble |= 0x04;
2096        diff -= step;
2097        delta += step;
2098    }
2099    if diff >= (step >> 1) {
2100        adpcm_nibble |= 0x02;
2101        diff -= step >> 1;
2102        delta += step >> 1;
2103    }
2104    if diff >= (step >> 2) {
2105        adpcm_nibble |= 0x01;
2106        delta += step >> 2;
2107    }
2108
2109    if (adpcm_nibble & 0x08) != 0 {
2110        *predictor_i32 -= delta;
2111    } else {
2112        *predictor_i32 += delta;
2113    }
2114
2115    if *predictor_i32 < i16::MIN as i32 {
2116        *predictor_i32 = i16::MIN as i32;
2117    } else if *predictor_i32 > i16::MAX as i32 {
2118        *predictor_i32 = i16::MAX as i32;
2119    }
2120    *step_index_i32 += ADPCM_INDEX_TABLE[adpcm_nibble as usize];
2121    if *step_index_i32 < 0 {
2122        *step_index_i32 = 0;
2123    } else if *step_index_i32 > 88 {
2124        *step_index_i32 = 88;
2125    }
2126
2127    adpcm_nibble
2128}
2129
2130// ============================================================================
2131// Macros
2132// ============================================================================
2133
2134#[doc(hidden)]
2135#[macro_export]
2136macro_rules! pcm_clip {
2137    // TODO_NIGHTLY When nightly feature `decl_macro` becomes stable, change this
2138    // code by replacing `#[macro_export] macro_rules!` with module-scoped `pub macro`
2139    // so macro visibility and helper exposure can be controlled more precisely.
2140    ($($tt:tt)*) => { $crate::__audio_clip_parse! { $($tt)* } };
2141}
2142
2143#[doc(hidden)]
2144#[macro_export]
2145macro_rules! __audio_clip_parse {
2146    (
2147        $vis:vis $name:ident {
2148            file: $file:expr,
2149            sample_rate_hz: $source_sample_rate_hz:expr,
2150            target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2151        }
2152    ) => {
2153        $crate::__audio_clip_dispatch! {
2154            vis: $vis,
2155            name: $name,
2156            file: $file,
2157            source_sample_rate_hz: $source_sample_rate_hz,
2158            target_sample_rate_hz: $target_sample_rate_hz,
2159        }
2160    };
2161    (
2162        $vis:vis $name:ident {
2163            file: $file:expr,
2164            sample_rate_hz: $source_sample_rate_hz:expr,
2165            target_sample_rate_hz: $target_sample_rate_hz:expr,
2166            $(,)?
2167        }
2168    ) => {
2169        $crate::__audio_clip_dispatch! {
2170            vis: $vis,
2171            name: $name,
2172            file: $file,
2173            source_sample_rate_hz: $source_sample_rate_hz,
2174            target_sample_rate_hz: $target_sample_rate_hz,
2175        }
2176    };
2177    (
2178        $vis:vis $name:ident {
2179            file: $file:expr,
2180            sample_rate_hz: $sample_rate_hz:expr $(,)?
2181        }
2182    ) => {
2183        $crate::__audio_clip_dispatch! {
2184            vis: $vis,
2185            name: $name,
2186            file: $file,
2187            source_sample_rate_hz: $sample_rate_hz,
2188            target_sample_rate_hz: $sample_rate_hz,
2189        }
2190    };
2191    (
2192        $vis:vis $name:ident {
2193            file: $file:expr,
2194            sample_rate_hz: $sample_rate_hz:expr,
2195            $(,)?
2196        }
2197    ) => {
2198        $crate::__audio_clip_dispatch! {
2199            vis: $vis,
2200            name: $name,
2201            file: $file,
2202            source_sample_rate_hz: $sample_rate_hz,
2203            target_sample_rate_hz: $sample_rate_hz,
2204        }
2205    };
2206    (
2207        $vis:vis $name:ident {
2208            file: $file:expr,
2209            sample_rate_hz: $sample_rate_hz:expr,
2210            $(,)?
2211        }
2212    ) => {
2213        $crate::__audio_clip_dispatch! {
2214            vis: $vis,
2215            name: $name,
2216            file: $file,
2217            source_sample_rate_hz: $sample_rate_hz,
2218            target_sample_rate_hz: $sample_rate_hz,
2219        }
2220    };
2221    // Alias: `source_sample_rate_hz:` is accepted as a synonym for `sample_rate_hz:`.
2222    (
2223        $vis:vis $name:ident {
2224            file: $file:expr,
2225            source_sample_rate_hz: $source_sample_rate_hz:expr,
2226            target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2227        }
2228    ) => {
2229        $crate::__audio_clip_dispatch! {
2230            vis: $vis,
2231            name: $name,
2232            file: $file,
2233            source_sample_rate_hz: $source_sample_rate_hz,
2234            target_sample_rate_hz: $target_sample_rate_hz,
2235        }
2236    };
2237    (
2238        $vis:vis $name:ident {
2239            file: $file:expr,
2240            source_sample_rate_hz: $sample_rate_hz:expr $(,)?
2241        }
2242    ) => {
2243        $crate::__audio_clip_dispatch! {
2244            vis: $vis,
2245            name: $name,
2246            file: $file,
2247            source_sample_rate_hz: $sample_rate_hz,
2248            target_sample_rate_hz: $sample_rate_hz,
2249        }
2250    };
2251}
2252
2253#[doc(hidden)]
2254#[macro_export]
2255macro_rules! __audio_clip_dispatch {
2256    (
2257        vis: $vis:vis,
2258        name: $name:ident,
2259        file: $file:expr,
2260        source_sample_rate_hz: $source_sample_rate_hz:expr,
2261        target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2262    ) => {
2263        $crate::__audio_clip_impl! {
2264            vis: $vis,
2265            name: $name,
2266            file: $file,
2267            source_sample_rate_hz: $source_sample_rate_hz,
2268            target_sample_rate_hz: $target_sample_rate_hz,
2269        }
2270    };
2271}
2272
2273#[doc(hidden)]
2274#[macro_export]
2275macro_rules! __audio_clip_impl {
2276    (
2277        vis: $vis:vis,
2278        name: $name:ident,
2279        file: $file:expr,
2280        source_sample_rate_hz: $source_sample_rate_hz:expr,
2281        target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2282    ) => {
2283        $crate::__paste! {
2284            const [<$name:upper _SOURCE_SAMPLE_RATE_HZ>]: u32 = $source_sample_rate_hz;
2285            const [<$name:upper _TARGET_SAMPLE_RATE_HZ>]: u32 = $target_sample_rate_hz;
2286
2287            #[allow(non_snake_case)]
2288            #[doc = concat!(
2289                "Audio clip module generated by [`pcm_clip!`](macro@crate::audio_player::pcm_clip).\n\n",
2290                "[`SAMPLE_RATE_HZ`](Self::SAMPLE_RATE_HZ), ",
2291                "[`PCM_SAMPLE_COUNT`](Self::PCM_SAMPLE_COUNT), ",
2292                "[`ADPCM_DATA_LEN`](Self::ADPCM_DATA_LEN), ",
2293                "[`pcm_clip`](Self::pcm_clip), ",
2294                "and [`adpcm_clip`](Self::adpcm_clip)."
2295            )]
2296            $vis mod $name {
2297                // TODO_NIGHTLY When nightly feature inherent_associated_types becomes stable,
2298                // change generated clip items from a module to inherent associated items on a struct.
2299                const SOURCE_SAMPLE_RATE_HZ: u32 = super::[<$name:upper _SOURCE_SAMPLE_RATE_HZ>];
2300                const TARGET_SAMPLE_RATE_HZ: u32 = super::[<$name:upper _TARGET_SAMPLE_RATE_HZ>];
2301                #[doc = "Sample rate in hertz for this generated clip output."]
2302                pub const SAMPLE_RATE_HZ: u32 = TARGET_SAMPLE_RATE_HZ;
2303                const AUDIO_SAMPLE_BYTES_LEN: usize = include_bytes!($file).len();
2304                const SOURCE_SAMPLE_COUNT: usize = AUDIO_SAMPLE_BYTES_LEN / 2;
2305                #[doc = "Number of samples for uncompressed (PCM) version of this clip."]
2306                pub const PCM_SAMPLE_COUNT: usize = $crate::audio_player::__resampled_sample_count(
2307                    SOURCE_SAMPLE_COUNT,
2308                    SOURCE_SAMPLE_RATE_HZ,
2309                    TARGET_SAMPLE_RATE_HZ,
2310                );
2311                #[doc = "Byte length for compressed (ADPCM) encoding this clip."]
2312                pub const ADPCM_DATA_LEN: usize =
2313                    $crate::audio_player::__adpcm_data_len_for_pcm_samples(PCM_SAMPLE_COUNT);
2314
2315                #[allow(dead_code)]
2316                type SourcePcmClip = $crate::audio_player::PcmClipBuf<
2317                    { SOURCE_SAMPLE_RATE_HZ },
2318                    { SOURCE_SAMPLE_COUNT },
2319                >;
2320
2321                #[doc = "`const` function that returns the uncompressed (PCM) version of this clip."]
2322                #[must_use]
2323                pub const fn pcm_clip() -> $crate::audio_player::PcmClipBuf<
2324                    { SAMPLE_RATE_HZ },
2325                    { PCM_SAMPLE_COUNT },
2326                > {
2327                    assert!(
2328                        AUDIO_SAMPLE_BYTES_LEN % 2 == 0,
2329                        "audio byte length must be even for s16le"
2330                    );
2331
2332                    let audio_sample_s16le: &[u8; AUDIO_SAMPLE_BYTES_LEN] = include_bytes!($file);
2333                    let mut samples = [0_i16; SOURCE_SAMPLE_COUNT];
2334                    let mut sample_index = 0_usize;
2335                    while sample_index < SOURCE_SAMPLE_COUNT {
2336                        let byte_index = sample_index * 2;
2337                        samples[sample_index] = i16::from_le_bytes([
2338                            audio_sample_s16le[byte_index],
2339                            audio_sample_s16le[byte_index + 1],
2340                        ]);
2341                        sample_index += 1;
2342                    }
2343                    $crate::audio_player::__resample_pcm_clip::<
2344                        SOURCE_SAMPLE_RATE_HZ,
2345                        SOURCE_SAMPLE_COUNT,
2346                        TARGET_SAMPLE_RATE_HZ,
2347                        PCM_SAMPLE_COUNT,
2348                    >($crate::audio_player::__pcm_clip_from_samples::<
2349                        SOURCE_SAMPLE_RATE_HZ,
2350                        SOURCE_SAMPLE_COUNT,
2351                    >(samples))
2352                }
2353
2354                #[doc = "`const` function that returns the compressed (ADPCM) encoding for this clip."]
2355                #[must_use]
2356                pub const fn adpcm_clip() -> $crate::audio_player::AdpcmClipBuf<
2357                    { SAMPLE_RATE_HZ },
2358                    { ADPCM_DATA_LEN },
2359                > {
2360                    pcm_clip().with_adpcm::<ADPCM_DATA_LEN>()
2361                }
2362
2363            }
2364        }
2365    };
2366}
2367
2368#[doc = "Macro to \"compile in\" a compressed (ADPCM) WAV clip from an external file (includes syntax details)."]
2369#[doc = include_str!("audio_player/adpcm_clip_docs.md")]
2370#[doc = include_str!("audio_player/audio_prep_steps_1_2.md")]
2371#[doc = include_str!("audio_player/adpcm_clip_step_3.md")]
2372#[doc(inline)]
2373pub use crate::adpcm_clip;
2374
2375#[doc(hidden)]
2376#[macro_export]
2377macro_rules! adpcm_clip {
2378    ($($tt:tt)*) => { $crate::__adpcm_clip_parse! { $($tt)* } };
2379}
2380
2381#[doc(hidden)]
2382#[macro_export]
2383macro_rules! __adpcm_clip_parse {
2384    (
2385        $vis:vis $name:ident {
2386            file: $file:expr,
2387            target_sample_rate_hz: $target_sample_rate_hz:expr $(,)?
2388        }
2389    ) => {
2390        $crate::__paste! {
2391            const [<$name:upper _TARGET_SAMPLE_RATE_HZ>]: u32 = $target_sample_rate_hz;
2392
2393            #[allow(non_snake_case)]
2394            #[allow(missing_docs)]
2395            $vis mod $name {
2396                const PARSED_WAV: $crate::audio_player::ParsedAdpcmWavHeader =
2397                    $crate::audio_player::__parse_adpcm_wav_header(include_bytes!($file));
2398                const SOURCE_SAMPLE_RATE_HZ: u32 = PARSED_WAV.sample_rate_hz;
2399                const TARGET_SAMPLE_RATE_HZ: u32 = super::[<$name:upper _TARGET_SAMPLE_RATE_HZ>];
2400                pub const SAMPLE_RATE_HZ: u32 = TARGET_SAMPLE_RATE_HZ;
2401
2402                const SOURCE_SAMPLE_COUNT: usize = PARSED_WAV.sample_count;
2403                #[doc = "Number of samples for uncompressed (PCM) version of this clip."]
2404                pub const PCM_SAMPLE_COUNT: usize = $crate::audio_player::__resampled_sample_count(
2405                    SOURCE_SAMPLE_COUNT,
2406                    SOURCE_SAMPLE_RATE_HZ,
2407                    TARGET_SAMPLE_RATE_HZ,
2408                );
2409                const BLOCK_ALIGN: usize = PARSED_WAV.block_align;
2410                const SOURCE_DATA_LEN: usize = PARSED_WAV.data_chunk_len;
2411                #[doc = "Byte length for compressed (ADPCM) encoding this clip."]
2412                pub const ADPCM_DATA_LEN: usize = if TARGET_SAMPLE_RATE_HZ == SOURCE_SAMPLE_RATE_HZ {
2413                    SOURCE_DATA_LEN
2414                } else {
2415                    $crate::audio_player::__adpcm_data_len_for_pcm_samples_with_block_align(
2416                        PCM_SAMPLE_COUNT,
2417                        BLOCK_ALIGN,
2418                    )
2419                };
2420                type SourceAdpcmClip = $crate::audio_player::AdpcmClipBuf<SOURCE_SAMPLE_RATE_HZ, SOURCE_DATA_LEN>;
2421
2422                #[must_use]
2423                const fn source_adpcm_clip() -> SourceAdpcmClip {
2424                    let wav_bytes = include_bytes!($file);
2425                    let parsed_wav = $crate::audio_player::__parse_adpcm_wav_header(wav_bytes);
2426                    assert!(parsed_wav.block_align <= u16::MAX as usize, "block_align too large");
2427                    assert!(
2428                        parsed_wav.samples_per_block <= u16::MAX as usize,
2429                        "samples_per_block too large"
2430                    );
2431
2432                    let mut adpcm_data = [0_u8; SOURCE_DATA_LEN];
2433                    let mut data_index = 0usize;
2434                    while data_index < SOURCE_DATA_LEN {
2435                        adpcm_data[data_index] = wav_bytes[parsed_wav.data_chunk_start + data_index];
2436                        data_index += 1;
2437                    }
2438
2439                    $crate::audio_player::__adpcm_clip_from_parts(
2440                        parsed_wav.block_align as u16,
2441                        parsed_wav.samples_per_block as u16,
2442                        parsed_wav.sample_count,
2443                        adpcm_data,
2444                    )
2445                }
2446
2447                #[doc = "`const` function that returns the uncompressed (PCM) version of this clip."]
2448                #[must_use]
2449                pub const fn pcm_clip() -> $crate::audio_player::PcmClipBuf<SAMPLE_RATE_HZ, PCM_SAMPLE_COUNT> {
2450                    $crate::audio_player::__resample_pcm_clip::<
2451                        SOURCE_SAMPLE_RATE_HZ,
2452                        SOURCE_SAMPLE_COUNT,
2453                        TARGET_SAMPLE_RATE_HZ,
2454                        PCM_SAMPLE_COUNT,
2455                    >(source_adpcm_clip().with_pcm::<SOURCE_SAMPLE_COUNT>())
2456                }
2457
2458                #[doc = "`const` function that returns the compressed (ADPCM) encoding for this clip."]
2459                #[must_use]
2460                pub const fn adpcm_clip() -> $crate::audio_player::AdpcmClipBuf<SAMPLE_RATE_HZ, ADPCM_DATA_LEN> {
2461                    if TARGET_SAMPLE_RATE_HZ == SOURCE_SAMPLE_RATE_HZ {
2462                        let wav_bytes = include_bytes!($file);
2463                        let parsed_wav = $crate::audio_player::__parse_adpcm_wav_header(wav_bytes);
2464                        assert!(parsed_wav.block_align <= u16::MAX as usize, "block_align too large");
2465                        assert!(
2466                            parsed_wav.samples_per_block <= u16::MAX as usize,
2467                            "samples_per_block too large"
2468                        );
2469                        let mut adpcm_data = [0_u8; ADPCM_DATA_LEN];
2470                        let mut data_index = 0usize;
2471                        while data_index < ADPCM_DATA_LEN {
2472                            adpcm_data[data_index] =
2473                                wav_bytes[parsed_wav.data_chunk_start + data_index];
2474                            data_index += 1;
2475                        }
2476                        $crate::audio_player::__adpcm_clip_from_parts(
2477                            parsed_wav.block_align as u16,
2478                            parsed_wav.samples_per_block as u16,
2479                            parsed_wav.sample_count,
2480                            adpcm_data,
2481                        )
2482                    } else {
2483                        $crate::audio_player::__pcm_with_adpcm_block_align::<
2484                            SAMPLE_RATE_HZ,
2485                            PCM_SAMPLE_COUNT,
2486                            ADPCM_DATA_LEN,
2487                        >(&pcm_clip(), BLOCK_ALIGN)
2488                    }
2489                }
2490
2491            }
2492        }
2493    };
2494
2495    (
2496        $vis:vis $name:ident {
2497            file: $file:expr $(,)?
2498        }
2499    ) => {
2500        $crate::__adpcm_clip_parse! {
2501            $vis $name {
2502                file: $file,
2503                target_sample_rate_hz: $crate::audio_player::__parse_adpcm_wav_header(include_bytes!($file)).sample_rate_hz,
2504            }
2505        }
2506    };
2507}
2508
2509/// Macro to create an audio clip of a musical tone.
2510///
2511/// Examples:
2512/// - `tone!(440, VOICE_22050_HZ, Duration::from_millis(500))`
2513/// - `tone!(440, AudioPlayer8::SAMPLE_RATE_HZ, Duration::from_millis(500))`
2514///
2515/// The result is an uncompressed (PCM) clip.
2516/// (It does not use compressed because ADPCM sounds poor for pure sine tones.)
2517///
2518/// See the [audio_player module documentation](mod@crate::audio_player) for
2519/// usage examples.
2520#[doc(hidden)]
2521#[macro_export]
2522macro_rules! tone {
2523    ($frequency_hz:expr, $sample_rate_hz:expr, $duration:expr) => {
2524        $crate::audio_player::__tone_pcm_clip_with_duration::<
2525            { $sample_rate_hz },
2526            { $crate::audio_player::__samples_for_duration($duration, $sample_rate_hz) },
2527        >($frequency_hz, $duration)
2528    };
2529}
2530
2531#[doc = "Macro to \"compile in\" an uncompressed (PCM) clip from an external file (includes syntax details)."]
2532#[doc = include_str!("audio_player/pcm_clip_docs.md")]
2533#[doc = include_str!("audio_player/audio_prep_steps_1_2.md")]
2534#[doc = include_str!("audio_player/pcm_clip_step_3.md")]
2535#[doc(inline)]
2536pub use crate::pcm_clip;
2537#[doc(inline)]
2538pub use crate::tone;