1#[cfg(feature = "std")]
2use crate::ReadSongNotes;
3use core::cell::RefCell;
4use core::fmt::Debug;
5#[cfg(feature = "std")]
6use std::{io::Read, path::Path};
7
8use crate::constants::{NOTE_NAMES, PENTATONIC_MINOR, SAMPLE_RATE};
9use crate::plugin::chiptune::ChiptunePlugin;
10use crate::plugin::koto::KotoPlugin;
11use crate::plugin::metal::MetalPlugin;
12use crate::plugin::rap::RapPlugin;
13use crate::plugin::rock::RockPlugin;
14use crate::plugin::samba::SambaPlugin;
15use crate::plugin::toy::ToyPlugin;
16use crate::plugin::trap::TrapPlugin;
17use crate::plugin::{Plugin, Random};
18use crate::{DrumSampleGenerator, MixGenerator, SampleDrumSteps, SongNoteReader};
19use crate::{SongMetadata, Timing};
20use alloc::format;
21use alloc::rc::Rc;
22use alloc::string::{String, ToString};
23use alloc::vec::Vec;
24use rand::{Rng, SeedableRng};
25use thiserror::Error;
26
27#[derive(Error, Debug)]
28pub enum GenerateError {
29 #[error("empty input")]
30 EmptyInput,
31 #[error("invalid seed")]
32 InvalidSeed,
33}
34
35#[cfg(all(
36 feature = "std",
37 not(all(target_arch = "wasm32", target_os = "unknown"))
38))]
39#[derive(Error, Debug)]
40pub enum SongMetadataFromPathError {
41 #[error("IO error")]
42 Io(#[from] std::io::Error),
43 #[error("file too large")]
44 FileTooLarge,
45 #[error("error generating")]
46 Generate(#[from] GenerateError),
47}
48
49#[cfg(all(
50 feature = "std",
51 feature = "wav",
52 not(all(target_arch = "wasm32", target_os = "unknown"))
53))]
54#[derive(Error, Debug)]
55#[error(transparent)]
56pub enum ConvertWavError {
57 IoError(#[from] std::io::Error),
58 SongMetadataError(#[from] SongMetadataFromPathError),
59 HoundError(#[from] hound::Error),
60}
61
62#[derive(Error, Debug)]
63pub enum ChiptunomaticError {
64 #[error("no mode {0}")]
65 InvalidMode(String),
66}
67
68pub struct RandomWrapper<R: Rng + ?Sized> {
69 rng: Rc<RefCell<R>>,
70}
71
72impl<R: Rng + ?Sized> Debug for RandomWrapper<R> {
73 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
74 f.debug_struct("RandomWrapper").finish()
75 }
76}
77
78impl<R: Rng + SeedableRng> RandomWrapper<R> {
79 pub fn from_seed(seed: u64) -> Self {
80 Self {
81 rng: Rc::new(RefCell::new(R::seed_from_u64(seed))),
82 }
83 }
84}
85
86impl<R: Rng + ?Sized> Random for RandomWrapper<R> {
87 fn next_float(&self) -> f32 {
88 self.rng.borrow_mut().gen::<f32>()
89 }
90}
91
92pub struct Chiptunomatic {
93 plugins: Vec<Rc<dyn Plugin>>,
94 selected_plugin: Rc<dyn Plugin>,
95 song_metadata: SongMetadata,
96 sample_rate: u32,
97}
98
99impl Chiptunomatic {
100 pub fn new() -> Self {
102 let plugin = Rc::new(ChiptunePlugin::new());
103 let mut chiptunomatic = Chiptunomatic {
104 plugins: Default::default(),
105 selected_plugin: plugin.clone(),
106 song_metadata: Default::default(),
107 sample_rate: SAMPLE_RATE,
108 };
109
110 chiptunomatic.plugins.push(plugin);
111 chiptunomatic
112 }
113
114 pub fn with_default_plugins(self) -> Self {
115 let mut other = Self { ..self };
116 other.register_default_plugins();
117 other
118 }
119
120 pub fn with_sample_rate(self, sample_rate: u32) -> Self {
121 Self {
122 sample_rate,
123 ..self
124 }
125 }
126
127 pub fn register_plugin(&mut self, plugin: Rc<dyn Plugin>) {
128 self.plugins.push(plugin);
129 }
130
131 pub fn register_default_plugins(&mut self) {
132 self.register_plugin(Rc::new(RockPlugin {}));
134 self.register_plugin(Rc::new(MetalPlugin {}));
135 self.register_plugin(Rc::new(RapPlugin {}));
137 self.register_plugin(Rc::new(TrapPlugin {}));
138 self.register_plugin(Rc::new(ToyPlugin {}));
139 self.register_plugin(Rc::new(SambaPlugin {}));
140 self.register_plugin(Rc::new(KotoPlugin {}));
141 }
143
144 pub fn plugin(&self) -> Rc<dyn Plugin> {
145 self.selected_plugin.clone()
146 }
147
148 pub fn modes(&self) -> Vec<&'static str> {
150 self.plugins.iter().map(|p| p.mode()).collect()
151 }
152
153 pub fn modes_string(&self) -> Vec<String> {
154 self.plugins.iter().map(|p| p.mode_string()).collect()
155 }
156
157 pub fn mode(&self) -> &'static str {
159 self.selected_plugin.mode()
160 }
161
162 pub fn mode_string(&self) -> String {
163 self.selected_plugin.mode_string()
164 }
165
166 pub fn set_mode(&mut self, mode: &String) -> Result<(), ChiptunomaticError> {
168 if let Some(plugin) = self.plugins.iter().find(|p| p.mode() == mode) {
169 self.selected_plugin = plugin.clone();
170 Ok(())
171 } else {
172 Err(ChiptunomaticError::InvalidMode(mode.clone()))
173 }
174 }
175
176 pub fn song_metadata_from_seed(
178 &self,
179 seed: &[u8],
180 data_byte_len: u64,
181 ) -> Result<SongMetadata, GenerateError> {
182 if data_byte_len == 0 {
183 return Err(GenerateError::EmptyInput);
184 }
185 let digest = md5::compute(seed).0;
186 let rng_seed = u64::from(
187 u32::from_be_bytes([digest[12], digest[13], digest[14], digest[15]]) % (1u32 << 31),
188 );
189
190 let root_seed = u32::from_be_bytes(
191 digest[0..4]
192 .try_into()
193 .map_err(|_| GenerateError::InvalidSeed)?,
194 );
195 let chord_seed = u32::from_be_bytes(
196 digest[4..8]
197 .try_into()
198 .map_err(|_| GenerateError::InvalidSeed)?,
199 );
200 let drum_pattern_seed: [u8; 8] = digest[8..16]
201 .try_into()
202 .map_err(|_| GenerateError::InvalidSeed)?;
203
204 let root = (root_seed % 12) as u8;
205 let bpm = self.selected_plugin.tempo_from_seed(root_seed);
206 let timing = Timing::from_bpm(bpm);
207 let total_beats = data_byte_len / 3;
208 let total_duration = (total_beats as f64) * timing.beat_duration;
209 let chord_progression = self.selected_plugin.chord_progression_from_seed(chord_seed);
210 let drum_pattern = self
211 .selected_plugin
212 .drum_pattern_from_seed(&drum_pattern_seed);
213 let chord_description = Self::build_chord_description(root, &chord_progression);
214
215 Ok(SongMetadata {
216 seed: digest.into(),
217 rng_seed,
218 root_semitone: root,
219 bpm,
220 total_beats,
221 total_duration,
222 total_duration_str: Self::format_duration(total_duration),
223 chord_progression,
224 chord_description,
225 timing,
226 data_byte_len,
227 data_byte_len_str: Self::format_data_byte_size(data_byte_len),
228 drum_pattern,
229 drum_seed: drum_pattern_seed,
230 })
231 }
232
233 pub fn load_song_metadata_from_seed(
235 &mut self,
236 seed: &[u8],
237 data_byte_len: u64,
238 ) -> Result<SongMetadata, GenerateError> {
239 self.song_metadata = self.song_metadata_from_seed(seed, data_byte_len)?;
240 Ok(self.song_metadata.clone())
241 }
242
243 pub fn song_metadata_from_string<S: Into<String>>(
244 &self,
245 s: S,
246 data_byte_len: u64,
247 ) -> Result<SongMetadata, GenerateError> {
248 self.song_metadata_from_seed(s.into().as_bytes(), data_byte_len)
249 }
250
251 pub fn load_song_metadata_from_string<S: Into<String>>(
252 &mut self,
253 s: S,
254 data_byte_len: u64,
255 ) -> Result<SongMetadata, GenerateError> {
256 self.load_song_metadata_from_seed(s.into().as_bytes(), data_byte_len)
257 }
258
259 #[cfg(all(
260 feature = "std",
261 not(all(target_arch = "wasm32", target_os = "unknown"))
262 ))]
263 pub fn song_metadata_from_path<P: std::convert::AsRef<std::path::Path>>(
264 &self,
265 path: P,
266 ) -> Result<SongMetadata, SongMetadataFromPathError> {
267 let path = path.as_ref();
268 let md = std::fs::metadata(path).map_err(SongMetadataFromPathError::Io)?;
269
270 let seed_name = path
271 .file_name()
272 .map(|os| os.to_string_lossy())
273 .unwrap_or_else(|| path.as_os_str().to_string_lossy());
274
275 self.song_metadata_from_string(seed_name, md.len())
276 .map_err(SongMetadataFromPathError::Generate)
277 }
278
279 #[cfg(all(
280 feature = "std",
281 not(all(target_arch = "wasm32", target_os = "unknown"))
282 ))]
283 pub fn load_song_metadata_from_path<P: std::convert::AsRef<std::path::Path>>(
284 &mut self,
285 path: P,
286 ) -> Result<SongMetadata, SongMetadataFromPathError> {
287 self.song_metadata = self.song_metadata_from_path(path)?;
288 Ok(self.song_metadata.clone())
289 }
290
291 pub fn song_note_reader(&self) -> SongNoteReader {
293 SongNoteReader::new(self.song_metadata.clone(), self.selected_plugin.clone())
294 }
295
296 #[cfg(feature = "std")]
297 pub fn read_song_notes<R: Read>(&self, reader: R) -> ReadSongNotes<R, SongNoteReader> {
299 ReadSongNotes::new(reader, self.song_note_reader())
300 }
301
302 pub fn drum_sample_generator<R: Rng + SeedableRng + 'static>(&self) -> DrumSampleGenerator {
304 DrumSampleGenerator::new(
305 self.song_metadata.clone(),
306 self.selected_plugin.clone(),
307 Rc::new(RandomWrapper::<R>::from_seed(self.song_metadata.rng_seed)),
308 )
309 .with_sample_rate(self.sample_rate)
310 }
311
312 pub fn sample_drum_steps<R: Rng + SeedableRng + 'static>(&self) -> SampleDrumSteps {
314 SampleDrumSteps::from_generator(
315 self.song_metadata.clone(),
316 self.drum_sample_generator::<R>(),
317 )
318 }
319
320 #[cfg(all(feature = "std", feature = "wav"))]
322 pub fn file_to_wav<IP: AsRef<Path>, OP: AsRef<Path>>(
323 &mut self,
324 input_path: IP,
325 output_path: OP,
326 sample_rate: u32,
327 mix_generator: MixGenerator,
328 ) -> Result<(), ConvertWavError> {
329 use std::{fs::File, io::BufWriter};
330
331 use hound::{SampleFormat, WavSpec, WavWriter};
332 use rand::rngs::StdRng;
333
334 self.load_song_metadata_from_path(&input_path)?;
335
336 let wav_spec = WavSpec {
337 channels: 1,
338 sample_rate: sample_rate,
339 bits_per_sample: 16,
340 sample_format: SampleFormat::Int,
341 };
342
343 let input_file = File::open(&input_path)?;
344 let output_file = File::create(&output_path)?;
345 let mut wav_writer = WavWriter::new(BufWriter::new(output_file), wav_spec)?;
346
347 let drum_samples = self.sample_drum_steps::<StdRng>();
349
350 for mix in self
352 .read_song_notes(input_file)
353 .samples()
354 .with_sample_rate(sample_rate)
355 .mix(drum_samples)
356 .with_mix_generator(mix_generator)
357 {
358 let mix = mix?;
359 let pcm = (mix.frequency * 32767.0).clamp(-32768.0, 32767.0) as i16;
360 wav_writer.write_sample(pcm)?;
361 }
362
363 wav_writer.finalize()?;
364 Ok(())
365 }
366
367 fn format_duration(secs: f64) -> String {
368 let total = secs.floor() as u64;
369 if total < 60 {
370 format!("{total}s")
371 } else {
372 let m = total / 60;
373 let s = total % 60;
374 format!("{m}:{s:02}")
375 }
376 }
377
378 fn format_data_byte_size(n: u64) -> String {
380 const KB: f64 = 1024.0;
381 const MB: f64 = KB * 1024.0;
382 const GB: f64 = MB * 1024.0;
383 let x = n as f64;
384 if n >= 1 << 30 {
385 format!("{:.2} G", x / GB)
386 } else if n >= 1 << 20 {
387 format!("{:.2} M", x / MB)
388 } else if n >= 1 << 10 {
389 format!("{:.2} K", x / KB)
390 } else {
391 format!("{n} B")
392 }
393 }
394
395 fn build_chord_description(root: u8, chord_progression: &[usize]) -> alloc::string::String {
396 chord_progression
397 .iter()
398 .map(|&d| {
399 let note_idx = (root as usize + usize::from(PENTATONIC_MINOR[d])) % 12;
400 NOTE_NAMES[note_idx].to_string()
401 })
402 .collect::<alloc::vec::Vec<_>>()
403 .join(" – ")
404 }
405}