Initial commit

This commit is contained in:
Thomas Forgione 2026-02-15 17:43:34 +01:00
commit d0d66c4e86
9 changed files with 306 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

16
Cargo.lock generated Normal file
View File

@ -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",
]

7
Cargo.toml Normal file
View File

@ -0,0 +1,7 @@
[package]
name = "wav"
version = "0.1.0"
edition = "2021"
[dependencies]
paste = "1.0.15"

BIN
output.wav Normal file

Binary file not shown.

42
src/lib.rs Normal file
View File

@ -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(())
}

5
src/main.rs Normal file
View File

@ -0,0 +1,5 @@
fn main() {
if let Err(e) = wav::main() {
eprintln!("error: {}", e);
}
}

102
src/note.rs Normal file
View File

@ -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 [<C $oct>]: Note = Note::new(Key::C, Modifier::Natural, $oct);
pub const [<CS $oct>]: Note = Note::new(Key::C, Modifier::Sharp, $oct);
pub const [<D $oct>]: Note = Note::new(Key::D, Modifier::Natural, $oct);
pub const [<DS $oct>]: Note = Note::new(Key::D, Modifier::Sharp, $oct);
pub const [<E $oct>]: Note = Note::new(Key::E, Modifier::Natural, $oct);
pub const [<F $oct>]: Note = Note::new(Key::F, Modifier::Natural, $oct);
pub const [<FS $oct>]: Note = Note::new(Key::F, Modifier::Sharp, $oct);
pub const [<G $oct>]: Note = Note::new(Key::G, Modifier::Natural, $oct);
pub const [<GS $oct>]: Note = Note::new(Key::G, Modifier::Sharp, $oct);
pub const [<A $oct>]: Note = Note::new(Key::A, Modifier::Natural, $oct);
pub const [<AS $oct>]: Note = Note::new(Key::A, Modifier::Sharp, $oct);
pub const [<B $oct>]: 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);
}

74
src/piece.rs Normal file
View File

@ -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<NoteEvent>,
pub bpm: u32,
}
impl Piece {
pub fn new(bpm: u32) -> Piece {
Piece { bpm, notes: vec![] }
}
pub fn generate_wav<W: Write>(&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, &params, 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(())
}
}

59
src/wav.rs Normal file
View File

@ -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<W: Write>(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(())
}