Skip to main content

launcher/pages/audio/
tasks.rs

1use crate::pages::audio::types::MusicPageState;
2#[cfg(target_os = "windows")]
3use crate::pages::audio::types::TargetBitDepth;
4use midas_audio::codecs::adpcm::{AdpcmState, SampleType};
5use midas_audio::types::Channels;
6#[cfg(target_os = "windows")]
7use rodio::Source;
8use std::fs::File;
9#[cfg(target_os = "windows")]
10use std::io::Write;
11use std::io::{BufReader, Read, Seek};
12#[cfg(target_os = "windows")]
13use std::num::NonZero;
14use std::path::Path;
15#[cfg(target_os = "windows")]
16use std::time::Duration;
17
18// Music import pipes the input through rodio's decoder, which is only
19// available on Windows builds (rodio is a Windows-only dependency here).
20#[cfg(target_os = "windows")]
21pub fn import_music_file(
22    input_path: &Path,
23    music_dir: &Path,
24    state: &MusicPageState,
25    _storage: &supreme_fs::GameStorage,
26) -> Result<supreme_music::MusicConfig, String> {
27    let file_name = input_path
28        .file_stem()
29        .and_then(|s| s.to_str())
30        .ok_or_else(|| format!("Invalid filename: {}", input_path.display()))?;
31    let output_filename = format!("{file_name}.adp");
32    let output_path = music_dir.join(&output_filename);
33
34    // Ensure music directory exists
35    if !music_dir.exists() {
36        return Err(format!(
37            "Music directory does not exist: {}. Game path may be incorrect.",
38            music_dir.display()
39        ));
40    }
41
42    // Open input file using rodio to detect characteristics
43    let inp = File::open(input_path)
44        .map_err(|e| format!("Failed to open input file '{}': {e}", input_path.display()))?;
45    let source = rodio::Decoder::new(BufReader::new(inp)).map_err(|e| {
46        format!(
47            "Failed to decode audio file '{}': {e}. Ensure it's a valid WAV/MP3/Vorbis/FLAC file.",
48            input_path.display()
49        )
50    })?;
51
52    let input_channels = source.channels();
53    let input_rate = source.sample_rate();
54
55    // We need to collect as i16. Rodio decoder can convert for us.
56    use rodio::Source as _;
57    let input_samples: Vec<i16> = source.map(|s: f32| (s * i16::MAX as f32) as i16).collect();
58
59    // Determine target format
60    let target_rate = state.target_sample_rate.to_u32(Some(input_rate.into()));
61    let target_channels = state.target_channels;
62    let target_channels_count = target_channels.count();
63
64    // Process samples (Resample & Remix)
65    let processed_samples = midas_audio::dsp::resample_and_mix(
66        &input_samples,
67        input_channels.get() as usize,
68        input_rate.into(),
69        target_channels_count,
70        target_rate,
71    );
72
73    // Prepare encoding state
74    let frame_len = 1024;
75    let encoding_type = match state.target_bit_depth {
76        TargetBitDepth::Adpcm => match target_channels {
77            Channels::Stereo => midas_audio::codecs::adpcm::SampleType::AdpcmStereo,
78            Channels::Mono => midas_audio::codecs::adpcm::SampleType::AdpcmMono,
79        },
80        TargetBitDepth::EightBit => match target_channels {
81            Channels::Stereo => midas_audio::codecs::adpcm::SampleType::PcmStereo8,
82            Channels::Mono => midas_audio::codecs::adpcm::SampleType::PcmMono8,
83        },
84        TargetBitDepth::SixteenBit => match target_channels {
85            Channels::Stereo => midas_audio::codecs::adpcm::SampleType::PcmStereo16,
86            Channels::Mono => midas_audio::codecs::adpcm::SampleType::PcmMono16,
87        },
88    };
89
90    let mut adpcm_state =
91        AdpcmState::with_type(target_channels, frame_len, encoding_type, target_rate);
92
93    let mut out_file = File::create(&output_path).map_err(|e| {
94        format!(
95            "Failed to create output file '{}': {e}. Check directory permissions and disk space.",
96            output_path.display()
97        )
98    })?;
99    let mut out_buf = vec![0u8; 1024 * 64]; // Big buffer
100
101    let mut offset = 0;
102    while offset < processed_samples.len() {
103        let (used, written) = adpcm_state.encode(&processed_samples[offset..], &mut out_buf);
104        out_file.write_all(&out_buf[..written]).map_err(|e| {
105            format!(
106                "Failed to write to output file '{}': {e}. Disk may be full.",
107                output_path.display()
108            )
109        })?;
110        offset += used / 2; // used is bytes
111        if used == 0 && written == 0 {
112            break;
113        }
114    }
115
116    let relative_path = format!("data/music/{output_filename}");
117
118    Ok(supreme_music::MusicConfig {
119        filename: relative_path,
120        halfpipe: true,
121        tracks: true,
122        author: "Unknown".to_string(),
123        track: file_name.replace('_', " "),
124        volume: 0.8,
125    })
126}
127
128/// Non-Windows stub: the rodio decoder (and with it the importer) is
129/// Windows-only in this build.
130#[cfg(not(target_os = "windows"))]
131pub fn import_music_file(
132    _input_path: &Path,
133    _music_dir: &Path,
134    _state: &MusicPageState,
135    _storage: &supreme_fs::GameStorage,
136) -> Result<supreme_music::MusicConfig, String> {
137    Err("Music import is only available on Windows builds.".to_string())
138}
139
140pub struct AdpcmSource {
141    reader: BufReader<File>,
142    state: AdpcmState,
143    buffer: Vec<i16>, // Decoded samples to play
144    buffer_pos: usize,
145    sample_rate: u32,
146    // Read by the rodio `Source` impl (Windows builds only).
147    #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
148    channels: u16,
149}
150
151impl AdpcmSource {
152    pub fn new(file: File, sample_rate: u32) -> std::io::Result<Self> {
153        let mut reader = BufReader::new(file);
154
155        // Peek at first header to get basic info
156        let mut header = [0u8; 12]; // Max header size for stereo is 12
157        if reader.read_exact(&mut header).is_err() {
158            return Err(std::io::Error::new(
159                std::io::ErrorKind::InvalidData,
160                "Failed to read ADP header",
161            ));
162        }
163
164        if header[0] != 0x17 || header[1] != 0x42 {
165            return Err(std::io::Error::new(
166                std::io::ErrorKind::InvalidData,
167                "Not a MIDAS ADPCM stream",
168            ));
169        }
170
171        let type_byte = header[5];
172        let encoding_type = SampleType::from(type_byte);
173        let channels = match encoding_type {
174            SampleType::PcmMono8 | SampleType::PcmMono16 | SampleType::AdpcmMono => Channels::Mono,
175            SampleType::PcmStereo8 | SampleType::PcmStereo16 | SampleType::AdpcmStereo => {
176                Channels::Stereo
177            }
178            _ => {
179                return Err(std::io::Error::new(
180                    std::io::ErrorKind::InvalidData,
181                    format!("Unsupported sample type: {type_byte}"),
182                ));
183            }
184        };
185
186        let h_size = 6 + channels.count() * 3;
187        let read_sum = header[4];
188        let mut calc_sum: u8 = 0;
189        for (i, b) in header.iter().take(h_size).enumerate() {
190            if i != 4 {
191                calc_sum = calc_sum.wrapping_add(*b);
192            }
193        }
194
195        if calc_sum != read_sum {
196            return Err(std::io::Error::new(
197                std::io::ErrorKind::InvalidData,
198                format!(
199                    "ADP header checksum mismatch: Expected {calc_sum} (original MIDAS), got {read_sum}"
200                ),
201            ));
202        }
203
204        // Seek back to start of file
205        reader.seek(std::io::SeekFrom::Start(0))?;
206
207        let mut state = AdpcmState::new(channels, 0); // frame_len will be read during first decode
208        state.sample_rate = sample_rate;
209
210        Ok(Self {
211            reader,
212            state,
213            buffer: Vec::new(),
214            buffer_pos: 0,
215            sample_rate,
216            channels: channels.count() as u16,
217        })
218    }
219}
220
221impl Iterator for AdpcmSource {
222    type Item = f32;
223
224    fn next(&mut self) -> Option<Self::Item> {
225        if self.buffer_pos < self.buffer.len() {
226            let sample = self.buffer[self.buffer_pos];
227            self.buffer_pos += 1;
228            return Some(f32::from(sample) / 32768.0);
229        }
230
231        // Decode next chunk
232        let mut in_buf = vec![0u8; 1200]; // enough for a frame + header
233
234        // We need to know current pos for rewinding if needed
235        let _pos_before = self.reader.stream_position().ok()?;
236        let read = self.reader.read(&mut in_buf).ok()?;
237        if read == 0 {
238            return None; // EOF
239        }
240
241        let mut out_samples = vec![0i16; 4096]; // Sufficient output buffer
242
243        let (used, written) = match self.state.decode(&in_buf[..read], &mut out_samples) {
244            Ok(res) => res,
245            Err(e) => {
246                eprintln!("ADPCM Decode Error: {e}");
247                return None;
248            }
249        };
250
251        if used == 0 && written == 0 {
252            return None;
253        }
254
255        // Seek back unused input
256        let read_u64 = read as u64;
257        let used_u64 = used as u64;
258        if used_u64 < read_u64 {
259            let back = (read_u64 - used_u64) as i64;
260            self.reader.seek(std::io::SeekFrom::Current(-back)).ok()?;
261        }
262
263        let samples_count = written / 2;
264        self.buffer = out_samples[..samples_count].to_vec();
265        self.buffer_pos = 0;
266
267        if self.buffer.is_empty() {
268            return None;
269        }
270
271        let sample = self.buffer[self.buffer_pos];
272        self.buffer_pos += 1;
273
274        // Sync sample rate if it changed (though GA files usually don't change rate mid-stream)
275        self.sample_rate = self.state.sample_rate;
276
277        Some(f32::from(sample) / 32768.0)
278    }
279}
280
281#[cfg(target_os = "windows")]
282impl Source for AdpcmSource {
283    fn current_span_len(&self) -> Option<usize> {
284        None
285    }
286
287    fn channels(&self) -> NonZero<u16> {
288        NonZero::new(self.channels).unwrap()
289    }
290
291    fn sample_rate(&self) -> NonZero<u32> {
292        NonZero::new(self.sample_rate).unwrap()
293    }
294
295    fn total_duration(&self) -> Option<Duration> {
296        None
297    }
298}