by @MattMcKegg
I make music with computers.
I have been using computers to make music for nearly 15 years.
Today I'm going to talk about my musical journey, and how I started using JavaScript to enhance my live performances, and what drove me to start writing my own music software called Loop Drop.
It's pretty much all live demos from here on in. Wish me luck :)
This is Ableton Live. And here is a song I made. It was not recorded, it was drawn. Note by note. This is what separates computer music, from computer recording.
You are master of time.
This is how the vast majority of electronic music is created today. Elements of this are also used in most other genres too. Less and less popular music is being recorded, instead it is being painted.
http://lunarmusic.net/album/hybrid-awaken
In 2006, I released my first album on the internet. It was just a collection of stuff I'd been working on for the years previous. I put up a website with some artwork, and a streaming player, with a great big free download button. Not sure quite how this happened, but somehow, it got picked up by a music blog. I guess bedroom composers were less common in those days? Next minute the link hit StumbleUpon, and I started getting hundreds of visitors every day. Dozens of people were downloading my album. I started to receive fan mail.
There were requests from people all over the world asking me to come play live.
There's really only two options.
That could be really cool, and a lot of fun! But oh, the cost. And it would be completely different, possible much better. But it's not computer music.
This is a very common strategy. People just wanna hear your tunes! Why not just push play and twiddle knobs? For bonus points, you could create loops of all the individual parts of your song, trigger those live, and maybe play the synth line, or percussion using a midi controller.
So many electronic musicians do this. They have a whole bunch of knobs and twiddles and complexity, but they are basically just DJing their bedroom/studio compositions (at most remixing). Minus the ordering and effects (filters, stutters, etc), they're stuck with whatever they painted in the studio.
This is what I started trying to do around 2010. I bought myself a Novation Launchpad, and tried to use it with Ableton Live and a midi keyboard.
A guitarist can just sit down and jam out, I wanted that kind of experience.
It took me so much effort to prepare my live sets. I had to mix down all the individual parts, cram them all into one project. And then when it came to play live, there was very little flexibility, and I'd end up making mistakes of a technical nature and be far too focused on remember the correct sequence trigger.
After my disappointing attempt at live computer music in 2010 and 2011, I started experiment with writing my own node.js hackery for processing midi.
Musical Instrument Digital Interface
It's a protocol developed way back in the 1980s allowing musicians to connect various different musical devices together. It allowed one device to trigger another devices sounds. You could connect your keyboard to an advanced sound module, or a sequencer to your synthesizer. In these days, the use of the computer would be the opposite way around to today. You would be sending MIDI to a synthesizer, sampler.
Important to note that MIDI is now mostly unrelated from .midi files.
https://github.com/livejs/midi-stream
This module let's you connect to midi devices in Node.js and the browser (via Web MIDI).
var MidiStream = require('midi-stream')
var keyboard = MidiStream('LPK25', {
normalizeNotes: true
})
keyboard.on('data', console.log)
We can also create virtual midi keyboards. It will show up in our music software as if it is hardware connected to the computer, but we can send whatever messages we want.
Looking at the output from my midi keyboard, I can see what to send to make music play in Ableton Live from javascript.
var MidiStream = require('midi-stream')
var output = MidiStream('JAVASCRIPT MUSIC', {
virtual: true
})
function play(note, at, duration) {
setTimeout(() => output.write([144, note, 127]), at * 1000)
setTimeout(() => output.write([144, note, 0]), (at + duration) * 1000)
}
play(48, 2, 0.7)
play(55, 3, 0.7)
play(60, 4, 2)
It may not look like a keyboard, but its buttons are just arranged differently.
var launchpad = MidiStream('Launchpad Mini')
launchpad.on('data', console.log)
Sending the same message back to the launchpad will light up that particular button.
launchpad.on('data', (data) => launchpad.write(data))
Streams can be okay for working with, but I prefer observables. This module is my favorite approach. It's "just javascript".
http://github.com/Raynos/observ
var Observ = require('observ')
var value = Observ()
value.set(100)
console.log(value()) //=> 100
value(function (newValue) {
// is called every time the value changes
console.log(newValue) //=> 200, 300
})
value.set(200)
value.set(300)
We can set any object as a value, including an array grid.
http://github.com/mmckegg/array-grid
1 2 3 4
5 6 7 8
9 10 11 12
var ArrayGrid = require('array-grid')
var grid = ArrayGrid([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], [3, 4])
grid.get(0, 1) //=> 2
grid.get(1, 2) //=> 7
grid.get(2, 3) //=> 12
grid.data //=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
Let's convert that midi stream to an observable grid.
var Observ = require('observ')
var ArrayGrid = require('array-grid')
function LaunchpadToGrid (midiPort) {
var obs = Observ(ArrayGrid([], [8, 8]))
midiPort.on('data', (data) => {
var col = data[1] % 16
var row = Math.floor(data[1] / 16)
if (col < 8 && row < 8) {
var newValue = ArrayGrid(obs().data.slice(), obs().shape)
newValue.set(row, col, data[2])
obs.set(newValue)
}
})
return obs
}
var inputGrid = LaunchpadToGrid(launchpad)
inputGrid((grid) => {
var result = ''
for (var row = 0; row < grid.shape[0]; row++) {
for (var col = 0; col < grid.shape[1]; col++) {
result += (' ' + (grid.get(row, col) || 0)).slice(-3) + ' '
}
result += '\n'
}
console.log(result)
})
function GridToMidi (port, fn) {
var obs = Observ()
var outputValues = {}
obs(function (grid) {
if (grid) {
var length = grid.shape[0] * grid.shape[1]
for (var i = 0; i < length; i++) {
var value = grid.data[i] || 0
var message = fn(value, i)
if (message) {
var key = message[0] + '/' + message[1]
var lastValue = outputValues[key] && outputValues[key][2] || 0
if (lastValue !== value) {
outputValues[key] = message
port.write(message)
}
}
}
} else {
Object.keys(outputValues).forEach(function (key) {
var lastMessage = outputValues[key]
if (lastMessage && lastMessage[2]) {
port.write(outputValues[key] = [lastMessage[0], lastMessage[1], 0])
}
})
}
})
return obs
}
var outputGrid = GridToMidi(output, (value, i) => {
return [144, 36 + i, value]
})
inputGrid(outputGrid.set)
var outputLights = GridToMidi(launchpad, (value, i) => {
var id = (i % 8) + (Math.floor(i / 8) * 16)
return [144, id, value]
})
inputGrid(outputLights.set)
Tools for transforming observables, works like transform streams.
http://github.com/mmckegg/observ-transform
var Transform = require('observ-transform')
var connect = require('observ-transform/connect')
var send = require('observ-transform/send')
connect(
LaunchpadToGrid(launchpad),
send(
GridToMidi(output, (value, i) => { return [144, 36 + i, value] }),
GridToMidi(launchpad, (value, i) => {
var id = (i % 8) + (Math.floor(i / 8) * 16)
return [144, id, value]
})
)
)
There, much nicer. A lot easier to see the signal flow now.
I want to be able to play multiple instruments at the same time using the one launchpad. This transform will allow me to grab parts of the grid and send it to different tracks.
Observe a subset of the grid.
function Range (shape, offset) {
return Transform((input) => {
return input && input.getRange(shape, offset)
})
}
145, 146, etc specify different input channels in midi. We can connect these to different tracks in Ableton Live. 36 corresponds to C1.
connect(
LaunchpadToGrid(launchpad),
send(
connect(Range([3, 8], [0, 0]), GridToMidi(output, (value, i) => {
return [144, scale(i, -1), value]
})),
connect(Range([2, 8], [3, 0]), GridToMidi(output, (value, i) => {
return [145, scale(i, -1), value]
})),
connect(Range([1, 8], [5, 0]), GridToMidi(output, (value, i) => {
return [146, scale(i, 0), value]
})),
connect(Range([1, 4], [6, 0]), GridToMidi(output, (value, i) => {
return [147, 36 + i, value]
})),
connect(Range([1, 4], [7, 0]), GridToMidi(output, (value, i) => {
return [148, 36 + i, value]
})),
connect(Range([2, 4], [6, 4]), GridToMidi(output, (value, i) => {
return [149, scale(i), value]
})),
GridToMidi(launchpad, (value, i) => {
var id = (i % 8) + (Math.floor(i / 8) * 16)
return [144, id, value]
})
)
)
function scale (pos, octave) {
var notes = [ 0, 2, 3, 4, 5, 7, 9, 10 ]
var scalePosition = Math.floor(pos % notes.length)
var multiplier = Math.floor(pos / notes.length) + (octave || 0)
var note = notes[scalePosition]
return note + (multiplier * 12) + 63
}
To do anything involving time, we first need to know what time it is.
Almost all midi compatible sequencers have the ability to send (or recieve) a midi clock. This allows one app to sync to the tempo and timing of another.
var midiClock = MidiStream.openInput('CLOCK INPUT', {
virtual: true,
includeTiming: true
})
midiClock.on('data', console.log)
Let's convert the ticks to a running position that we can use later in our transforms.
function PositionFromMidiClock (port) {
var obs = Observ(0)
var ticks = 0
port.on('data', (data) => {
if (data[0] === 248) {
ticks += 1
obs.set(ticks / 24)
}
})
return obs
}
var currentPosition = PositionFromMidiClock(midiClock)
currentPosition(console.log)
Now that I'm triggering all of these instruments with just my two hands, it would be nice if I didn't have to play exactly in time. Also it's one of the defining sounds of computer music, that perfectly synced beat.
Held buttons will trigger continuously synced with the clock at the rate specified.
function Repeater (currentPosition, rate) {
var currentFrame = null
return Transform((input, args) => {
if (args.rate) {
var pos = (args.currentPosition % args.rate)
if (pos === 0) {
currentFrame = input
}
return (pos / args.rate) < 0.5 ? currentFrame : null
} else {
return input
}
}, {
rate: rate,
currentPosition: currentPosition
})
}
connect(
LaunchpadToGrid(launchpad),
Repeater(currentPosition, 1 / 2)
send(...)
)
It would be really nice if I could lock in a sequence if I like what I hear. Let's add a recorder that records all events ready for use later.
function RecordTo (target, currentPosition) {
var obs = Observ()
obs((value) => {
var last = target[target.length - 1]
if (last && last.at === currentPosition()) {
target.pop()
}
target.push({
at: currentPosition(),
value: value
})
})
return obs
}
Whenever we play something that we like, and want to hear again, we can press a loop button, and whatever we just played will start to loop.
function getLoop (start, length) {
return {
length: length,
events: recordedEvents.filter((event) => {
return event.at >= start && event.at < start + length
}).map((event) => {
return {
at: event.at % length,
value: event.value
}
}).sort((a, b) => { return a.at - b.at })
}
}
var loopLength = Observ(8)
var currentLoop = Observ()
launchpad.on('data', function (data) {
if (data[0] === 176 && data[1] === 104 && data[2]) {
currentLoop.set(
getLoop(currentPosition() - loopLength(), loopLength())
)
}
})
function PlayLoop (currentPosition) {
return Transform((input, args) => {
if (input && input.events.length) {
var value = input.events[input.events.length - 1].value
for (var i = 0; i < input.events.length; i++) {
if (input.events[i].at > args.currentPosition % input.length) {
return value
} else {
value = input.events[i].value
}
}
return value
}
}, { currentPosition: currentPosition })
}
I've gone ahead and implemented the looper and added a bunch of controls for manipulating the loop. Clear, undo, redo, stutter, suppress. The ability to remove notes. Everything is done with observ-transform. See midi-looper.js.
This is cool and all, but why are we even using this Ableton Live thing, when we could just use Web Audio?
It can be a lot of fiddly work using the Web Audio API directly. This module makes it easier to create audio graphs and retriggering sounds.
http://github.com/mmckegg/audio-slot
var AudioSlot = require('audio-slot')
We need to create a context object that includes all of the different nodes we want to use in our graph.
var context = {
audio: new AudioContext(),
nodes: {
osc: require('audio-slot/sources/oscillator'),
env: require('audio-slot/params/envelope'),
filter: require('audio-slot/processors/filter')
}
}
var slot = AudioSlot(context)
slot.set({
sources: [
{ node: 'osc',
shape: 'sawtooth',
amp: { node: 'env', attack: 1, release: 1, value: 0.4 }
}
],
processors: [
{ node: 'filter',
frequency: { node: 'env', value: 20000, decay: 0.5, sustain: 0.01 }
}
]
})
slot.connect(context.audio.destination)
slot.triggerOn(2)
slot.triggerOff(4)
A dev server for rapid prototyping that automatically "browserifies" your code.
https://github.com/mattdesl/budo
Let's try it out:
$ npm install budo -g
$ budo web-audio-looper.js:bundle
I've taken everything so far in this talk, added a bunch of css and html and put it into an app.
A looper, modular synth and sampler designed for improvisation and live performance.
$ npm install loop-drop -g
$ loop-drop
My app is written in JavaScript and is powered by Web Audio and Web MIDI, but it runs on the desktop and has access to your file system.
Build cross-platform desktop applications in JavaScript and HTML+CSS.
My live experiments
https://soundcloud.com/destroy-with-science
I have now developed a new method of composing music. Rather than painting all of my music by hand and then trying to figure out how to play it live, I instead start live. I just play what I want to hear, and record everything. When I like what I hear, I release it.
When I started this project I barely new anything. If you have a creative problem that needs solving, do give JavaScript a try.
JavaScript makes a great sketchbook!
Don't worry about frameworks or best practices. Get your ideas out there. Solve small problems for yourself. Scratch your own itch.
With a creative JavaScript project like Loop Drop, it is easy to get carried away on interesting sidetracks, but as you continue, keep reminding yourself what problem you are trying to solve.
But, not all at once!
Don't be afraid to completely rewrite things, just don't try and tackle the whole project at once.
This is very easy to do if you follow a modular pattern.
- Any questions? Tweet @MattMcKegg
- View notes at github.com/mmckegg/jsconfasia-talk-2015
- To hear more visit soundcloud.com/destroy-with-science