interval.rs/interval.rs

710 lines
25 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Interval.rs Music Maker v1.4
// Ellie, Eve, and Amelia Diode
// May 2019
// Public Domain (via the Unlicense)
// Interval is dedicated to tilde.town and all of its wonderful users. Thank you for being the best online community we
// have ever had the privilege of calling ourselves part of!
// ~♥
// Additionally, we would like to issue a special thanks to World's End Girlfriend, whose track "The Octuple Personality
// and Eleven Crows" contains the most iconic example of a 4:3 polyrhythm we have ever encountered and provided the
// original inspiration for creating Interval.
/*
Disclaimer:
Interval.rs is provided with the hope that it will be useful, but with absolutely no warranty at all, not even regarding
merchantability, fitness for a particular purpose, or safety. To the extent permitted by applicable law, the creator(s)
of Interval shall not be held liable for any damages, losses, or other undesirable outcomes resulting from the use of this
program, its source code, its compiled binary forms, or any data processed or produced by this program in any form.
*/
/*
Introduction:
Interval.rs is a terse, nonlinear, compiled language for composing music. Code is read from a `.itv` plaintext source file
and converted to PCM audio data, which is emitted to stdout by the compiler. Interval is designed with unconventional
music in mind, particularly that involving polyrhythms and microtonal tunings. Notes are defined by position in time,
pitch, and other parameters, and played using simple sine waves modulated by a gaussian envelope generator. Any number
of notes can be played simultaneously, and note definitions can be supplied out of the order in which they play. It is
also possible to modify the way the notes combine, producing effects such as tremolo, vibrato, and ring modulation.
*/
/*
How to Compile:
Interval.rs is written in Rust version 1.31.1, and depends only on Rust's stdlib.
With the Rust compiler installed under a Unix-like OS, you can compile this source file with this shell command:
`rustc -O interval.rs` to produce the `interval` binary.
How to Use:
Interval can be run at the command line with the following syntax:
`./interval [input file] [output file]`
The input file is an Interval source code file. The output file is a WAV sound file containing a signed 16-bit stereo audio
stream. If the output file is not specified, then it will have the same name as the input file, with its extension changed
to `.wav`.
Example Usage:
`./interval test.itv`
This will emit audio data into `test.wav`, creating it if it does not exist and overwriting it if it does.
*/
/*
ITV Source Code Syntax
Each line of the file defines one note.
Lines are in the following form:
`p[ratio] v[volume] t[time] r[rise] h[hold] f[fall] m[mode] d[phase];`
Parameters can be given in any order, and all parameters are optional.
For example:
`p1:1 v200 t0 r0 h0 f200;`
produces a bell-like tone, starting immediately when the program starts and ringing with a time constant of 200 milliseconds.
Lines do not need to be complete; lines with missing parameters will recycle the values for those parameters from the
previous line. If no lines define one or more parameters, then defaults will be used, with the file treated as though it
began with the line `~r1:1 v200 t0 r0 h0 f200 c0 d0 b0 m0;`.
Note parameters are committed whenever a semicolon (;) is encountered. Under normal conditions, this will result in a note
being played with those parameters. If a line begins with a tilde (~), then the note will not play, although its parameters
will still be entered. You can use this for lines that define or modify a parameter that will be used by subsequent notes -
for instance, if you want to raise the overall tempo by a factor of 4:3, you could issue `~T4:3*;` which will adjust the tempo
parameter without playing a note. Tildes can also be used to silence notes whose parameter settings are depended on by
following notes, and for which simply commenting them would cause problems.
Comments can be added with the # character. Multiline comments using /* */ are also supported.
- Detailed Explanation of Parameters -
p pitch : Pitch factor, multiple of the base frequency value.
v volume : Note amplitude between 0 and 1000. Be warned: if you set all your notes to 1000 and they overlap,
then they will clip and sound dreadful. Try starting with something like 200, and scaling it as needed.
t time : Time between the start of playback and the start of the note, in milliseconds - think of it as
a timestamp for when the note plays.
r rise : The time constant in milliseconds for the rise rate of the note's loudness. This program uses a simple
unsymmetric gaussian envelope, and this parameter is the σ parameter for the leading half of that gaussian.
Set this to a smaller value for a sharper sound.
h hold : Time in milliseconds to hold the note at constant volume before allowing it to fall.
f fall : Similar to rise, this is the width of the trailing half of the gaussian envelope, the σ parameter for the
decay of the note. Set this to a larger value for a longer ring time.
m mixing : Defines how a note should overlay onto other notes. Followed by a mode selection character, which can be one
of the following:
p pitch : Vary the pitch of the overlayed notes with this waveform, like FM synthesis.
Also useful for vibrato effects.
v volume : Vary the volume of the overlayed notes with this waveform. Useful for tremolo effects.
x ring : Multiply the waveform of this note by the overlayed notes, in the manner of a ring modulator.
After the mode selecton character, you can also specify an integer "range" parameter, which determines
how many of the subsequently-defined notes will be affected by the overlay. If unspecified, this parameter
will default to 0, which causes it to affect all notes in the file. This parameter can also be negative,
in which case it will affect the notes that come before it instead.
b balance : The relative loudness of the note between the left and right channels, in degrees. Positive values will make
the note louder on the right, while negative values will make it louder on the left.
c com. phase : The phase offset of the waveform defined by the note that is the same between the left and right channels,
given in degrees. Useful when mixing notes of very low frequency, but will have little effect on typical
notes.
d diff.phase : The relative phase offset between the left and right channels in degrees. Can potentially be used to adjust
the apparent location of a sound to a listener. Positive values will make the right side lead the left
side, negative values vice versa.
T tempo : Overall tempo factor for notes (a percentage). This parameter can be changed using relative operators, but
unlike others, operator changes do not automatically re-apply with each subsequent note - the value always
remains constant unless it is explicitly changed. The tempo factor scales the notes' timing values and
envelope parameters, but not their pitches.
P base pitch : Base scaling factor for pitches, in Hz. Defaults to 440 Hz. Like tempo, this parameter does not change
automatically.
Every time a note is played, the given parameters will be set again, including operators. This means that if you give
`p3:2*` for a pitch parameter on a note, then play three more notes with no pitch parameter supplied, then each subsequent
note will have a pitch one fifth higher than the previous one. If you want to simply play the same pitch again and not
continue to change it, issue `p1*` or `p0+` to indicate that the pitch should use the value of the previous note and
stop changing.
- Relative Mode Operators -
Pitch ratios, volumes, and times can have operators appended to them to make these values depend on the corresponding value
of the previous note. For example, if you wanted one note come 400 milliseconds after one that plays at the 3-second mark,
you could do this:
`p1:1 t3000;
p2:1 t400+;`
The list of allowed operators is as follows:
+ : Adds the value to the previous one. For pitch, this calculates the frequency of the given interval against the base
pitch, then adds the frequency directly to the previous frequency. If you want to change the pitch by an interval
ratio, you almost certainly want * instead.
- : Subtracts the value from the previous one.
* : Multiplies the value by the previous one.
/ : Divides the value by the previous one.
^ : Multiplies the previous value by 2 raised to the power of this value. Hint: use this to raise the pitch of a note by a
given number of cents: `p211:1200^` will raise the pitch by 211 cents.
_ : Divides the previous value by 2 raised to the power of this value. Can be used to lower a pitch by a number of cents.
Any value without an operator is taken as an absolute value instead.
- Loops -
Loops can be added using the syntax `R[iteration count] { [notes to be repeated] }`. For example, to play a note five times
in succession, you could use this:
`R5 {
p1:1 t300+;
}`
Loops are equivalent to simply writing out the same sequence of notes multiple times. The same changes to parameters that are
specified absolutely or using relative mode operators will be applied repeatedly.
Note that loops need not necessarily iterate in time - you can also iterate pitch, volume, rise time, fall time, base
frequency, and base tempo.
Tricks:
- Notes do not have to be defined in the same order as they are to be played. Use this to your advantage for easier
polyrhythms! Parameters are set and inherited in the order they are defined in the file, which is not the same as
the order they play in.
- Relative time offsets can be negative, resulting in notes that play before the note they follow in the file. Timestamps
that go below zero will simply push back the beginning of the file.
- Loudness values can be set above 1000 (this will cause the notes to clip, but maybe you want that!)
- Ratios can be arbitrarily large, or non-integers. Go nuts with microtones!
*/
static RATE:f64 = 48000.0; // audio sample rate in samples/sec
static TAUS:f64 = 5.0; // number of time contants to allow for notes to finish ringing before ending the output stream
use std::io::prelude::*;
use std::env::args;
use std::f64::consts::PI;
use std::fs::File;
fn i16_to_bytes(number:&i16) -> [u8;2] {
let mut bytes:[u8;2] = [0;2];
for x in 0..2 {
bytes[x] = ((*number >> (8*x)) & 0xFF) as u8;
}
return bytes;
}
fn u16_to_bytes(number:&u16) -> [u8;2] {
let mut bytes:[u8;2] = [0;2];
for x in 0..2 {
bytes[x] = ((*number >> (8*x)) & 0xFF) as u8;
}
return bytes;
}
fn u32_to_bytes(number:&u32) -> [u8;4] {
let mut bytes:[u8;4] = [0;4];
for x in 0..4 {
bytes[x] = ((*number >> (8*x)) & 0xFF) as u8;
}
return bytes;
}
fn operate(oper:char,arg1:f64,arg2:f64,scale:f64) -> f64 {
match oper {
'*' => {
return arg1*arg2;
},
'/' => {
return arg1/arg2;
},
'+' => {
return arg1+arg2*scale;
},
'-' => {
return arg1-arg2*scale;
},
'^' => {
return arg1*arg2.exp2();
},
'_' => {
return arg1/arg2.exp2();
},
_ => {
return arg2*scale;
},
};
}
#[derive(Clone)]
struct Note {
pitch:f64,
louds:f64,
atime:f64,
riset:f64,
holdt:f64,
fallt:f64,
fbase:f64,
tempo:f64,
cphas:f64,
dphas:f64,
balnc:f64,
amode:char,
range:i64
}
struct Loop {
iters:usize,
conts:Vec<Vec<String>>,
}
fn main() {
// parse CLI arguments and read the file we're after; gripe if something breaks
let argv = args().collect::<Vec<String>>();
if argv.len() < 2 {
eprintln!("Usage: interval [input file] [output file] | aplay");
return;
}
let notefilename = &argv[1];
let outfilename:&str;
if argv.len() > 2 {
outfilename = &argv[2];
} else {
outfilename = "";
}
let mut notefile = match File::open(notefilename) {
Err(why) => {
eprintln!("Failed to open file {}: {}",notefilename,why);
return;
},
Ok(f) => f,
};
let mut notestring = String::new();
match notefile.read_to_string(&mut notestring) {
Err(why) => {
eprintln!("Failed to read file {}: {}",notefilename,why);
return;
},
Ok(_) => (),
};
let opers:&[_] = &['+','-','*','/','^','_'];
let params:&[_] = &['p','v','t','r','f','T','P','h','m','c','d','b','~'];
let nonums:&[_] = &['m'];
let loopcs:&[_] = &['R','{','}'];
let mut notelines:Vec<Vec<String>> = vec![];
let mut loops:Vec<Loop> = vec![];
let mut reps:usize = 1;
let mut commented:u8 = 0; // 0-not commented; 1-single line comment; 2-multiline comment
for statement in notestring.trim().split_terminator(";") {
let mut newnote:Vec<String> = vec![];
for line in statement.lines() {
if commented == 1 {
commented = 0;
}
for part in line.split_whitespace() {
if commented == 0 {
if part.starts_with("#") {
commented = 1;
} else if part.starts_with("/*") {
commented = 2;
}
} else if commented == 2 {
if part.ends_with("*/") {
commented = 0;
}
}
if commented != 0 {
continue;
}
if part.starts_with("R") {
reps = match part.trim_matches(loopcs).parse::<usize>() {
Err(_) => {
eprintln!(" {:3}n!# {}",part.trim_matches(loopcs),line);
continue;
},
Ok(n) => n,
};
}
if part.ends_with("{") || part.starts_with("{") {
loops.push(Loop {
iters:reps,
conts:vec![],
});
} else if part.starts_with("}") || part.ends_with("}") {
if let Some(loope) = loops.pop() {
match loops.last_mut() {
Some(outer) => {
for _ in 0..loope.iters {
outer.conts.append(&mut loope.conts.clone());
}
},
None => {
for _ in 0..loope.iters {
notelines.append(&mut loope.conts.clone());
}
},
};
}
}
if part.starts_with(params) {
newnote.push(part.to_owned());
} else if !part.starts_with(loopcs) && !part.ends_with(loopcs) {
eprintln!(" {:3}?!# {}",part.chars().collect::<Vec<char>>()[0],line);
}
}
}
if let Some(loope) = loops.last_mut() {
loope.conts.push(newnote);
} else if commented == 0 {
notelines.push(newnote);
}
}
// parse note definitions from file
let mut notes:Vec<Note> = vec![]; // notes are of the form "numerator:denominator amplitude atime rise fall"
let mut note = Note {
pitch:440.0,
louds:200.0,
atime:0.0,
riset:0.0,
holdt:0.0,
fallt:200.0,
tempo:100.0,
fbase:440.0,
cphas:0.0,
dphas:0.0,
balnc:0.0,
amode:' ',
range:0,
};
let mut param_opers:[char;9] = [' ',' ',' ',' ',' ',' ',' ',' ',' '];
let mut param_valus:[f64;9] = [440.0,200.0,0.0,0.0,0.0,200.0,0.0,0.0,0.0];
/*
0 pitch
1 louds
2 atime
3 riset
4 holdt
5 fallt
6 cphas
7 dphas
8 balnc
*/
let mut oper:char;
let mut rparts:Vec<&str>;
let mut realnote:bool = true;
let mut numer:f64 = 1.0;
let mut denom:f64 = 1.0;
'lines:for line in notelines.iter() {
if line.join(" ").starts_with("~") {
realnote = false;
}
'args:for part in line.iter() {
let firstchar = part.trim_matches('~').chars().collect::<Vec<char>>()[0];
if !nonums.contains(&firstchar) {
rparts = part.trim_matches(opers).trim_matches(params).split(":").collect::<Vec<&str>>();
numer = match rparts[0].parse::<f64>() {
Err(_) => {
eprintln!(" {:3}n!# {}",rparts[0],line.join(" "));
continue 'args;
},
Ok(n) => n,
};
if rparts.len() >= 2 {
denom = match rparts[1].parse::<f64>() {
Err(_) => {
eprintln!(" {:3}n!# {}",rparts[1],line.join(" "));
continue 'args;
},
Ok(n) => n,
};
} else {
denom = 1.0;
}
if denom == 0.0 {
eprintln!(" {:3}0!# {}",rparts[0],line.join(" "));
continue 'args;
}
}
if part.ends_with(opers) {
oper = part.chars().last().unwrap_or(' ');
} else {
oper = ' ';
}
match firstchar {
'm' => {
if let Some(c) = part.trim_matches('m').trim_matches(opers).chars().nth(0) {
note.amode = c;
note.range = match part.trim_matches('m').trim_matches(c).parse::<i64>() {
Err(_) => 0,
Ok(n) => n,
};
}
},
'T' => {
note.tempo = operate(oper,note.tempo,numer/denom,1.0);
},
'P' => {
note.fbase = operate(oper,note.fbase,numer/denom,1.0);
},
'p' => {
param_opers[0] = oper;
param_valus[0] = numer/denom;
},
'v' => {
param_opers[1] = oper;
param_valus[1] = numer/denom;
},
't' => {
param_opers[2] = oper;
param_valus[2] = numer/denom;
},
'r' => {
param_opers[3] = oper;
param_valus[3] = numer/denom;
},
'h' => {
param_opers[4] = oper;
param_valus[4] = numer/denom;
},
'f' => {
param_opers[5] = oper;
param_valus[5] = numer/denom;
},
'c' => {
param_opers[6] = oper;
param_valus[6] = numer/denom;
},
'd' => {
param_opers[7] = oper;
param_valus[7] = numer/denom;
},
'b' => {
param_opers[8] = oper;
param_valus[8] = numer/denom;
},
_ => (),
};
}
// finished accumulating note parameters; now we process the note value changes.
note.pitch = operate(param_opers[0],note.pitch,param_valus[0],note.fbase);
note.louds = operate(param_opers[1],note.louds,param_valus[1],1.0);
note.atime = operate(param_opers[2],note.atime,param_valus[2],1.0);
note.riset = operate(param_opers[3],note.riset,param_valus[3],1.0);
note.holdt = operate(param_opers[4],note.holdt,param_valus[4],1.0);
note.fallt = operate(param_opers[5],note.fallt,param_valus[5],1.0);
note.cphas = operate(param_opers[6],note.cphas,param_valus[6],1.0);
note.dphas = operate(param_opers[7],note.dphas,param_valus[7],1.0);
note.balnc = operate(param_opers[8],note.balnc,param_valus[8],1.0);
if realnote {
notes.push(note.clone());
} else {
realnote = true;
}
note.amode = ' ';
note.range = 0;
}
let mut overlays:Vec<(usize,Note)> = vec![];
let mut realnotes:Vec<Note> = vec![];
for i in 0..notes.len() {
if notes[i].amode != ' ' {
overlays.push((i,notes[i].clone()));
} else {
realnotes.push(notes[i].clone());
}
}
// generate actual sounds using note parameters
let mut sound:Vec<(f64,f64)> = vec![];
let mut skew:f64 = 0.0;
for i in 0..realnotes.len() {
let note = &notes[i];
if note.pitch == 0.0 || note.tempo == 0.0 {
continue;
}
eprintln!("p: {:6}Hz | v: {:5}% | t: {:6}ms | r: {:5}ms | h: {:5}ms | f: {:5}ms | c: {:3}° | d: {:3}° | b: {:3}° | m: {}{}",
(note.pitch*10.0).round()/10.0,note.louds.round()/10.0,(note.atime*(note.tempo/100.00)).round(),note.riset.round(),note.holdt.round(),
note.fallt.round(),note.cphas.round(),note.dphas.round(),note.balnc.round(),note.amode,note.range);
let mut activeoverlays:Vec<Note> = vec![];
for overlay in overlays.iter() {
if overlay.1.range == 0 {
activeoverlays.push(overlay.1.clone());
} else if overlay.1.range > 0 {
if overlay.0+(overlay.1.range as usize) >= i && overlay.0 <= i {
activeoverlays.push(overlay.1.clone());
}
} else if overlay.1.range < 0 {
if (overlay.0 as i64)-(overlay.1.range*-1) <= i as i64 && overlay.0 >= i {
activeoverlays.push(overlay.1.clone());
}
}
}
let ar = note.riset*RATE/(10.0*note.tempo);
let rr = note.fallt*RATE/(10.0*note.tempo);
let start = (-1.0*ar*TAUS) as i64;
let startf = start as f64;
let end = ((note.holdt/(10.0*note.tempo))*RATE+rr*TAUS) as i64;
let endf = end as f64;
let mut neededmax = ((endf-startf)+RATE*((note.atime+skew)/(note.tempo*10.0))) as isize;
let mut neededmin = (RATE*((note.atime+skew)/(note.tempo*10.0))) as isize;
if 0 > neededmin {
let mut new:Vec<(f64,f64)> = vec![];
new.resize((0-neededmin) as usize,(0.0,0.0));
new.append(&mut sound);
sound = new;
skew -= note.atime;
}
if neededmax > 0 && sound.len() < (neededmax as usize) {
sound.resize(neededmax as usize,(0.0,0.0));
}
for t in start..end {
let tf = t as f64;
let mut vwaves = (1.0,1.0); // amplitude factor, not clamped
let mut vlouds = (1.0,1.0); // amplitude factor, clamped above 0
let mut vpitch = (0.0,0.0); // relative frequency delta, clamped above -1
for overlay in activeoverlays.iter() {
let oar = overlay.riset*RATE/(10.0*overlay.tempo);
let orr = overlay.fallt*RATE/(10.0*overlay.tempo);
let overskew = (note.atime-overlay.atime)*RATE/(10.0*overlay.tempo);
let ramp;
if t < 0 {
ramp = (-0.5*((tf+overskew)/oar).powi(2)).exp();
} else if tf+overskew > overlay.holdt*RATE/(10.0*overlay.tempo) {
ramp = (-0.5*((tf+overskew+overlay.holdt/1000.0)/orr).powi(2)).exp();
} else {
ramp = 1.0;
}
let mut ophas:(f64,f64) = (0.0,0.0);
ophas.0 = overlay.cphas+overlay.dphas/2.0;
ophas.1 = overlay.cphas-overlay.dphas/2.0;
let mut overb:(f64,f64) = (0.0,0.0);
overb.0 = ((0.0-overlay.balnc)*PI/180.0).sin()/2.0+1.0;
overb.1 = ((0.0+overlay.balnc)*PI/180.0).sin()/2.0+1.0;
match overlay.amode {
'v' => { // modulate volume
vlouds.0 += overb.0*ramp*(overlay.louds/1000.0)*(((tf-overskew)*overlay.pitch*2.0*PI/RATE)-(ophas.0*PI/180.0)).sin();
vlouds.1 += overb.1*ramp*(overlay.louds/1000.0)*(((tf-overskew)*overlay.pitch*2.0*PI/RATE)-(ophas.1*PI/180.0)).sin();
},
'p' => { // modulate pitch (
vpitch.0 += overb.0*ramp*(overlay.louds/1000.0)*(((tf-overskew)*overlay.pitch*2.0*PI/RATE)-(ophas.0*PI/180.0)).cos();
vpitch.1 += overb.1*ramp*(overlay.louds/1000.0)*(((tf-overskew)*overlay.pitch*2.0*PI/RATE)-(ophas.1*PI/180.0)).cos();
},
'x' => { // ring modulation
vwaves.0 *= overb.0*ramp*(overlay.louds/1000.0)*(((tf-overskew)*overlay.pitch*2.0*PI/RATE)-(ophas.0*PI/180.0)).sin();
vwaves.1 *= overb.1*ramp*(overlay.louds/1000.0)*(((tf-overskew)*overlay.pitch*2.0*PI/RATE)-(ophas.1*PI/180.0)).sin();
},
_ => (),
};
}
if vlouds.0 < 0.0 {
vlouds.0 = 0.0;
}
if vlouds.1 < 0.0 {
vlouds.1 = 0.0;
}
if vpitch.0 < -1.0 {
vpitch.0 = -1.0;
}
if vpitch.1 < -1.0 {
vpitch.1 = -1.0;
}
// `ramp` is the relative envelope scaling factor at this position in time. it uses an asymmetric
// gaussian function.
let ramp;
if t < 0 {
ramp = (-0.5*(tf/ar).powi(2)).exp();
} else if tf > note.holdt*RATE/(10.0*note.tempo) {
ramp = (-0.5*((tf+note.holdt/1000.0)/rr).powi(2)).exp();
} else {
ramp = 1.0;
}
let mut phase:(f64,f64) = (0.0,0.0);
phase.0 = note.cphas-note.dphas/2.0;
phase.1 = note.cphas+note.dphas/2.0;
let mut loudb:(f64,f64) = (0.0,0.0);
loudb.0 = ((0.0-note.balnc)*PI/180.0).sin()/2.0+1.0;
loudb.1 = ((0.0+note.balnc)*PI/180.0).sin()/2.0+1.0;
let mut wave:(f64,f64) = (0.0,0.0);
wave.0 = loudb.0*vwaves.0*vlouds.0*ramp*(note.louds/1000.0)*((tf*note.pitch*2.0*PI/RATE)-(phase.0*PI/180.0)-2.0*note.pitch*vpitch.0).sin();
wave.1 = loudb.1*vwaves.1*vlouds.1*ramp*(note.louds/1000.0)*((tf*note.pitch*2.0*PI/RATE)-(phase.1*PI/180.0)-2.0*note.pitch*vpitch.1).sin();
let soundpos = ((tf-startf)+RATE*((note.atime+skew)/(note.tempo*10.0))) as usize;
sound[soundpos].0 += wave.0;
sound[soundpos].1 += wave.1;
}
}
// generate and emit a WAV RIFF header, to make this a true WAV file and tell aplay how to deal with it
let mut header:Vec<u8> = Vec::with_capacity(44);
header.append(&mut b"RIFF".to_vec()); // format info
header.append(&mut u32_to_bytes(&(sound.len() as u32 + 44)).to_vec()); // total file size
header.append(&mut b"WAVE".to_vec()); // format info
header.append(&mut b"fmt ".to_vec()); // magic
header.append(&mut u32_to_bytes(&16).to_vec()); // length of format data
header.append(&mut u16_to_bytes(&1).to_vec()); // 1 means PCM
header.append(&mut u16_to_bytes(&2).to_vec()); // channel count
header.append(&mut u32_to_bytes(&(RATE as u32)).to_vec()); // sample rate
header.append(&mut u32_to_bytes(&((RATE as u32)*4)).to_vec()); // equivalent to RATE*BITS*CHANNELS/8
header.append(&mut u16_to_bytes(&4).to_vec()); // 4 means 16-bit stereo
header.append(&mut u16_to_bytes(&16).to_vec()); // 16 bits per sample
header.append(&mut b"data".to_vec()); // magic
header.append(&mut u32_to_bytes(&((sound.len() as u32)*4)).to_vec()); // (n samples)*(2 bytes per sample)*(2 channels)
assert_eq!(header.len(),44);
let outname:String;
if outfilename == "" {
outname = format!("{}.wav",notefilename.split(".").collect::<Vec<&str>>()[0]);
} else {
outname = outfilename.to_owned();
}
let mut outfile = match File::create(&outname) {
Err(why) => {
eprintln!("Failed to open {} for writing: {}",outname,why);
return;
},
Ok(f) => f,
};
match outfile.write(&header) {
Err(_) => return,
Ok(_) => (),
};
for sample in sound.iter() {
let mut csample:(f64,f64) = (0.0,0.0);
if sample.0 > 1.0 {
csample.0 = 1.0;
} else if sample.0 < -1.0 {
csample.0 = -1.0;
} else {
csample.0 = sample.0;
}
if sample.1 > 1.0 {
csample.1 = 1.0;
} else if sample.1 < -1.0 {
csample.1 = -1.0;
} else {
csample.1 = sample.1;
}
let mut isample:(i16,i16) = (0,0);
isample.0 = (csample.0*32767.0).round() as i16;
isample.1 = (csample.1*32767.0).round() as i16;
// left channel
match outfile.write(&i16_to_bytes(&isample.0)) {
Err(_) => break,
Ok(_) => (),
};
// right channel
match outfile.write(&i16_to_bytes(&isample.1)) {
Err(_) => break,
Ok(_) => (),
};
}
}