Beep.js
TL;DR
Create a synthesizer with one line of code: synth = new Beep.Instrument()
Or plink away on the demo synth: http://beepjs.com. Tap the pulsing Play button for a jaunty music lesson.
MIDI controller support
Beep accepts input from MIDI controller keyboards via the brand new Web MIDI API. (Just so weāre clear, this is very awesome ;) Youāll need either Chrome 42 or Chrome 43+. For Chrome 42 you must enable the Web MIDI API manually by visiting chrome://flags/#enable-web-midi and clicking Enable. For Chrome 43 and later this is enabled by default. Simply plug in your modern MIDI controller keyboard via USB, then load up Beep. Your keys and pitch-bending wheel will work just fine. And further support is coming soon!
Hackable
Beep is a JavaScript toolkit for building browser-based synthesizers using the WebAudio API. It takes a ābatteries includedā approach, meaning it boots up ready to give you the audio equivalent of āHello, World!ā without too much fuss. One line like synth = new Beep.Instrument()
will build a bundle of Trigger
interfaces, each with its own Voices
for Notes
āthat is, a piano keyboard that you can begin banging on immediately. But whatās a software piano that canāt play itself? Use synth.scorePlay()
to play the default score provided for you. (And yes, you can always write your own scores!)
The blurb above and descriptions below include some sample code
. If youāre new to hacking around in the browser you may be wondering where that codeās supposed to go. Are you viewing this in a modern desktop browser? Then you can open up your browserās JavaScript Console and start hacking away right now. Hereās how:
Chrome: View ā Developer ā JavaScript Console, or ā„āJ.
Safari: First, enable the Developer menu. Then, Develop ā Show Error Console, or ā„āC.
Firefox: Tools ā Web Developer ā Web Console, or ā„āK.
Opera: View ā Developer Tools ā Opera Dragonfly, or ā„āI, then click on the Console tab.
Notes
Creating a new note is easy: n = new Beep.Note()
. But unless youāre content with nothing but concert Aās blaring at 440Hz all day, youāre going to want to create other notes like so: new Beep.Note('Eā')
or new Beep.Note('5Eā')
for an Eā thatās in the 5th octave rather than the default 4th octave. So what does that 5Eā give you anyway? An object like this:
{ A: 440, // What Concert A are we tuned to? hertz: 622.253ā¦, // Frequency of the note. isFlat: true, // Set if ā. Similar: isSharp and isNatural. letter: "E", // Explains itself, no? letterIndex: 4, // ['ABCDEFG'].indexOf(letter). midiNumber: 75, // Corresponding MIDI controller keyboard code. modifier: "ā", // Set to ā, ā®, or āÆ. name: "Eā", // Note name. Will include ā®. nameIndex: 7, // ['Aā','Aā®','Bā','Bā®','Cā®'ā¦].indexOf(name) nameSimple: "Eā", // Note name. Will NOT include ā®. octaveIndex: 5, // On a standard piano, 0ā8. pianoKeyIndex: 55,// On a standard piano, 0ā87. tuning: "EDO12" // Default: Equal Division of Octave into 12 steps. }
Flexible parameters
Sure, you can call new Beep.Note('Eā')
and accept the above default parameters that come with it. But you can also send an Object to Note
instead of a String and set each of those parameters manually! No specific param is required so just send what you need:
new Beep.Note({ A: 442, name: 'Eā', octaveIndex: 5 })
By the numbers
Can we just throw all this named-note garbage out the window? Yes. Want the Devilās note? Try new Beep.Note(666)
. What does that give you? { hertz: 666 }
I happen to like named notes though. They provide a pretty nice grid to work with, eh?
Easy ASCII
It might quell your anxieties to know that Note
will intelligently convert the common # (number) into a proper āÆ (sharp) and will also accept a lowercase b as a substitue for ā (flat). Thereās no need to use ā® (natural) but it is in the code there should you desire to invoke it.
Smart conversion
If you commit a serious blunder like new Beep.Note('BāÆ')
donāt stress, Note
will kindly assume you intended Note('Cā®')
instead. (There is no BāÆ.) If you happen to be old school German then, yes, you can use H instead of B. (Similarly, there is no HāÆ. Weirdo.)
Western tunings
Right now only western tunings are supportedāIām afraid thatās all I know how to work with. All note params pass through Note.validateWestern()
which does the above fancy logic. From there Iāve included support for two separate tunings: Just intonation and Equal temperament. More needs to be written on this topic for sureā¦
Bach up a second
So all thatās great, but a Note
is just a mathematical model. (Youāll notice it has no play()
method for example.) It doesnāt make any sound. For that we will need a Voice
.
Voices
How do you make a Note
sing? Give it a Voice
. Or ratherācreate a Voice
initialized with a Note
and maybe pass it an AudioContext
to pipe the sound out to. Just as with Note
the arguments for Voice
are all optional. Providing none will yield a Voice
with a default Note
of 440Hz:
voice = new Beep.Voice()// Weāre running with defaults. voice.play()// Listen to that pure 440Hz Concert A. voice.pause()// Ok, weāve had enough.
Note arguments
Voice
will pass note-like arguments to Note
. It doesnāt take an in-state Liberal Arts degree to imagine what new Beep.Voice('2Eā')
or new Beep.Voice({ A: 442, name: 'Eā', octaveIndex: 2 })
might produce then. You could even try new Beep.Voice(new Beep.Note('2Eā'))
if youāre not into that whole brevity thing, man.
Audio arguments
If you do not pass an AudioContext
or GainNode
to Voice
it will create an AudioContext
for itself. This is convenient because it means Voice
just works (batteries-included, eh?) but there are hardware limits on the number of AudioContexts
you can create. Weāll see how to solve this later by creating an Instrument
and passing its AudioContext
to each Voice
.
Only fix whatās Baroque
I guess all the above is pretty cool, but having to type voice.play()
and voice.pause()
everytime I want to voice a Note
is kind of a drag. And thatās where Trigger
comes in.
Triggers
We can dream up a Note
, give it a Voice
, but wouldnāt it be great if we had some visible DOM Elements and Event Listeners working on our behalf? Behold, your default Concert A: t = new Beep.Trigger()
. Simply creating a new Trigger
will also construct the DOM bits and listeners for you. No further fuss necessary.
Notes & Voices
As you may have guessed, Trigger
will create a Voice
for you and assign it a Note
. Setting this at initialization time is trivial: new Beep.Trigger('Eā')
. See the Voice
description above to get an idea of the variation possible here. And itās likewise trivial to alter the Note
or Voice
after creation.
Many Voices
Rather than one single voice, Trigger
is setup to handle a whole Array of them. In fact, the default Trigger
uses two voices: one employs a sine-wave oscillator at the intended Note
while a second employs a square-wave osciallator running one octave lower for a nice chunky Nintendo sound. Customizing your instanceās createVoices()
method is the name of the game!
Audio arguments
Just like Voice
, Trigger
is happy to ingest an AudioContext
or GainNode
argument but will make do without one if it has to. See the above Voice
blurb for more details. Additionally you can pass it a Functionā¦
Customizing Triggerās createVoices() method
Upon initialization each instance of Trigger calls its createVoices()
method. If youāre the type of gal that likes to annihilate mosquitos using atom bombs then you can just overwrite Beep.Trigger.prototype.createVoices
. Otherwise, why not pass a custom function during initialization like so:
var trigger = new Beep.Trigger( '2Eb', function(){ this.voices.push( // Letās call this our āFoundation Voiceā // because it will sing the intended Note. new Beep.Voice( this.note, this.audioContext ) .setOscillatorType( 'sine' ) .setAttackGain( 0.4 ), // This Voice will sing a Perfect 5th above the Foundation Voice. new Beep.Voice( this.note.hertz * 3 / 2, this.audioContext ) .setOscillatorType( 'triangle' ) .setAttackGain( 0.1 ), // This Voice will sing 2 octaves above the Foundation Voice. new Beep.Voice( this.note.hertz * 4, this.audioContext ) .setOscillatorType( 'sawtooth' ) .setAttackGain( 0.01 ), // This Voice will sing 1 octave below the Foundation Voice. new Beep.Voice( this.note.hertz / 2, this.audioContext ) .setOscillatorType( 'square' ) .setAttackGain( 0.01 ) )})
Many Triggers
Throw a few of these together and you have a mini-keyboard. What famous movie theme does this keyboard play? Notice how we can optionally add keyboard event listeners to bind characters to Triggers
? Here weāve assigned the characters 1ā5 to activate the five triggers respectively.
new Beep.Trigger('4G').addTriggerChar('1') new Beep.Trigger('4A').addTriggerChar('2') new Beep.Trigger('4F').addTriggerChar('3') new Beep.Trigger('3F').addTriggerChar('4') new Beep.Trigger('4C').addTriggerChar('5')
What if we had a convenient way to bundle these Triggers
together? You guessed it: Instrument
to the rescue.
Instruments
How simple is this? synth = new Beep.Instrument()
. You can pass the constructor either a DOM Element or a String representing the ID of a DOM Element and it will target that for the build. Otherwise it will just create its own. That one command gives you a default keyboard of Triggers
with Voices
and so on. Pretty nifty, eh?
Triggers
Sure, upon creation your instance of Instrument
will run build()
on itself, creating a default set of Triggers
. But it is so easy to overwrite this function with your own custom keyboard. (You should do this!) There is a corresponding unbuild()
method for removing all of its Triggers
. And that movie-theme keyboard from above? It comes built-in as well:
Beep.Instrument.prototype.buildCloseEncounters = function(){ this.unbuild() .newTrigger( '4G', '1' ) .newTrigger( '4A', '2' ) .newTrigger( '4F', '3' ) .newTrigger( '3F', '4' ) .newTrigger( '4C', '5' ) return this }
The newTrigger()
convenience method creates a new Trigger
, passes it the existing AudioContext
, and adds keyboard Event Listeners. Oh, my!
Customizing Triggerās createVoices() methodāRedux
You can also pass a custom createVoices()
method to Instrument
and it will in turn pass that function to each Trigger
instance that it creates. See the main Trigger
description above for details!
Scores
Instrument
comes with a built-in score that you might recognize as Do Re Mi. In the demo you can click the pulsing Play button to run it. This is equivalent to Instrument.scorePlay()
in code. Check out the source to see how weāre able to compose the melody and harmony separately and Instrument.scoreLoad()
blends them together.
Composing
Scores are just Arrays ingested three entries at a time: 1. Delay time (relative to the previous command), 2. Trigger
ID to engage, 3. Engagement duration. I find itās easiest to write the durations in fractions like the musical notation they are replacing: Ā¼ = quarter note, Ā½ = half note, and so on. Hereās a sample from the default score:
melody = [ 36/4, '4C', 6/4,// Do[e] 6/4, '4D', 2/4,// a 2/4, '4E', 5/4,// deer 6/4, '4C', 2/4,// A 2/4, '4E', 4/4,// fe 4/4, '4C', 3/4,// male 4/4, '4E', 4/4,// deer ā¦
Further
In the future it might make more sense to separate Score into its own Class. Beepās naming conventions could use some tightening. And there is definitely a need for more explanation (and a demo) related to the difference between Just intonation and Equal temperament. And so much more to come. Itās early days.