Wednesday, February 07, 2018

Let's Make Music, Version 2

Yesterday, I described three steps I took in the pursuit of generating better sounding music. Today I took yet another step: I implemented an alternative API for music generation. While the underlying functionality is still powered by Web Audio API oscillators, how I interact with those oscillators has changed significantly.

Here's a few guiding principles of the new system:

My audio generator continues to take in, and return a state context. This remains consistent with the visual creation language I described previously.

On this state context are a number of critical values: bpm, width and synths. bpm, as the name suggests, is the beats per minute of the audio being created. The audio generation system now works strictly in beats, rather than absolute time. width corresponds to how many beats the music generator is expected to account for. synths is an array of Synth objects which generate musical tones. The values of bpm and width are set to sane values, but the music generator can change these at will.

After width number of beats, the music generator is called again and expected to return a new set of Synth objects ready to produce music.

A Synth is an abstraction of an Oscillator. To simplify matters, it has two controls: frequency and gain.

By default, a Synth's frequency and gain are set to 0. To set either frequency or gain, you must provide a target value, a position in beats when the value becomes active and a duration in beats that the value should remain active.

For lack of a better convention, I used people names for the synthesizer objects above. You can see two common patterns. First, you set the gain as constant and tweak the frequency:

var adam = new Synth().gain(.5, 0, ctx.width);

This sets up adam to play at half volume for the current iteration. There's no frequency set, so this Synth isn't actually generating any music. When I say:

adam.f({value: n, fadeIn: 0.01, fadeOut: .1 }, i, .5)

I'm setting the frequency to n at beat i for half a beat. The values of fadeIn and fadeOut allow me to gracefully ramp up to, and out of the frequency. I learned the hard way, that without using an envelope of some kind, the tone is almost always ear splitting.

The other pattern is to set the frequency as constant, and change the gain on the fly. Consider brenda:

var brenda = new Synth().f(Note.F, 0, ctx.width);

brenda is configured to play an F note. However, with the gain at 0, no sound is generated. To create a series of half notes, I raise the gain appropriately:

This code is raising the gain a larger amount for every half note generated.

Give this code a listen:

Is this music? I supposed. Certainly after a couple of iterations it will make your head hurt. But it has too much repetition to be noise. Let's call it aurally interesting.

Here are some other examples:

I think it's noteworthy how the change in music API has vastly influenced the music generated. This is one of the joys of programming: we can invent, with relative ease, new metaphors for solving problems. And just by changing the metaphor, you can arrive at new solutions. I suppose this is the virtual version of deciding you can't make music with a piano, so you go out and buy a drum set.

The bottom line: I'm enjoying my new toy and thankfully you aren't being forced to hear me practice.