initial commit of version 1.2

This commit is contained in:
Ellie D 2019-05-16 12:47:19 -05:00
commit 114a46ac82
5 changed files with 684 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/interval

24
LICENSE Normal file
View File

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org>

146
README.md Normal file
View File

@ -0,0 +1,146 @@
#Interval Music Maker v1.2
###Ellie Diode, May 2019
###Public Domain
##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 ringing duration, 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.
###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]`
This will produce a WAV RIFF header followed by a PCM audio stream to stdout, which can then be piped into a file, audio
device, or other audio stream acceptor. If you don't pipe it into anything and it dumps the stream out onto the console,
then it will probably flood your screen with garbage text, so be warned.
###Example Usage
Under Linux with ALSA, you can compile and play a .ITV source file with a command like this:
`./interval test.itv | aplay`
###Writing to Files
You can pipe the output of the program into a file:
`./interval test.itv > test.wav`
This file will be in standard WAV format, and can be converted to other formats with a conversion tool such as
avconv or ffmpeg.
##ITV Source Code Syntax
Each line of the file defines one note.
Lines are in the following form (in any order):
`p[ratio] v[volume] t[time] r[rise] f[fall]`
For example, `p1:1 v200 t0 r0 f2000;` produces a bell-like tone, starting immediately when the program starts and ringing
with a time constant of 2 seconds.
Lines do not need to be complete; lines with missing parameters will recycle the values for those parameters from the
previous line. If the first line is missing one or more parameters, then the defaults will be used, which are as follows:
`r1:1 v200 t0 r0 f1000`
Notes are played with whatever parameters are set whenever a semicolon is placed. Use semicolons to terminate lines that
describe audible notes. Lines that do not end in semicolons will simply set the parameters given without playing a note.
You can define a relative tempo percentage with a line with a single argument in the form `T[tempo]` (e.g. `T100`), and
a base pitch with a line in the form `P[frequency]` (e.g. `P440`). Tempo and base frequency can be set anywhere, including
on the same line as a note (in which case they will apply to that note). They can be set an arbitrary number of times.
Comments can be added with the # character. Multiline comments using /* */ are also supported.
###Detailed Explanation of Parameters
####p pitch
Frequency ratio relative to 440 Hz, in the form 'numerator:denominator'.
####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.
####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.
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 v200 t3000;
p2:1 v200 t400+;`
The list of allowed operators is as follows:
####+ Plus
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.
####- Minus
Subtracts the value from the previous one.
####* Asterisk
Multiplies the value by the previous one.
####/ Forward Slash
Divides the value by the previous one.
####^ Caret
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.
####_ Underscore
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 A440 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! You can even set relative tempos and base pitches out-of-order.
- Relative time offsets can be negative, resulting in notes that play before the note they follow in 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!

18
example.itv Normal file
View File

@ -0,0 +1,18 @@
# A little polyrhythm jingle for testing interval.rs!
# Ellie Diode, May 2019
# Public Domain
T100
P440
p1:1 v200 t0 r0 f4000;
p4:3 t800+;
p3:2; #comment comment comment
p2:1;
T4:3*
p2:1 v200 t0 r0 f2000;
p8:3 t800+;
p3:1;
p4:1;
p5:1;

495
interval.rs Normal file
View File

@ -0,0 +1,495 @@
// Interval Music Maker v1.2
// Ellie Diode, May 2019
// Public Domain (via the Unlicense)
/*
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 ringing duration, 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.
*/
/*
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]`
This will produce a WAV RIFF header followed by a PCM audio stream to stdout, which can then be piped into a file, audio
device, or other audio stream acceptor. If you don't pipe it into anything and it dumps the stream out onto the console,
then it will probably flood your screen with garbage text, so be warned.
Example Usage:
Under Linux with ALSA, you can compile and play a .ITV source file with a command like this:
`./interval test.itv | aplay`
Writing to Files:
You can pipe the output of the program into a file:
`./interval test.itv > test.wav`
This file will be in standard WAV format, and can be converted to other formats with a conversion tool such as
avconv or ffmpeg.
*/
/*
ITV Source Code Syntax
Each line of the file defines one note.
Lines are in the following form (in any order):
`p[ratio] v[volume] t[time] r[rise] f[fall]`
For example:
`p1:1 v200 t0 r0 f2000;`
produces a bell-like tone, starting immediately when the program starts and ringing with a time constant of 2 seconds.
Lines do not need to be complete; lines with missing parameters will recycle the values for those parameters from the
previous line. If the first line is missing one or more parameters, then the defaults will be used, which are as follows:
`r1:1 v200 t0 r0 f1000`
Notes are played with whatever parameters are set whenever a semicolon is placed. Use semicolons to terminate lines that
describe audible notes. Lines that do not end in semicolons will simply set the parameters given without playing a note.
You can define a relative tempo percentage with a line with a single argument in the form `T[tempo]` (e.g. `T100`), and
a base pitch with a line in the form `P[frequency]` (e.g. `P440`). Tempo and base frequency can be set anywhere, including
on the same line as a note (in which case they will apply to that note). They can be set an arbitrary number of times.
Comments can be added with the # character. Multiline comments using /* */ are also supported.
- Detailed Explanation of Parameters -
p pitch : Frequency ratio relative to 440 Hz, in the form 'numerator:denominator'.
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.
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.
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 v200 t3000;
p2:1 v200 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 A440 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! You can even set relative tempos and base pitches out-of-order.
- Relative time offsets can be negative, resulting in notes that play before the note they follow in 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!
*/
// you can modify these all you like!
static RATE:u64 = 44100; // 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::io::stdout;
use std::env::args;
use std::f64::consts::PI;
use std::fs::File;
use std::collections::VecDeque;
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,
fallt:f64,
fbase:f64,
tempo:f64,
}
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] | aplay -rcd");
return;
}
let mut notefile = match File::open(&argv[1]) {
Err(why) => {
eprintln!("Failed to open file {}: {}",argv[1],why);
return;
},
Ok(f) => f,
};
let mut notestring = String::new();
match notefile.read_to_string(&mut notestring) {
Err(why) => {
eprintln!("Failed to read file {}: {}",argv[1],why);
return;
},
Ok(_) => (),
};
let opers:&[_] = &['+','-','*','/','^','_'];
let params:&[_] = &['p','v','t','r','f','T','P'];
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());
}
}
}
if let Some(loope) = loops.last_mut() {
loope.conts.push(newnote);
} else {
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,
fallt:0000.0,
tempo:100.0,
fbase:440.0,
};
let mut param_opers:[char;5] = [' ';5];
let mut param_valus:[f64;5] = [440.0,200.0,0.0,0.0,2000.0];
let mut oper:char;
let mut rparts:Vec<&str>;
let mut numer:f64;
let mut denom:f64;
'lines:for line in notelines.iter() {
'args:for part in line.iter() {
let firstchar = part.chars().collect::<Vec<char>>()[0];
if firstchar == '#' {
break 'args;
}
if params.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 'lines;
},
Ok(n) => n,
};
if rparts.len() >= 2 {
denom = match rparts[1].parse::<f64>() {
Err(_) => {
eprintln!(" {:3}n!# {}",rparts[1],line.join(" "));
continue 'lines;
},
Ok(n) => n,
};
} else {
denom = 1.0;
}
if part.ends_with(opers) {
oper = part.chars().last().unwrap_or(' ');
} else {
oper = ' ';
}
match firstchar {
'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;
},
'f' => {
param_opers[4] = oper;
param_valus[4] = numer/denom;
},
_ => (),
};
} else {
eprintln!(" {:3}?!# {}",firstchar,line.join(" "));
}
}
// 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.fallt = operate(param_opers[4],note.fallt,param_valus[4],1.0);
notes.push(note.clone());
}
let mut sound:VecDeque<i16> = VecDeque::new();
let mut skew:f64 = 0.0;
// generate actual sounds using note parameters
for note in notes.iter() {
if note.pitch == 0.0 || note.tempo == 0.0 {
continue;
}
eprintln!("pitch: {:6}Hz | vol: {:5}% | time: {:6}ms | rise: {:6}ms | fall: {:6}ms",
(note.pitch*10.0).round()/10.0,note.louds.round()/10.0,(note.atime*note.tempo/100.00).round(),note.riset.round(),note.fallt.round());
let ar = note.riset*(RATE as f64)/10000.0;
let rr = note.fallt*(RATE as f64)/10000.0;
let start = (-1.0*ar*TAUS) as i64;
let end = (rr*TAUS) as i64;
let mut neededmax = (((end-start) as f64)+(RATE as f64)*((note.atime+skew)/(note.tempo*10.0))) as isize;
let mut neededmin = ((RATE as f64)*((note.atime+skew)/(note.tempo*10.0))) as isize;
if 0 > neededmin {
let mut new:VecDeque<i16> = VecDeque::new();
new.resize((0-neededmin) as usize,0);
new.append(&mut sound);
sound = new;
skew -= note.atime;
}
if neededmax > 0 && sound.len() < (neededmax as usize) {
sound.resize(neededmax as usize,0);
}
for t in start..end {
// `wave` is the value of the waveform at this position in time. it uses a simple sine function.
let wave = (((t as f64)*note.pitch*2.0*PI)/((RATE as f64))).sin();
// `ramp` is the relative envelope scaling factor at this position in time. it uses an asymmetric
// gaussian function.
let mut ramp;
if t < 0 {
ramp = (-0.5*((t as f64)/ar).powi(2)).exp();
} else if t > 0 {
ramp = (-0.5*((t as f64)/rr).powi(2)).exp();
} else {
ramp = 1.0;
}
let soundpos = (((t-start) as f64)+(RATE as f64)*((note.atime+skew)/(note.tempo*10.0))) as usize;
let mut newv = sound[soundpos] as f64 + 32.767*note.louds*wave*ramp;
if newv > 32767.0 {
newv = 32767.0;
} else if newv < -32767.0 {
newv = -32767.0;
}
sound[soundpos] = newv.round() as i16;
}
}
// 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);
// write the audio stream to stdout
let sout = stdout();
let mut handle = sout.lock();
match handle.write(&header) {
Err(_) => return,
Ok(_) => (),
};
for b in sound.iter() {
// left channel
match handle.write(&i16_to_bytes(&b)) {
Err(_) => break,
Ok(_) => (),
};
// right channel
match handle.write(&i16_to_bytes(&b)) {
Err(_) => break,
Ok(_) => (),
};
}
}