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 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 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}