pyxel/
audio.rs

1use std::cmp::min;
2use std::env::temp_dir;
3use std::fs::{remove_file, write};
4use std::process::Command;
5
6use hound::{SampleFormat, WavSpec, WavWriter};
7use parking_lot::MutexGuard;
8
9use crate::blip_buf::BlipBuf;
10use crate::channel::SharedChannel;
11use crate::pyxel::{Pyxel, CHANNELS};
12use crate::settings::{CLOCKS_PER_TICK, CLOCK_RATE, NUM_SAMPLES, SAMPLE_RATE};
13use crate::utils;
14
15struct AudioCore {
16    blip_buf: BlipBuf,
17}
18
19impl pyxel_platform::AudioCallback for AudioCore {
20    fn update(&mut self, out: &mut [i16]) {
21        let channels = CHANNELS.lock();
22        Audio::render_samples(&channels, &mut self.blip_buf, out);
23    }
24}
25
26pub struct Audio {}
27
28impl Audio {
29    pub fn new(sample_rate: u32, num_samples: u32) -> Self {
30        let mut blip_buf = BlipBuf::new(NUM_SAMPLES as usize);
31        blip_buf.set_rates(CLOCK_RATE as f64, SAMPLE_RATE as f64);
32        pyxel_platform::start_audio(
33            sample_rate,
34            1,
35            num_samples as u16,
36            new_shared_type!(AudioCore { blip_buf }),
37        );
38        Self {}
39    }
40
41    pub fn render_samples(
42        channels_: &MutexGuard<'_, Vec<SharedChannel>>,
43        blip_buf: &mut BlipBuf,
44        samples: &mut [i16],
45    ) {
46        let mut channels: Vec<_> = channels_.iter().map(|channel| channel.lock()).collect();
47        let mut num_samples = blip_buf.read_samples(samples, false);
48
49        while num_samples < samples.len() {
50            for channel in &mut *channels {
51                channel.update(blip_buf);
52            }
53            blip_buf.end_frame(CLOCKS_PER_TICK as u64);
54            num_samples += blip_buf.read_samples(&mut samples[num_samples..], false);
55        }
56    }
57
58    pub fn save_samples(filename: &str, samples: &[i16], ffmpeg: bool) {
59        // Save WAV file
60        let spec = WavSpec {
61            channels: 1,
62            sample_rate: SAMPLE_RATE,
63            bits_per_sample: 16,
64            sample_format: SampleFormat::Int,
65        };
66        let filename = utils::add_file_extension(filename, ".wav");
67        let mut writer = WavWriter::create(&filename, spec)
68            .unwrap_or_else(|_| panic!("Failed to open file '{filename}'"));
69
70        for sample in samples {
71            writer.write_sample(*sample).unwrap();
72        }
73        writer.finalize().unwrap();
74
75        // Save MP4 file
76        if !ffmpeg {
77            return;
78        }
79
80        let image_data = include_bytes!("assets/pyxel_logo_152x64.png");
81        let image_path = temp_dir().join("pyxel_mp4_image.png");
82        let png_file = image_path.to_str().unwrap();
83        let wav_file = &filename;
84        let mp4_file = filename.replace(".wav", ".mp4");
85
86        write(&image_path, image_data).unwrap();
87        Command::new("ffmpeg")
88            .arg("-loop")
89            .arg("1")
90            .arg("-i")
91            .arg(png_file)
92            .arg("-f")
93            .arg("lavfi")
94            .arg("-i")
95            .arg("color=c=black:s=480x360")
96            .arg("-i")
97            .arg(wav_file)
98            .arg("-filter_complex")
99            .arg("[1][0]overlay=(W-w)/2:(H-h)/2")
100            .arg("-c:v")
101            .arg("libx264")
102            .arg("-c:a")
103            .arg("aac")
104            .arg("-b:a")
105            .arg("192k")
106            .arg("-shortest")
107            .arg(mp4_file)
108            .arg("-y")
109            .output()
110            .unwrap_or_else(|e| panic!("Failed to execute FFmpeg: {e}"));
111
112        remove_file(png_file).unwrap();
113    }
114}
115
116impl Pyxel {
117    pub fn play(
118        &self,
119        channel_index: u32,
120        sequence: &[u32],
121        start_tick: Option<u32>,
122        should_loop: bool,
123        should_resume: bool,
124    ) {
125        if sequence.is_empty() {
126            return;
127        }
128
129        let sounds = sequence
130            .iter()
131            .map(|sound_index| self.sounds.lock()[*sound_index as usize].clone())
132            .collect();
133
134        self.channels.lock()[channel_index as usize].lock().play(
135            sounds,
136            start_tick,
137            should_loop,
138            should_resume,
139        );
140    }
141
142    pub fn play1(
143        &self,
144        channel_index: u32,
145        sound_index: u32,
146        start_tick: Option<u32>,
147        should_loop: bool,
148        should_resume: bool,
149    ) {
150        self.channels.lock()[channel_index as usize].lock().play1(
151            self.sounds.lock()[sound_index as usize].clone(),
152            start_tick,
153            should_loop,
154            should_resume,
155        );
156    }
157
158    pub fn playm(&self, music_index: u32, start_tick: Option<u32>, should_loop: bool) {
159        let num_channels = self.channels.lock().len();
160        let musics = self.musics.lock();
161        let music = musics[music_index as usize].lock();
162
163        for i in 0..min(num_channels, music.seqs.len()) {
164            self.play(
165                i as u32,
166                &music.seqs[i].lock(),
167                start_tick,
168                should_loop,
169                false,
170            );
171        }
172    }
173
174    pub fn stop(&self, channel_index: u32) {
175        self.channels.lock()[channel_index as usize].lock().stop();
176    }
177
178    pub fn stop0(&self) {
179        let num_channels = self.channels.lock().len();
180
181        for i in 0..num_channels {
182            self.stop(i as u32);
183        }
184    }
185
186    pub fn play_pos(&self, channel_index: u32) -> Option<(u32, u32)> {
187        self.channels.lock()[channel_index as usize]
188            .lock()
189            .play_pos()
190    }
191}