Skip to main content

chiptunomatic/
chiptunomatic.rs

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    /// Initialize with the default plugin
101    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(LofiPlugin {}));
133        self.register_plugin(Rc::new(RockPlugin {}));
134        self.register_plugin(Rc::new(MetalPlugin {}));
135        // self.register_plugin(Rc::new(PersianPlugin {}));
136        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        // self.register_plugin(Rc::new(MedievalPlugin {}));
142    }
143
144    pub fn plugin(&self) -> Rc<dyn Plugin> {
145        self.selected_plugin.clone()
146    }
147
148    /// Return the possible modes
149    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    /// Get the selected mode
158    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    /// Set the selected mode
167    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    /// Builds [`SongMetadata`] from a bytes seed and stream byte length.
177    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    /// Builds [`SongMetadata`] from a bytes seed and stream byte length.
234    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    /// Return a reader for the notes of a byte stream
292    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    /// Iterate over the notes of a byte stream
298    pub fn read_song_notes<R: Read>(&self, reader: R) -> ReadSongNotes<R, SongNoteReader> {
299        ReadSongNotes::new(reader, self.song_note_reader())
300    }
301
302    /// Return a generator for the drum samples
303    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    /// Iterate over the samples of the drum steps
313    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    /// Convert a file to WAV. `mode` selects chiptune or lofi synthesis.
321    #[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        // This iterator generates the drum samples on demand using StdRng
348        let drum_samples = self.sample_drum_steps::<StdRng>();
349
350        // This generates the song samples and mix them with the drum notes
351        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    /// Formats byte length using `K` / `M` / `G` at 1024-based thresholds (`>= 1 KiB`, etc.).
379    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}