commit d0d66c4e86547e6eed1160255c253b3d0ad62014 Author: Thomas Forgione Date: Sun Feb 15 17:43:34 2026 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..94d8c2d --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "wav" +version = "0.1.0" +dependencies = [ + "paste", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..049180a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "wav" +version = "0.1.0" +edition = "2021" + +[dependencies] +paste = "1.0.15" diff --git a/output.wav b/output.wav new file mode 100644 index 0000000..850db1f Binary files /dev/null and b/output.wav differ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..51de56a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,42 @@ +pub mod note; +pub mod piece; +pub mod wav; + +use std::fs::File; +use std::io::{self, BufWriter}; + +use note::notes::*; +use piece::{NoteEvent, Piece}; + +#[rustfmt::skip] +pub fn main() -> io::Result<()> { + let mut piece = Piece::new(120); + let mut time = 0.0; + + for _ in 0..4 { + piece.notes.push(NoteEvent::new(FS3, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(FS3, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(CS4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(FS3, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(D4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(FS3, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(CS4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(FS3, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(B4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(A4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(GS3, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(A4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(B4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(A4, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(GS3, time, 0.5)); time += 0.5; + piece.notes.push(NoteEvent::new(E3, time, 0.5)); time += 0.5; + } + + piece.notes.push(NoteEvent::new(FS3, time, 0.5)); + + let file = File::create("output.wav")?; + let mut writer = BufWriter::new(file); + piece.generate_wav(&mut writer)?; + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6e1f9f4 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +fn main() { + if let Err(e) = wav::main() { + eprintln!("error: {}", e); + } +} diff --git a/src/note.rs b/src/note.rs new file mode 100644 index 0000000..70071bd --- /dev/null +++ b/src/note.rs @@ -0,0 +1,102 @@ +#[derive(Copy, Clone)] +pub enum Key { + A, + B, + C, + D, + E, + F, + G, +} + +impl Key { + pub fn semitones(self) -> i32 { + match self { + Key::A => 0, + Key::B => 2, + Key::C => 3, + Key::D => 5, + Key::E => 7, + Key::F => 8, + Key::G => 10, + } + } +} + +#[derive(Copy, Clone)] +pub enum Modifier { + Flat, + Natural, + Sharp, +} + +impl Modifier { + pub fn semitones(self) -> i32 { + match self { + Modifier::Flat => -1, + Modifier::Natural => 0, + Modifier::Sharp => 1, + } + } +} + +#[derive(Copy, Clone)] +pub struct Note { + pub key: Key, + pub modifier: Modifier, + pub octave: i32, +} + +impl Note { + pub const fn new(key: Key, modifier: Modifier, octave: i32) -> Note { + Note { + key, + modifier, + octave, + } + } +} + +impl Note { + pub fn freq(self) -> f64 { + let semitones = self.key.semitones() + self.modifier.semitones(); + let n = semitones + ((self.octave as i32 - 4) * 12); + 440.0 * 2f64.powf(n as f64 / 12.0) + } +} + +#[rustfmt::skip] +pub mod notes { + use crate::note::{Key, Modifier, Note}; + use paste::paste; + + macro_rules! notes_octave { + ($oct:expr) => { + paste! { + pub const []: Note = Note::new(Key::C, Modifier::Natural, $oct); + pub const []: Note = Note::new(Key::C, Modifier::Sharp, $oct); + pub const []: Note = Note::new(Key::D, Modifier::Natural, $oct); + pub const []: Note = Note::new(Key::D, Modifier::Sharp, $oct); + pub const []: Note = Note::new(Key::E, Modifier::Natural, $oct); + pub const []: Note = Note::new(Key::F, Modifier::Natural, $oct); + pub const []: Note = Note::new(Key::F, Modifier::Sharp, $oct); + pub const []: Note = Note::new(Key::G, Modifier::Natural, $oct); + pub const []: Note = Note::new(Key::G, Modifier::Sharp, $oct); + pub const []: Note = Note::new(Key::A, Modifier::Natural, $oct); + pub const []: Note = Note::new(Key::A, Modifier::Sharp, $oct); + pub const []: Note = Note::new(Key::B, Modifier::Natural, $oct); + } + }; + } + + notes_octave!(0); + notes_octave!(1); + notes_octave!(2); + notes_octave!(3); + notes_octave!(4); + notes_octave!(5); + notes_octave!(6); + notes_octave!(7); + notes_octave!(8); + notes_octave!(9); +} diff --git a/src/piece.rs b/src/piece.rs new file mode 100644 index 0000000..7de789d --- /dev/null +++ b/src/piece.rs @@ -0,0 +1,74 @@ +use std::io::{self, Write}; + +use crate::note::*; +use crate::wav; + +pub struct NoteEvent { + pub note: Note, + pub start: f64, + pub duration: f64, +} + +impl NoteEvent { + pub fn new(note: Note, start: f64, duration: f64) -> NoteEvent { + NoteEvent { + note, + start, + duration, + } + } +} + +pub struct Piece { + pub notes: Vec, + pub bpm: u32, +} + +impl Piece { + pub fn new(bpm: u32) -> Piece { + Piece { bpm, notes: vec![] } + } + + pub fn generate_wav(&self, writer: &mut W) -> io::Result<()> { + let amplitude = 0.1; + let seconds_per_beat = 60.0 / self.bpm as f64; + let params = wav::Params::new(); + + let max_beat = self + .notes + .iter() + .map(|x| x.start + x.duration) + .max_by(|x, y| x.total_cmp(y)) + .unwrap_or(0.0); + + let total_samples = + (max_beat * seconds_per_beat * params.sampling_rate as f64).ceil() as u32; + + let data_size = total_samples * params.channels as u32 * params.bytes_per_sample as u32; + let total_size = 44 + data_size; + + wav::write_header(total_size, ¶ms, writer)?; + + writer.write_all(b"data")?; + writer.write_all(&data_size.to_le_bytes())?; + + for i in 0..total_samples { + let t = i as f64 / params.sampling_rate as f64; + let mut value: f64 = 0.0; + for event in &self.notes { + let start_time = event.start * seconds_per_beat; + let end_time = start_time + event.duration * seconds_per_beat; + if t >= start_time && t < end_time { + let freq = event.note.freq(); + let sin = (std::f64::consts::TAU * freq * (t - start_time)).sin(); + value += if sin >= 0.0 { 1.0 } else { -1.0 }; + } + } + + let sample = (value * (i16::MAX as f64) * amplitude).round() as i16; + writer.write_all(&sample.to_le_bytes())?; + } + + Ok(()) + } +} diff --git a/src/wav.rs b/src/wav.rs new file mode 100644 index 0000000..b79f8f0 --- /dev/null +++ b/src/wav.rs @@ -0,0 +1,59 @@ +use std::io::{self, Write}; + +pub struct Params { + pub sampling_rate: u32, + pub channels: u16, + pub bytes_per_sample: u16, +} + +impl Params { + pub fn new() -> Params { + Params { + sampling_rate: 44100, + channels: 1, + bytes_per_sample: 2, + } + } +} + +pub fn write_header(total_size: u32, p: &Params, writer: &mut W) -> io::Result<()> { + // Premier bloc : déclaration du fichier + // FileTypeBlocID (4 octets) : Constante « RIFF » (0x52,0x49,0x46,0x46) + writer.write_all(b"RIFF")?; + + // FileSize (4 octets) : Taille du fichier moins 8 octets + writer.write_all(&(total_size - 8).to_le_bytes())?; + + // FileSize (4 octets) : Taille du fichier moins 8 octets + writer.write_all(b"WAVE")?; + + // Deuxième bloc : format audio + // FormatBlocID (4 octets) : Identifiant « fmt␣ » (0x66,0x6D,0x74,0x20) + writer.write_all(b"fmt ")?; + + // BlocSize (4 octets) : Nombre d'octets du bloc moins 8 octets, soit ici 16 octets (0x10) + writer.write_all(&16_u32.to_le_bytes())?; + + // AudioFormat (2 octets) : Format du stockage dans le fichier (1: PCM entier, 3: PCM flottant, 65534: WAVE_FORMAT_EXTENSIBLE) + writer.write_all(&1_u16.to_le_bytes())?; + + // NbrCanaux (2 octets) : Nombre de canaux (de 1 à 6, cf. ci-dessous) + writer.write_all(&p.channels.to_le_bytes())?; + + // Frequence (4 octets) : Fréquence d'échantillonnage (en hertz) [Valeurs standardisées : 11 025, 22 050, 44 100 et éventuellement 48 000 et 96 000] + writer.write_all(&p.sampling_rate.to_le_bytes())?; + + // BytePerSec (4 octets) : Nombre d'octets à lire par seconde (c.-à-d., Frequence * BytePerBloc). + let byte_per_sec = p.sampling_rate * p.channels as u32 * p.bytes_per_sample as u32; + writer.write_all(&byte_per_sec.to_le_bytes())?; + + // BytePerBloc (2 octets) : Nombre d'octets par bloc d'échantillonnage (c.-à-d., tous canaux confondus : NbrCanaux * BitsPerSample/8). + let byte_per_block = p.channels * p.bytes_per_sample; + writer.write_all(&byte_per_block.to_le_bytes())?; + + // BitsPerSample (2 octets) : Nombre de bits utilisés pour le codage de chaque échantillon (8, 16, 24) + let bits_per_sample = p.bytes_per_sample * 8; + writer.write_all(&bits_per_sample.to_le_bytes())?; + + Ok(()) +}