Initial commit
This commit is contained in:
commit
d0d66c4e86
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
16
Cargo.lock
generated
Normal file
16
Cargo.lock
generated
Normal 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
7
Cargo.toml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[package]
|
||||||
|
name = "wav"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
paste = "1.0.15"
|
||||||
BIN
output.wav
Normal file
BIN
output.wav
Normal file
Binary file not shown.
42
src/lib.rs
Normal file
42
src/lib.rs
Normal 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
5
src/main.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
fn main() {
|
||||||
|
if let Err(e) = wav::main() {
|
||||||
|
eprintln!("error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/note.rs
Normal file
102
src/note.rs
Normal 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
74
src/piece.rs
Normal 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, ¶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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
59
src/wav.rs
Normal file
59
src/wav.rs
Normal 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(())
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user