Skip to main content

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    #[inline]
106    pub fn next_sample(&mut self) -> f32 {
107        if self.len == 0 {
108            return 0.0;
109        }
110        let sample = self.samples[self.pos];
111        self.pos += 1;
112        sample
113    }
114
115    /// Get the channel outputs for the current/last sample.
116    #[must_use]
117    pub fn channel_outputs(&self) -> [f32; 3] {
118        if self.pos > 0 && self.pos <= self.len {
119            self.channel_outputs[self.pos - 1]
120        } else if !self.channel_outputs.is_empty() {
121            self.channel_outputs[0]
122        } else {
123            [0.0, 0.0, 0.0]
124        }
125    }
126
127    /// Reset the cache, forcing a refill on the next access.
128    pub fn reset(&mut self) {
129        self.pos = 0;
130        self.len = 0;
131    }
132
133    /// Get the cache size.
134    #[must_use]
135    pub fn size(&self) -> usize {
136        self.size
137    }
138}
139
140impl Default for SampleCache {
141    fn default() -> Self {
142        Self::new(DEFAULT_CACHE_SIZE)
143    }
144}
145
146// ============================================================================
147// CachedPlayer - Full player wrapper
148// ============================================================================
149
150/// Trait for players that can be wrapped with caching.
151///
152/// This trait provides the hooks needed by [`CachedPlayer`] to interact
153/// with the underlying player.
154pub trait CacheablePlayer: ChiptunePlayerBase {
155    /// Get the current channel outputs from the chip.
156    ///
157    /// Returns `[channel_a, channel_b, channel_c]` as f32 values.
158    fn get_channel_outputs(&self) -> [f32; 3];
159
160    /// Called when the cache is about to be refilled.
161    ///
162    /// Override this to perform any pre-fill setup. Default is no-op.
163    fn on_cache_refill(&mut self) {}
164}
165
166/// A cached wrapper around a chiptune player.
167///
168/// This wrapper maintains a sample cache and channel output cache,
169/// reducing the overhead of single-sample generation for CPU-emulated players.
170///
171/// # Example
172///
173/// ```ignore
174/// use ym2149_common::{CachedPlayer, CacheablePlayer, DEFAULT_CACHE_SIZE};
175///
176/// let player = SomePlayer::new();
177/// let mut cached = CachedPlayer::new(player, DEFAULT_CACHE_SIZE);
178///
179/// // Generate samples one at a time (efficient due to caching)
180/// let sample = cached.generate_sample();
181/// let channels = cached.cached_channel_outputs();
182/// ```
183pub struct CachedPlayer<P: CacheablePlayer> {
184    player: P,
185    cache: SampleCache,
186}
187
188impl<P: CacheablePlayer> CachedPlayer<P> {
189    /// Create a new cached player with the specified cache size.
190    pub fn new(player: P, cache_size: usize) -> Self {
191        Self {
192            player,
193            cache: SampleCache::new(cache_size),
194        }
195    }
196
197    /// Create a new cached player with the default cache size (512 samples).
198    pub fn with_default_cache(player: P) -> Self {
199        Self::new(player, DEFAULT_CACHE_SIZE)
200    }
201
202    /// Get a reference to the underlying player.
203    pub fn inner(&self) -> &P {
204        &self.player
205    }
206
207    /// Get a mutable reference to the underlying player.
208    pub fn inner_mut(&mut self) -> &mut P {
209        &mut self.player
210    }
211
212    /// Consume the wrapper and return the underlying player.
213    pub fn into_inner(self) -> P {
214        self.player
215    }
216
217    /// Generate a single sample, using the cache.
218    ///
219    /// If the cache is exhausted, it will be refilled automatically.
220    pub fn generate_sample(&mut self) -> f32 {
221        if self.cache.needs_refill() {
222            self.refill_cache();
223        }
224        self.cache.next_sample()
225    }
226
227    /// Get the channel outputs corresponding to the last generated sample.
228    ///
229    /// This returns the channel outputs that were captured when the cache
230    /// was last filled. Not sample-accurate, but provides reasonable
231    /// visualization data.
232    pub fn cached_channel_outputs(&self) -> [f32; 3] {
233        self.cache.channel_outputs()
234    }
235
236    /// Reset the cache, forcing a refill on the next sample request.
237    pub fn reset_cache(&mut self) {
238        self.cache.reset();
239    }
240
241    /// Refill the cache from the underlying player.
242    fn refill_cache(&mut self) {
243        self.player.on_cache_refill();
244        self.player
245            .generate_samples_into(self.cache.sample_buffer_mut());
246        self.cache
247            .fill_channel_outputs(self.player.get_channel_outputs());
248        self.cache.mark_filled();
249    }
250}
251
252// Forward ChiptunePlayerBase methods to the inner player
253impl<P: CacheablePlayer> ChiptunePlayerBase for CachedPlayer<P> {
254    fn play(&mut self) {
255        self.player.play();
256    }
257
258    fn pause(&mut self) {
259        self.player.pause();
260    }
261
262    fn stop(&mut self) {
263        self.player.stop();
264        self.reset_cache();
265    }
266
267    fn state(&self) -> PlaybackState {
268        self.player.state()
269    }
270
271    fn generate_samples_into(&mut self, buffer: &mut [f32]) {
272        // For bulk generation, bypass the cache and use the player directly
273        self.player.generate_samples_into(buffer);
274    }
275
276    fn sample_rate(&self) -> u32 {
277        self.player.sample_rate()
278    }
279
280    fn set_channel_mute(&mut self, channel: usize, mute: bool) {
281        self.player.set_channel_mute(channel, mute);
282    }
283
284    fn is_channel_muted(&self, channel: usize) -> bool {
285        self.player.is_channel_muted(channel)
286    }
287
288    fn playback_position(&self) -> f32 {
289        self.player.playback_position()
290    }
291
292    fn subsong_count(&self) -> usize {
293        self.player.subsong_count()
294    }
295
296    fn current_subsong(&self) -> usize {
297        self.player.current_subsong()
298    }
299
300    fn set_subsong(&mut self, index: usize) -> bool {
301        if self.player.set_subsong(index) {
302            self.reset_cache();
303            true
304        } else {
305            false
306        }
307    }
308
309    fn psg_count(&self) -> usize {
310        self.player.psg_count()
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn test_sample_cache_basic() {
320        let mut cache = SampleCache::new(4);
321        assert!(cache.needs_refill());
322
323        // Fill the cache
324        cache
325            .sample_buffer_mut()
326            .copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
327        cache.fill_channel_outputs([0.1, 0.2, 0.3]);
328        cache.mark_filled();
329
330        assert!(!cache.needs_refill());
331        assert_eq!(cache.next_sample(), 1.0);
332        assert_eq!(cache.next_sample(), 2.0);
333        assert_eq!(cache.channel_outputs(), [0.1, 0.2, 0.3]);
334    }
335
336    #[test]
337    fn test_sample_cache_reset() {
338        let mut cache = SampleCache::new(4);
339        cache
340            .sample_buffer_mut()
341            .copy_from_slice(&[1.0, 2.0, 3.0, 4.0]);
342        cache.mark_filled();
343
344        let _ = cache.next_sample();
345        cache.reset();
346
347        assert!(cache.needs_refill());
348    }
349
350    // Mock player for CachedPlayer tests
351    struct MockPlayer {
352        samples_generated: usize,
353        state: PlaybackState,
354    }
355
356    impl MockPlayer {
357        fn new() -> Self {
358            Self {
359                samples_generated: 0,
360                state: PlaybackState::Playing,
361            }
362        }
363    }
364
365    impl ChiptunePlayerBase for MockPlayer {
366        fn play(&mut self) {
367            self.state = PlaybackState::Playing;
368        }
369
370        fn pause(&mut self) {
371            self.state = PlaybackState::Paused;
372        }
373
374        fn stop(&mut self) {
375            self.state = PlaybackState::Stopped;
376        }
377
378        fn state(&self) -> PlaybackState {
379            self.state
380        }
381
382        fn generate_samples_into(&mut self, buffer: &mut [f32]) {
383            for (i, sample) in buffer.iter_mut().enumerate() {
384                *sample = (self.samples_generated + i) as f32 * 0.001;
385            }
386            self.samples_generated += buffer.len();
387        }
388    }
389
390    impl CacheablePlayer for MockPlayer {
391        fn get_channel_outputs(&self) -> [f32; 3] {
392            [0.1, 0.2, 0.3]
393        }
394    }
395
396    #[test]
397    fn test_cached_player_generates_samples() {
398        let player = MockPlayer::new();
399        let mut cached = CachedPlayer::new(player, 16);
400
401        let s1 = cached.generate_sample();
402        let s2 = cached.generate_sample();
403
404        assert!((s1 - 0.0).abs() < 0.0001);
405        assert!((s2 - 0.001).abs() < 0.0001);
406    }
407
408    #[test]
409    fn test_cached_player_channel_outputs() {
410        let player = MockPlayer::new();
411        let mut cached = CachedPlayer::new(player, 16);
412
413        let _ = cached.generate_sample();
414        let channels = cached.cached_channel_outputs();
415
416        assert_eq!(channels, [0.1, 0.2, 0.3]);
417    }
418
419    #[test]
420    fn test_cache_reset_on_stop() {
421        let player = MockPlayer::new();
422        let mut cached = CachedPlayer::new(player, 16);
423
424        let _ = cached.generate_sample();
425        cached.stop();
426
427        // After stop, cache should need refill
428        assert!(cached.cache.needs_refill());
429    }
430}