pyxel/
sound.rs

1use crate::audio::Audio;
2use crate::blip_buf::BlipBuf;
3use crate::channel::{Note, Speed, Volume};
4use crate::oscillator::{Effect, ToneIndex};
5use crate::pyxel::CHANNELS;
6use crate::settings::{
7    CLOCK_RATE, EFFECT_FADEOUT, EFFECT_HALF_FADEOUT, EFFECT_NONE, EFFECT_QUARTER_FADEOUT,
8    EFFECT_SLIDE, EFFECT_VIBRATO, INITIAL_SOUND_SPEED, SAMPLE_RATE, TICKS_PER_SECOND, TONE_NOISE,
9    TONE_PULSE, TONE_SQUARE, TONE_TRIANGLE,
10};
11use crate::utils::simplify_string;
12
13#[derive(Clone)]
14pub struct Sound {
15    pub notes: Vec<Note>,
16    pub tones: Vec<ToneIndex>,
17    pub volumes: Vec<Volume>,
18    pub effects: Vec<Effect>,
19    pub speed: Speed,
20}
21
22pub type SharedSound = shared_type!(Sound);
23
24impl Sound {
25    pub fn new() -> SharedSound {
26        new_shared_type!(Self {
27            notes: Vec::new(),
28            tones: Vec::new(),
29            volumes: Vec::new(),
30            effects: Vec::new(),
31            speed: INITIAL_SOUND_SPEED,
32        })
33    }
34
35    pub fn set(
36        &mut self,
37        note_str: &str,
38        tone_str: &str,
39        volume_str: &str,
40        effect_str: &str,
41        speed: Speed,
42    ) {
43        self.set_notes(note_str);
44        self.set_tones(tone_str);
45        self.set_volumes(volume_str);
46        self.set_effects(effect_str);
47        self.speed = speed;
48    }
49
50    pub fn set_notes(&mut self, note_str: &str) {
51        let note_str = simplify_string(note_str);
52        let mut chars = note_str.chars();
53        self.notes.clear();
54
55        while let Some(c) = chars.next() {
56            let mut note: Note;
57            if ('a'..='g').contains(&c) {
58                note = match c {
59                    'c' => 0,
60                    'd' => 2,
61                    'e' => 4,
62                    'f' => 5,
63                    'g' => 7,
64                    'a' => 9,
65                    'b' => 11,
66                    _ => panic!("Invalid sound note '{c}'"),
67                };
68
69                let mut c = chars.next().unwrap_or(0 as char);
70                if c == '#' {
71                    note += 1;
72                    c = chars.next().unwrap_or(0 as char);
73                } else if c == '-' {
74                    note -= 1;
75                    c = chars.next().unwrap_or(0 as char);
76                }
77
78                if ('0'..='4').contains(&c) {
79                    note += (c.to_digit(10).unwrap() as Note) * 12;
80                } else {
81                    panic!("Invalid sound note '{c}'");
82                }
83            } else if c == 'r' {
84                note = -1;
85            } else {
86                panic!("Invalid sound note '{c}'");
87            }
88            self.notes.push(note);
89        }
90    }
91
92    pub fn set_tones(&mut self, tone_str: &str) {
93        self.tones.clear();
94        for c in simplify_string(tone_str).chars() {
95            let tone = match c {
96                't' => TONE_TRIANGLE,
97                's' => TONE_SQUARE,
98                'p' => TONE_PULSE,
99                'n' => TONE_NOISE,
100                '0'..='9' => c.to_digit(10).unwrap() as ToneIndex,
101                _ => panic!("Invalid sound tone '{c}'"),
102            };
103            self.tones.push(tone);
104        }
105    }
106
107    pub fn set_volumes(&mut self, volume_str: &str) {
108        self.volumes.clear();
109        for c in simplify_string(volume_str).chars() {
110            if ('0'..='7').contains(&c) {
111                self.volumes.push(c.to_digit(10).unwrap() as Volume);
112            } else {
113                panic!("Invalid sound volume '{c}'");
114            }
115        }
116    }
117
118    pub fn set_effects(&mut self, effect_str: &str) {
119        self.effects.clear();
120        for c in simplify_string(effect_str).chars() {
121            let effect = match c {
122                'n' => EFFECT_NONE,
123                's' => EFFECT_SLIDE,
124                'v' => EFFECT_VIBRATO,
125                'f' => EFFECT_FADEOUT,
126                'h' => EFFECT_HALF_FADEOUT,
127                'q' => EFFECT_QUARTER_FADEOUT,
128                _ => panic!("Invalid sound effect '{c}'"),
129            };
130            self.effects.push(effect);
131        }
132    }
133
134    pub fn save(&self, filename: &str, count: u32, ffmpeg: Option<bool>) {
135        assert!(count > 0);
136        let ticks_per_sound = self.speed * self.notes.len() as u32;
137        let samples_per_sound = ticks_per_sound * SAMPLE_RATE / TICKS_PER_SECOND;
138        let num_samples = samples_per_sound * count;
139
140        if num_samples == 0 {
141            return;
142        }
143
144        let mut samples = vec![0; num_samples as usize];
145        let mut blip_buf = BlipBuf::new(num_samples as usize);
146        blip_buf.set_rates(CLOCK_RATE as f64, SAMPLE_RATE as f64);
147
148        let channels = CHANNELS.lock();
149        channels.iter().for_each(|channel| channel.lock().stop());
150
151        {
152            let mut channels: Vec<_> = channels.iter().map(|channel| channel.lock()).collect();
153            let sounds = vec![new_shared_type!(self.clone())];
154            channels[0].play(sounds, None, true, false);
155        }
156
157        Audio::render_samples(&channels, &mut blip_buf, &mut samples);
158        Audio::save_samples(filename, &samples, ffmpeg.unwrap_or(false));
159        channels.iter().for_each(|channel| channel.lock().stop());
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166
167    #[test]
168    fn test_sound_new() {
169        let sound = Sound::new();
170        assert_eq!(sound.lock().notes.len(), 0);
171        assert_eq!(sound.lock().tones.len(), 0);
172        assert_eq!(sound.lock().volumes.len(), 0);
173        assert_eq!(sound.lock().effects.len(), 0);
174        assert_eq!(sound.lock().speed, INITIAL_SOUND_SPEED);
175    }
176
177    #[test]
178    fn test_sound_set() {
179        let sound = Sound::new();
180        sound
181            .lock()
182            .set("c0d-0d0d#0", "tspn", "012345", "nsvfhq", 123);
183        assert_eq!(&sound.lock().notes, &vec![0, 1, 2, 3]);
184        assert_eq!(
185            &sound.lock().tones,
186            &vec![TONE_TRIANGLE, TONE_SQUARE, TONE_PULSE, TONE_NOISE]
187        );
188        assert_eq!(&sound.lock().volumes, &vec![0, 1, 2, 3, 4, 5]);
189        assert_eq!(
190            &sound.lock().effects,
191            &vec![
192                EFFECT_NONE,
193                EFFECT_SLIDE,
194                EFFECT_VIBRATO,
195                EFFECT_FADEOUT,
196                EFFECT_HALF_FADEOUT,
197                EFFECT_QUARTER_FADEOUT
198            ]
199        );
200        assert_eq!(sound.lock().speed, 123);
201    }
202
203    #[test]
204    fn test_sound_set_note() {
205        let sound = Sound::new();
206        sound
207            .lock()
208            .set_notes(" c 0 d # 1 r e 2 f 3 g 4 r a - 0 b 1 ");
209        assert_eq!(&sound.lock().notes, &vec![0, 15, -1, 28, 41, 55, -1, 8, 23]);
210    }
211
212    #[test]
213    fn test_sound_set_tone() {
214        let sound = Sound::new();
215        sound.lock().set_tones(" t s p n ");
216        assert_eq!(
217            &sound.lock().tones,
218            &vec![TONE_TRIANGLE, TONE_SQUARE, TONE_PULSE, TONE_NOISE]
219        );
220    }
221
222    #[test]
223    fn test_sound_set_volume() {
224        let sound = Sound::new();
225        sound.lock().set_volumes(" 0 1 2 3 4 5 6 7 ");
226        assert_eq!(&sound.lock().volumes, &vec![0, 1, 2, 3, 4, 5, 6, 7]);
227    }
228
229    #[test]
230    fn test_sound_set_effect() {
231        let sound = Sound::new();
232        sound.lock().set_effects(" n s v f h q");
233        assert_eq!(
234            &sound.lock().effects,
235            &vec![
236                EFFECT_NONE,
237                EFFECT_SLIDE,
238                EFFECT_VIBRATO,
239                EFFECT_FADEOUT,
240                EFFECT_HALF_FADEOUT,
241                EFFECT_QUARTER_FADEOUT
242            ]
243        );
244    }
245}