forked from fluora/interval.rs
710 lines
25 KiB
Rust
710 lines
25 KiB
Rust
// 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 = ¬es[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(_) => (),
|
||
};
|
||
}
|
||
}
|