pyxel/
oscillator.rs

1use crate::blip_buf::BlipBuf;
2use crate::pyxel::TONES;
3use crate::settings::{
4    CLOCKS_PER_TICK, CLOCK_RATE, EFFECT_FADEOUT, EFFECT_HALF_FADEOUT, EFFECT_NONE,
5    EFFECT_QUARTER_FADEOUT, EFFECT_SLIDE, EFFECT_VIBRATO, INITIAL_NOISE_REG, OSCILLATOR_RESOLUTION,
6    TONE_TRIANGLE, VIBRATO_DEPTH, VIBRATO_FREQUENCY,
7};
8
9pub type Gain = f64;
10pub type ToneIndex = u16;
11pub type Effect = u16;
12
13const VIBRATO_PERIOD: u32 =
14    (CLOCK_RATE as f64 / VIBRATO_FREQUENCY / OSCILLATOR_RESOLUTION as f64) as u32;
15
16struct Slide {
17    pitch: f64,
18}
19
20struct Vibrato {
21    clock: u32,
22    phase: u32,
23}
24
25struct FadeOut {
26    duration: u32,
27    delta: Gain,
28}
29
30pub struct Oscillator {
31    pitch: f64,
32    tone: ToneIndex,
33    gain: Gain,
34    effect: Effect,
35    duration: u32,
36    clock: u32,
37    phase: u32,
38    amplitude: i16,
39    noise_reg: u16,
40    slide: Slide,
41    vibrato: Vibrato,
42    fadeout: FadeOut,
43}
44
45impl Oscillator {
46    pub fn new() -> Self {
47        Self {
48            pitch: Self::note_to_pitch(0.0),
49            tone: TONE_TRIANGLE,
50            gain: 0.0,
51            effect: EFFECT_NONE,
52            duration: 0,
53            clock: 0,
54            phase: 0,
55            amplitude: 0,
56            noise_reg: INITIAL_NOISE_REG,
57            slide: Slide { pitch: 0.0 },
58            vibrato: Vibrato { clock: 0, phase: 0 },
59            fadeout: FadeOut {
60                duration: 0,
61                delta: 0.0,
62            },
63        }
64    }
65
66    pub fn play(&mut self, note: f64, tone: ToneIndex, gain: Gain, effect: Effect, duration: u32) {
67        let last_pitch = self.pitch;
68        self.pitch = Self::note_to_pitch(note);
69        self.tone = tone;
70        self.gain = gain;
71        self.effect = effect;
72        self.duration = duration;
73
74        match effect {
75            EFFECT_NONE | EFFECT_VIBRATO => {}
76            EFFECT_SLIDE => {
77                self.slide.pitch = (self.pitch - last_pitch) / self.duration as f64;
78                self.pitch = last_pitch;
79            }
80            EFFECT_FADEOUT | EFFECT_HALF_FADEOUT | EFFECT_QUARTER_FADEOUT => {
81                self.fadeout.duration = duration;
82                if effect == EFFECT_HALF_FADEOUT {
83                    self.fadeout.duration /= 2;
84                } else if effect == EFFECT_QUARTER_FADEOUT {
85                    self.fadeout.duration /= 4;
86                }
87                self.fadeout.delta = -self.gain / self.fadeout.duration as f64;
88            }
89            _ => panic!("Invalid effect '{}'", self.effect),
90        }
91    }
92
93    pub fn stop(&mut self) {
94        self.duration = 0;
95    }
96
97    pub fn update(&mut self, blip_buf: &mut BlipBuf) {
98        // Mute sound
99        if self.duration == 0 {
100            if self.amplitude != 0 {
101                let delta = if self.amplitude > 0 { -1 } else { 1 };
102                for i in 0..CLOCKS_PER_TICK {
103                    self.amplitude += delta;
104                    blip_buf.add_delta(i as u64, delta as i32);
105                    if self.amplitude == 0 {
106                        break;
107                    }
108                }
109            }
110
111            self.clock = 0;
112            self.phase = 0;
113            self.vibrato.clock = 0;
114            self.vibrato.phase = 0;
115            return;
116        }
117
118        // Apply effect
119        match self.effect {
120            EFFECT_SLIDE => {
121                self.pitch += self.slide.pitch;
122            }
123            EFFECT_VIBRATO => {
124                self.vibrato.clock += CLOCKS_PER_TICK;
125                self.vibrato.phase = (self.vibrato.phase + self.vibrato.clock / VIBRATO_PERIOD)
126                    % OSCILLATOR_RESOLUTION;
127                self.vibrato.clock %= VIBRATO_PERIOD;
128            }
129            _ => {}
130        }
131
132        // Play sound
133        let pitch = self.pitch
134            + if self.effect == EFFECT_VIBRATO {
135                self.pitch
136                    * (if self.vibrato.phase < OSCILLATOR_RESOLUTION / 2 {
137                        self.vibrato.phase as f64 / (OSCILLATOR_RESOLUTION / 4) as f64 - 1.0
138                    } else {
139                        3.0 - self.vibrato.phase as f64 / (OSCILLATOR_RESOLUTION / 4) as f64
140                    })
141                    * VIBRATO_DEPTH
142            } else {
143                0.0
144            };
145
146        let period = (CLOCK_RATE as f64 / pitch / OSCILLATOR_RESOLUTION as f64) as u32;
147
148        let tones = TONES.lock();
149        let tone = tones[self.tone as usize].lock();
150
151        let fade_delta = if self.effect >= EFFECT_FADEOUT && self.duration <= self.fadeout.duration
152        {
153            self.fadeout.delta / ((CLOCKS_PER_TICK - self.clock) / period) as f64
154        } else {
155            0.0
156        };
157
158        while self.clock < CLOCKS_PER_TICK {
159            let last_amplitude = self.amplitude;
160            self.phase = (self.phase + 1) % OSCILLATOR_RESOLUTION;
161            self.amplitude = (tone.amplitude(self.phase, &mut self.noise_reg)
162                * self.gain
163                * i16::MAX as f64) as i16;
164            blip_buf.add_delta(
165                self.clock as u64,
166                self.amplitude as i32 - last_amplitude as i32,
167            );
168            self.clock += period;
169            self.gain += fade_delta;
170        }
171
172        self.duration -= 1;
173        if self.duration == 0 && self.effect >= EFFECT_FADEOUT {
174            self.clock = 0;
175            self.phase = 0;
176            self.vibrato.clock = 0;
177            self.vibrato.phase = 0;
178        } else {
179            self.clock -= CLOCKS_PER_TICK;
180        }
181    }
182
183    fn note_to_pitch(note: f64) -> f64 {
184        440.0 * ((note - 33.0) / 12.0).exp2()
185    }
186}