SFX Maker
The sound effect scripting language is a shorthand way to make practical sound effects.
Rules
1. | Node names begin with a # symbol. The last node is used as output. |
2. | Node outputs can be used as inputs for most parameters. |
Ex: a square wave modulated by a sine wave: #osc: SIN F 200 #out: SQR F #osc | |
3. | Combine inputs with reverse polish notation. Leftover terms are summed. |
Ex: (#osca*2)+(#oscb-1) becomes #out: SQR F #osca 2 * #oscb 1 - |
Symbol | Description | Parameters |
' | Line comment. | |
" | Block comment. Terminate with " | |
#bass: | Define a node named #bass. | |
#bass | Reference a node named #bass. | |
EXPR | Generic math expression. This is the default node type. | Any equation |
ENV | Envelope filter. | [a]ttack = 0 [s]sustain = 0 [r]elease = 0 [i]nput = 1 |
TRI SAW SIN SQR | Common oscillators. | [f]req [p]hase = 0 [h]igh = 1 [l]ow = -high |
PULSE | Pulse oscillator. | [w]idth = 0.5 and osc params |
TBL | Wave table oscillator. List x,y points in order like t 0 1 0.5 -1 1 1 | [t]able and osc params |
NOISE | White noise. | [h]igh = 1 [l]ow = -high |
DEL | Delay filter. Max must be constant. | [t]ime = 0 [m]ax [i]nput = 0 |
LPF HPF BPF NPF APF PKF LSF HSF | Biquad filters: lowpass, highpass, etc. Gain is linear. | [f]req [b]andwidth = 1 [g]ain = 1 [i]nput = 0 |
Samples
Motivation
The sound effect scripting language is a simple, shorthand way to write sound effects. Like my music editor, I needed an easy way to create effects - explosions, beeps, etc. With the tools I had available I could create those effects, but it was very tedious and non portable. A simple hi-hat in my old notation took 658 bytes, whereas the same effect is only 144 bytes now.
Old | New |
function createdrumhihat(volume=1.0,freq=7000,time=0.1,sndfreq=44100) {
let len=Math.ceil(time*sndfreq);
let snd=new Audio.Sound(sndfreq,len);
let gain=new Audio.Envelope([Audio.Envelope.LIN,0.005,volume*0.7,Audio.Envelope.EXP,time-0.005,0]);
let hp=new Audio.Biquad(Audio.Biquad.HIGHPASS,freq/sndfreq);
let bp=new Audio.Biquad(Audio.Biquad.BANDPASS,freq*1.4/sndfreq);
let data=snd.data;
for (let i=0;i<len;i++) {
let t=i/sndfreq,u=(1-0.2*i/len)*freq/sndfreq;
let x=Audio.noise();
hp.updatecoefs(Audio.Biquad.HIGHPASS,u);
bp.updatecoefs(Audio.Biquad.BANDPASS,u*1.4);
x=bp.process(x)+hp.process(x);
data[i]=x*gain.get(t);
}
return snd;
} |
#freq: 7000
#time: 0.1
#sig: NOISE
#hpf: HPF F #freq I #sig G 0.7
#bpf: BPF F #freq 1.4 * I #sig G 0.7
#out: ENV A 0.005 R #time 0.005 - I #hpf #bpf
|
There were several attempts before settling on the current notation. The first being to rework my music sequencer to also allow waveform editing. This "worked", but the static nature of rendering everything to sounds made it difficult to work with.
One of the things I didn't realize was that working with time dependent data (as opposed to images) required a different way of thinking. For example: if we draw a rectangle with draw.fillrect(0,0,10,10), any data it needs can be contained in that one function call. There's no need to tweak the size or color while it's drawing, but if a sound is playing we may need to change the frequency as it moves away from us.
I decided to go with the dynamic, node-based nature that most editors and DAWs have. Effects can be built up from nodes, and multiple node inputs can be fed into a parameter and combined with reverse polish notation, like if we want to vary an oscillator's frequency. This notation is not only easier to parse, but lets us works better with the plugin nature of nodes.
Sound effects generate a lot of raw data, so to make it performant these effects would also be compiled down to a simple program. I created a decompiler to double check my audio programs - this is what peak hi-hat performance looks like:
Notes
I'm really happy with how this turned out. Compared to the mangled music editor notation I was attempting to make, this allows for simple AND efficient handling of sound effects.
Among other failed attempts was an assembly-like notation and basically remaking LISP. My attempts to make a general purpose sound effect language were leading me to remake entire programming languages. Luckily I've worked my way out of that to create something purely focused on living time-dependent sounds.
Work remains to simplify the parser and handle a few situations (like delay filters) I chose to ignore to get something out the door.