- Notifications
You must be signed in to change notification settings - Fork85
Beep is a JavaScript toolkit for building browser-based synthesizers.
License
stewdio/beep.js
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
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 ajaunty music lesson.
Beep accepts input from MIDI controller keyboards via the brand newWeb MIDI API. (Just so we’re clear, this isvery awesome ;) You’ll need either Chrome 42 or Chrome 43+. For Chrome 42you must enable the Web MIDI API manually by visitingchrome://flags/#enable-web-midi and clickingEnable. For Chrome 43 andlater this is enabled by default. Simply plug in your modern MIDI controllerkeyboard via USB,then load up Beep. Your keys and pitch-bending wheel willwork just fine. And further support is coming soon!
Beep is a JavaScript toolkit for building browser-based synthesizers usingthe WebAudio API. It takes a “batteries included” approach, meaning it bootsup ready to give you the audio equivalent of “Hello, World!” without too muchfuss. One line likesynth = new Beep.Instrument()
will build a bundle ofTrigger
interfaces, each with its ownVoices
forNotes
—that is, a pianokeyboard that you can begin banging on immediately. But what’s a softwarepiano that can’t play itself? Usesynth.scorePlay()
to play the defaultscore provided for you. (And yes, you can always write your own scores!)
The blurb above and descriptions below include somesample code
. If you’renew to hacking around in the browser you may be wondering where that code’ssupposed to go. Are you viewing this in a modern desktop browser? Then youcan open up your browser’sJavaScript Console and start hacking away rightnow. 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.
Creating a new note is easy:n = new Beep.Note()
. But unless you’re contentwith nothing but concert A’s blaring at 440Hz all day, you’re going to want tocreate other notes like so:new Beep.Note('E♭')
ornew Beep.Note('5E♭')
for an E♭ that’s in the 5th octave rather than the default 4th octave. So whatdoes that5E♭ 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 callnew Beep.Note('E♭')
and accept the above defaultparameters that come with it. But you can also send an Object toNote
instead of a String and set each of those parameters manually! No specificparam is required so just send what you need:
newBeep.Note({A:442,name:'E♭',octaveIndex:5})
By the numbers
Can we just throw all this named-note garbage out the window? Yes. Want theDevil’s note? Trynew Beep.Note(666)
. What does that give you?{ hertz: 666 }
I happen to like named notes though. They provide a prettynice grid to work with, eh?
Easy ASCII
It might quell your anxieties to know thatNote
will intelligently convertthe common# (number) into a proper♯ (sharp) and will also accept alowercaseb 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 likenew Beep.Note('B♯')
don’t stress,Note
will kindly assume you intendedNote('C♮')
instead.(There is no B♯.)If you happen to be old school German then, yes, you can useH instead ofB. (Similarly, there is no H♯. Weirdo.)
Western tunings
Right now only western tunings are supported—I’m afraid that’s all I know howto work with. All note params pass throughNote.validateWestern()
which doesthe above fancy logic. From there I’ve included support for two separatetunings:Just intonationandEqual temperament.More needs to be written on this topic for sure…
Bach up a second
So all that’s great, but aNote
is just a mathematical model. (You’ll noticeit has noplay()
method for example.) It doesn’t make any sound. For that wewill need aVoice
.
How do you make aNote
sing? Give it aVoice
. Or rather—create aVoice
initialized with aNote
and maybe pass it anAudioContext
to pipe thesound out to. Just as withNote
the arguments forVoice
are all optional.Providing none will yield aVoice
with a defaultNote
of 440Hz:
voice=newBeep.Voice()// We’re running with defaults.voice.play()// Listen to that pure 440Hz Concert A.voice.pause()// Ok, we’ve had enough.
Note argumentsVoice
will pass note-like arguments toNote
. It doesn’t take an in-stateLiberal Arts degree to imagine whatnew Beep.Voice('2E♭')
ornew Beep.Voice({ A: 442, name: 'E♭', octaveIndex: 2 })
might produce then.You could even trynew Beep.Voice(new Beep.Note('2E♭'))
if you’re not intothat whole brevity thing, man.
Audio arguments
If you do not pass anAudioContext
orGainNode
toVoice
it will createanAudioContext
for itself. This is convenient because it meansVoice
justworks (batteries-included, eh?) but there are hardware limits on the number ofAudioContexts
you can create. We’ll see how to solve this later by creatinganInstrument
and passing itsAudioContext
to eachVoice
.
Only fix what’s Baroque
I guess all the above is pretty cool, but having to typevoice.play()
andvoice.pause()
everytime I want to voice aNote
is kind of a drag. Andthat’s whereTrigger
comes in.
We can dream up aNote
, give it aVoice
, but wouldn’t it be great if wehad some visible DOM Elements and Event Listeners working on our behalf?Behold, your default Concert A:t = new Beep.Trigger()
. Simply creatinga newTrigger
will also construct the DOM bits and listeners for you.No further fuss necessary.
Notes & Voices
As you may have guessed,Trigger
will create aVoice
for you and assign itaNote
. Setting this at initialization time is trivial:new Beep.Trigger('E♭')
. See theVoice
description above to get an idea ofthe variation possible here. And it’s likewise trivial to alter theNote
orVoice
after creation.
Many Voices
Rather than one single voice,Trigger
is setup to handle a whole Array ofthem. In fact, the defaultTrigger
uses two voices: one employs a sine-waveoscillator at the intendedNote
while a second employs a square-waveosciallator running one octave lower for a nice chunky Nintendo sound.Customizing your instance’screateVoices()
method is the name of the game!
Audio arguments
Just likeVoice
,Trigger
is happy to ingest anAudioContext
orGainNode
argument but will make do without one if it has to. See the aboveVoice
blurb for more details. Additionally you can pass it a Function…
Customizing Trigger’s createVoices() method
Upon initialization each instance of Trigger calls itscreateVoices()
method. If you’re the type of gal that likes to annihilate mosquitos usingatom bombs then you can just overwriteBeep.Trigger.prototype.createVoices
.Otherwise, why not pass a custom function during initialization like so:
vartrigger=newBeep.Trigger('2Eb',function(){this.voices.push(// Let’s call this our “Foundation Voice”// because it will sing the intended Note.newBeep.Voice(this.note,this.audioContext).setOscillatorType('sine').setAttackGain(0.4),// This Voice will sing a Perfect 5th above the Foundation Voice.newBeep.Voice(this.note.hertz*3/2,this.audioContext).setOscillatorType('triangle').setAttackGain(0.1),// This Voice will sing 2 octaves above the Foundation Voice.newBeep.Voice(this.note.hertz*4,this.audioContext).setOscillatorType('sawtooth').setAttackGain(0.01),// This Voice will sing 1 octave below the Foundation Voice.newBeep.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 famousmovie theme does this keyboard play? Notice how we can optionally addkeyboard event listeners to bind characters toTriggers
? Here we’veassigned the characters 1–5 to activate the five triggers respectively.
newBeep.Trigger('4G').addTriggerChar('1')newBeep.Trigger('4A').addTriggerChar('2')newBeep.Trigger('4F').addTriggerChar('3')newBeep.Trigger('3F').addTriggerChar('4')newBeep.Trigger('4C').addTriggerChar('5')
What if we had a convenient way to bundle theseTriggers
together? Youguessed it:Instrument
to the rescue.
How simple is this?synth = new Beep.Instrument()
. You can pass theconstructor either a DOM Element or a String representing the ID of a DOMElement and it will target that for the build. Otherwise it will just createits own. That one command gives you a default keyboard ofTriggers
withVoices
and so on. Pretty nifty, eh?
Triggers
Sure, upon creation your instance ofInstrument
will runbuild()
onitself, creating a default set ofTriggers
. But it is so easy to overwritethis function with your own custom keyboard. (You should do this!) There is acorrespondingunbuild()
method for removing all of itsTriggers
. And thatmovie-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')returnthis}
ThenewTrigger()
convenience method creates a newTrigger
, passes it theexistingAudioContext
, and adds keyboard Event Listeners. Oh, my!
Customizing Trigger’s createVoices() method—Redux
You can also pass a customcreateVoices()
method toInstrument
and it willin turn pass that function to eachTrigger
instance that it creates. See themainTrigger
description above for details!
Instrument
comes with a built-in score that you might recognize asDo Re Mi. In the demo you canclick the pulsing Play button to run it. This isequivalent toInstrument.scorePlay()
in code. Check out the source to seehow we’re able to compose the melody and harmony separately andInstrument.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. Engagementduration. I find it’s easiest to write the durations in fractions like themusical notation they are replacing: ¼ = quarter note, ½ = half note, and soon. Here’s a sample from the default score:
melody=[36/4,'4C',6/4,// Do[e]6/4,'4D',2/4,// a2/4,'4E',5/4,// deer6/4,'4C',2/4,// A2/4,'4E',4/4,// fe4/4,'4C',3/4,// male4/4,'4E',4/4,// deer…
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 aneed for more explanation (and a demo) related to the difference betweenJust intonation andEqual temperament.And so much more to come. It’s early days.