Hacktunes is a monthly collaborative music programming project. Each month, we pick a song to cover using the Web Audio API. Anyone can contribute tracks (written in JavaScript/ES6) which generate a part of the song in real-time. We mash all of these tracks together into a combined performance, creating a Hacktune!
This month, we're covering Still Alive by Jonathan Coulton.
To make songs, we need two things: instruments (:guitar::saxophone::microphone:), and a sequence of notes to play (:musical_score:). Hacktunes "tracks" are instruments using the Web Audio API, triggered by MIDI events. We provide a basic MIDI file as a starting point, but can add your own to make new melodies and rhythms. You can implement synthesizers, drum machines, sample audio clips, etc -- the sky's the limit!
The hacktunes codebase doubles as a development environment. We use Hot Module Replacement to update tracks immediately: you can make changes to the code during playback and hear them live! ⚡⚡⚡
- Clone the repository.
npm installnpm start
The best way to jump in is to modify an existing track. Hear something you like? Copy it and start playing around. This month's tracks can be found in /songs/still-alive.
Everyone is welcome to contribute!
- Build your own track in a subdirectory of
/songs/still-alive - Send us a pull request (squash your commits, please!)
When we merge your commit, your track will automatically go live on hacktun.es.
🚧 Hacktunes is in early development. Need something that isn't listed here? Please file an issue for discussion.
⚡ Prefer to read code? Here's an annotated example.
Your track is a module consisting of two functions: load, and create.
Declare resources your track needs to fetch here. Use the require function to refer to file paths relative to the current module.
Return an object mapping resource names to resources (see below).
Fetch a MIDI file at the specified URL.
Fetch and decode an audio file at the specified URL.
export function load({ loadAudio, loadMIDI }) {
return {
midi: loadMIDI(require('../still-alive.mid')),
sample: loadMIDI(require('./meow.wav')),
}
}Instantiate an instrument and trigger it by playing MIDI files. This will be called before the song begins, as well as any time your track hot reloads.
Queue a MIDI file to be played. Pass it a MIDI resource from res. While playing, your eventHandler will be called with MIDI events right before they happen. Use this callback to queue Web Audio operations at the precise time specified by each MIDI event.
Your eventHandler(ev) callback will be called with MIDI events of the following structure:
{
"time": 2545.03, // Precise AudioContext event time (seconds)
"channel": 0, // MIDI channel number
"note": 66, // MIDI note number
"frequency": 369.99, // Note frequency (Hz)
"type": 8, // MIDI event type constant
"subtype": 8, // MIDI event subtype constant
"param1": 66, // MIDI event param value
"param2": 0, // MIDI event param value
"midi": {
"data": [0, 255, 1, 0, 0, 128, 66, 0 ], // Raw MIDI protocol bytes, suitable for Web MIDI API
"time": 2545670.91, // Web MIDI time (ms relative to navigation start)
},
}Event type constants can be imported from the midievents module:
import {
EVENT_MIDI_NOTE_ON,
EVENT_MIDI_NOTE_OFF,
} from 'midievents'An object containing fetched resources specified by your load function.
The AudioContext for the playing song.
The destination AudioNode to output audio to. Connect your Web Audio API nodes here.
import { EVENT_MIDI_NOTE_ON } from 'midievents'
export function create({ transport, res, ctx, out }) {
transport.playMIDI(res.midi, ev => {
if (ev.subtype === EVENT_MIDI_NOTE_ON) {
const osc = ctx.createOscillator()
osc.type = 'sine'
osc.frequency.value = ev.frequency
noteGain.gain.setValueAtTime(0, ev.time)
noteGain.gain.linearRampToValueAtTime(1, ev.time + 10)
noteGain.gain.linearRampToValueAtTime(0, ev.time + 100)
osc.start(ev.time)
osc.stop(ev.time + 100)
osc.connect(out)
}
})
}