ym2149_common/
cached_player.rs

1//! Sample caching utilities for CPU-emulated backends.
2//!
3//! This module provides [`SampleCache`], a helper for efficient sample caching,
4//! and [`CachedPlayer`], a full wrapper around chiptune players.
5//!
6//! # Why Caching?
7//!
8//! Players like AY (Z80) and SNDH (68000) generate samples in batches through
9//! CPU emulation. The cache allows efficient single-sample access while still
10//! generating samples in larger batches.
11//!
12//! # Channel Output Caching
13//!
14//! The cache also stores YM2149 channel outputs after each refill,
15//! enabling synchronized visualization without sample-accurate overhead.
16
17use crate::{ChiptunePlayerBase, PlaybackState};
18
19/// Default cache size in samples.
20pub const DEFAULT_CACHE_SIZE: usize = 512;
21
22// ============================================================================
23// SampleCache - Standalone cache helper
24// ============================================================================
25
26/// A sample cache for efficient single-sample access.
27///
28/// This is a lightweight helper that manages sample and channel output caching.
29/// Use it when you need to add caching to a player without wrapping it entirely.
30///
31/// # Example
32///
33/// ```ignore
34/// use ym2149_common::SampleCache;
35///
36/// struct MyPlayer {
37///     inner: SomePlayer,
38///     cache: SampleCache,
39/// }
40///
41/// impl MyPlayer {
42///     fn generate_sample(&mut self) -> f32 {
43///         if self.cache.needs_refill() {
44///             // Fill the cache
45///             self.inner.generate_samples_into(self.cache.sample_buffer_mut());
46///             let outputs = self.inner.get_channel_outputs();
47///             self.cache.fill_channel_outputs(outputs);
48///             self.cache.mark_filled();
49///         }
50///         self.cache.next_sample()
51///     }
52/// }
53/// ```
54#[derive(Clone)]
55pub struct SampleCache {
56    samples: Vec<f32>,
57    channel_outputs: Vec<[f32; 3]>,
58    pos: usize,
59    len: usize,
60    size: usize,
61}
62
63impl SampleCache {
64    /// Create a new sample cache with the specified size.
65    #[must_use]
66    pub fn new(size: usize) -> Self {
67        Self {
68            samples: vec![0.0; size],
69            channel_outputs: vec![[0.0; 3]; size],
70            pos: 0,
71            len: 0,
72            size,
73        }
74    }
75
76    /// Check if the cache needs to be refilled.
77    #[inline]
78    #[must_use]
79    pub fn needs_refill(&self) -> bool {
80        self.pos >= self.len
81    }
82
83    /// Get a mutable reference to the sample buffer for filling.
84    pub fn sample_buffer_mut(&mut self) -> &mut [f32] {
85        &mut self.samples[..self.size]
86    }
87
88    /// Fill all channel output entries with the same value.
89    ///
90    /// Call this after filling samples to set the channel outputs
91    /// for visualization.
92    pub fn fill_channel_outputs(&mut self, outputs: [f32; 3]) {
93        self.channel_outputs[..self.size].fill(outputs);
94    }
95
96    /// Mark the cache as filled and reset position to start.
97    pub fn mark_filled(&mut self) {
98        self.pos = 0;
99        self.len = self.size;
100    }
101
102    /// Get the next sample from the cache.
103    ///
104    /// Returns 0.0 if the cache is empty.
105    pub fn next_sample(&mut self) -> f32 {
106        if self.len == 0 {
107            return 0.0;
108        }
109        let sample = self.samples[self.pos];
110        self.pos += 1;
111        sample
112    }
113
114    /// Get the channel outputs for the current/last sample.
115    #[must_use]
116    pub fn channel_outputs(&self) -> [f32; 3] {
117        if self.pos > 0 && self.pos <= self.len {
118            self.channel_outputs[self.pos - 1]
119        } else if !self.channel_outputs.is_empty() {
120            self.channel_outputs[0]
121        } else {
122            [0.0, 0.0, 0.0]
123        }
124    }
125
126    /// Reset the cache, forcing a refill on the next access.
127    pub fn reset(&mut self) {
128        self.pos = 0;
129        self.len = 0;
130    }
131
132    /// Get the cache size.
133    #[must_use]
134    pub fn size(&self) -> usize {
135        self.size
136    }
137}
138
139impl Default for SampleCache {
140    fn default() -> Self {
141        Self::new(DEFAULT_CACHE_SIZE)
142    }
143}
144
145// ============================================================================
146// CachedPlayer - Full player wrapper
147// ============================================================================
148
149/// Trait for players that can be wrapped with caching.
150///
151/// This trait provides the hooks needed by [`CachedPlayer`] to interact
152/// with the underlying player.
153pub trait CacheablePlayer: ChiptunePlayerBase {
154    /// Get the current channel outputs from the chip.
155    ///
156    /// Returns `[channel_a, channel_b, channel_c]` as f32 values.
157    fn get_channel_outputs(&self) -> [f32; 3];
158
159    /// Called when the cache is about to be refilled.
160    ///
161    /// Override this to perform any pre-fill setup. Default is no-op.
162    fn on_cache_refill(&mut self) {}
163}
164
165/// A cached wrapper around a chiptune player.
166///
167/// This wrapper maintains a sample cache and channel output cache,
168/// reducing the overhead of single-sample generation for CPU-emulated players.
169///
170/// # Example
171///
172/// ```ignore
173/// use ym2149_common::{CachedPlayer, CacheablePlayer, DEFAULT_CACHE_SIZE};
174///
175/// let player = SomePlayer::new();
176/// let mut cached = CachedPlayer::new(player, DEFAULT_CACHE_SIZE);
177///
178/// // Generate samples one at a time (efficient due to caching)
179/// let sample = cached.generate_sample();
180/// let channels = cached.cached_channel_outputs();
181/// ```
182pub struct CachedPlayer<P: CacheablePlayer> {
183    player: P,
184    cache: SampleCache,
185}
186
187impl<P: CacheablePlayer> CachedPlayer<P> {
188    /// Create a new cached player with the specified cache size.
189    pub fn new(player: P, cache_size: usize) -> Self {
190        Self {
191            player,
192            cache: SampleCache::new(cache_size),
193        }
194    }
195
196    /// Create a new cached player with the default cache size (512 samples).
197    pub fn with_default_cache(player: P) -> Self {
198        Self::new(player, DEFAULT_CACHE_SIZE)
199    }
200
201    /// Get a reference to the underlying player.
202    pub fn inner(&self) -> &P {
203        &self.player
204    }
205
206    /// Get a mutable reference to the underlying player.
207    pub fn inner_mut(&mut self) -> &mut P {
208        &mut self.player
209    }
210
211    /// Consume the wrapper and return the underlying player.
212    pub fn into_inner(self) -> P {
213        self.player
214    }
215
216    /// Generate a single sample, using the cache.
217    ///
218    /// If the cache is exhausted, it will be refilled automatically.
219    pub fn generate_sample(&mut self) -> f32 {
220        if self.cache.needs_refill() {
221            self.refill_cache();
222        }
223        self.cache.next_sample()
224    }
225
226    /// Get the channel outputs corresponding to the last generated sample.
227    ///
228    /// This returns the channel outputs that were captured when the cache
229    /// was last filled. Not sample-accurate, but provides reasonable
230    /// visualization data.
231    pub fn cached_channel_outputs(&self) -> [f32; 3] {
232        self.cache.channel_outputs()
233    }
234
235    /// Reset the cache, forcing a refill on the next sample request.
236    pub fn reset_cache(&mut self) {
237        self.cache.reset();
238    }
239
240    /// Refill the cache from the underlying player.
241    fn refill_cache(&mut self) {
242        self.player.on_cache_refill();
243        self.player
244            .generate_samples_into(self.cache.sample_buffer_mut());
245        self.cache
246            .fill_channel_outputs(self.player.get_channel_outputs());
247        self.cache.mark_filled();
248    }
249}
250
251// Forward ChiptunePlayerBase methods to the inner player
252impl<P: CacheablePlayer> ChiptunePlayerBase for CachedPlayer<P> {
253    fn play(&mut self) {
254        self.player.play();
255    }
256
257    fn pause(&mut self) {
258        self.player.pause();
259    }
260
261    fn stop(&mut self) {
262        self.player.stop();
263        self.reset_cache();
264    }
265
266    fn state(&self) -> PlaybackState {
267        self.player.state()
268    }
269
270    fn generate_samples_into(&mut self, buffer: &mut [f32]) {
271        // For bulk generation, bypass the cache and use the player directly
272        self.player.generate_samples_into(buffer);
273    }
274
275    fn sample_rate(&self) -> u32 {
276        self.player.sample_rate()
277    }
278
279    fn set_channel_mute(&mut self, channel: usize, mute: bool) {
280        self.player.set_channel_mute(channel, mute);
281    }
282
283    fn is_channel_muted(&self, channel: usize) -> bool {
284        self.player.is_channel_muted(channel)
285    }
286
287    fn playback_position(&self) -> f32 {
288        self.player.playback_position()
289    }
290
291    fn subsong_count(&self) -> usize {
292        self.player.subsong_count()
293    }
294
295    fn current_subsong(&self) -> usize {
296        self.player.current_subsong()
297    }
298
299    fn set_subsong(&mut self, index: usize) -> bool {
300        if self.player.set_subsong(index) {
301            self.reset_cache();
302            true
303        } else {
304            false
305        }
306    }
307
308    fn psg_count(&self) -> usize {
309        self.player.psg_count()
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316
317    #[test]
318    fn test_sample_cache_basic() {
319        let mut cache = SampleCache::new(4);
320        assert!(cache.needs_refill());
321
322        // Fill the cache
323        cache
324            .sample_buffer_mut()
325            .copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
326        cache.fill_channel_outputs([0.1, 0.2, 0.3]);
327        cache.mark_filled();
328
329        assert!(!cache.needs_refill());
330        assert_eq!(cache.next_sample(), 1.0);
331        assert_eq!(cache.next_sample(), 2.0);
332        assert_eq!(cache.channel_outputs(), [0.1, 0.2, 0.3]);
333    }
334
335    #[test]
336    fn test_sample_cache_reset() {
337        let mut cache = SampleCache::new(4);
338        cache
339            .sample_buffer_mut()
340            .copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
341        cache.mark_filled();
342
343        let _ = cache.next_sample();
344        cache.reset();
345
346        assert!(cache.needs_refill());
347    }
348
349    // Mock player for CachedPlayer tests
350    struct MockPlayer {
351        samples_generated: usize,
352        state: PlaybackState,
353    }
354
355    impl MockPlayer {
356        fn new() -> Self {
357            Self {
358                samples_generated: 0,
359                state: PlaybackState::Playing,
360            }
361        }
362    }
363
364    impl ChiptunePlayerBase for MockPlayer {
365        fn play(&mut self) {
366            self.state = PlaybackState::Playing;
367        }
368
369        fn pause(&mut self) {
370            self.state = PlaybackState::Paused;
371        }
372
373        fn stop(&mut self) {
374            self.state = PlaybackState::Stopped;
375        }
376
377        fn state(&self) -> PlaybackState {
378            self.state
379        }
380
381        fn generate_samples_into(&mut self, buffer: &mut [f32]) {
382            for (i, sample) in buffer.iter_mut().enumerate() {
383                *sample = (self.samples_generated + i) as f32 * 0.001;
384            }
385            self.samples_generated += buffer.len();
386        }
387    }
388
389    impl CacheablePlayer for MockPlayer {
390        fn get_channel_outputs(&self) -> [f32; 3] {
391            [0.1, 0.2, 0.3]
392        }
393    }
394
395    #[test]
396    fn test_cached_player_generates_samples() {
397        let player = MockPlayer::new();
398        let mut cached = CachedPlayer::new(player, 16);
399
400        let s1 = cached.generate_sample();
401        let s2 = cached.generate_sample();
402
403        assert!((s1 - 0.0).abs() < 0.0001);
404        assert!((s2 - 0.001).abs() < 0.0001);
405    }
406
407    #[test]
408    fn test_cached_player_channel_outputs() {
409        let player = MockPlayer::new();
410        let mut cached = CachedPlayer::new(player, 16);
411
412        let _ = cached.generate_sample();
413        let channels = cached.cached_channel_outputs();
414
415        assert_eq!(channels, [0.1, 0.2, 0.3]);
416    }
417
418    #[test]
419    fn test_cache_reset_on_stop() {
420        let player = MockPlayer::new();
421        let mut cached = CachedPlayer::new(player, 16);
422
423        let _ = cached.generate_sample();
424        cached.stop();
425
426        // After stop, cache should need refill
427        assert!(cached.cache.needs_refill());
428    }
429}