Stewart Smith
Stewart Smith Beep Hero 1

Beep is an open-source JavaScript toolkit for building playable browser-based music synthesizers. Create your own synthesizer with one line of code: synth = new Beep.Instrument(). Or plink away on the demo synth at https://beepjs.com. Tap that pulsing “Play” button for a jaunty music lesson.

Beep takes a “batteries included” approach, providing an audio equivalent of “Hello, World!” without 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!)

Live editing

The built-in Beep Editor provides an easy way to make tweaks on the fly to the sound (and visual style) of your instrument. Just tap the curly brace icon in the upper-right corner of the Beep demo to pop open the Editor tray. Make your edits (you’ll see the status bar color switch from green to yellow), then tap the “Enter” icon in the upper-right to apply your changes. The example code in the Editor will change your keyboard’s color to a rainbow, applying a the full spectrum of hues to each octave from C to C. It will also adjust the sound such that the intended note is represented by a sine wave played at 80% volume. A triangle wave will sit 3/2 above that frequency (a perfect fifth) at 5% volume. A much higher sawtooth wave will add another 4% volume at two octaves above the intended note. And finally, a bass square wave set an octave below will add 6% volume.

Hackable

The intro 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.

MIDI controllers

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.

End of support 😭

I began experimenting with an early draft of Beep.js in 2014 following my transition from Google to Yahoo. (By the end of 2016 I’d find myself back at Google Creative Lab focussed exclusively on virtual reality.) Beep drew me into the Web Audio community here in New York and I got to meet some great folks like Tone.js author, Yotam Mann. But work and life continued their steady beat onward. I haven’t had time or opportunity to maintain Beep.js in the interim years, although I did briefly create / experiment with Darkwave as a virtual guitar effects pedal during the early days of the pandemic. The description below captures the spirit of Beep as it was under development.


Beep’s name

But why’s it named Beep? Wouldn’t Tone be more appropriate for something that holds a note? Perhaps. But I feel I adequately answered this in Issue #6 here 😉


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 substitute 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.

Attack, Decay, Sustain, and Release

Beep supports ADSR envelopes for each Voice, either through directly editing voice properties, or by using setter functions which then support function chaining. The direct properties are delayDuration, attackGain, attackDuration, decayDuration, sustainGain, sustainDuration, and releaseDuration. The setter functions use the same names, but pre-pend CamelCase function names with set, such as setDelayDuration. (And don’t forget to include occasional ASCII diagrams in the source code of your toolkits. It adds a little fun spice.)


                        D + ADSR Envelope                      

      ┌───────┬────────┬───────┬─────────────────┬─────────┐   
      │ Delay │ Attack │ Decay │     Sustain     │ Release │   
      │                                                    │  ↑
      │               •••                                  │   
      │             ••   •••                               │  G
      │           ••        •••                            │  A
      │          •             •••••••••••••••••••         │  I
      │         •                                 •        │  N
      │        •                                   •••     │   
      └••••••••────────┴───────┴─────────────────┴────•••••┘   

                              TIME →                           


    ADSR stands for Attack, Decay, Sustain, and Release. These are all units
    of duration with the exception of Sustain which instead represents gain
    rather than time. That exception can easily become a point of confusion, 
    particularly in this context where you may wish to script the duration of
    Sustain! For that reason I have named these variables rather verbosely.
    Additionally I’ve added a Delay duration. For more useful information see
    http://en.wikipedia.org/wiki/Synthesizer#ADSR_envelope

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.