Skip to content
Colin Wren

Creating chiptunes with Python, MIDI and LSDJ

Python, Music, Software Development5 min read

gameboy running lsdj next to a boss hm-2 guitar pedal
Technical death metal on a gameboy? Why not!

Like any human being I love music. Like any nerd I love music made on old video game consoles.

In order to make music on my old video game console of choice, the Nintendo Gameboy, I use a piece of software called Little Sound DJ (LSDJ).

LSDJ, to put it very basically allows you to program notes into phrases (which are essentially bars of music) and then create chains of phrases which then become a song.

Working with limitations

The Gameboy only supports four channels (there is a fifth, but it’s hard to work with), two pulse channels, one wave and one noise channel and while it can play all four channels at once the individual channels themselves are monophonic.

These limitations are actually what drew me to chiptune music, the way you have to create your own means of implementing simple musical structures such as chords or non-common-time time signatures.

LSDJ offers tools to compensate for this lack of functionality, for instance a 9/8 time signature can be achieved by having two phrases of 16 notes with the second phrase ending after the 2nd note (so it’s 18/16) and chords by playing the notes in the chords very fast.

Chiptune artists aren’t just limited by the musical tooling available to them however, there’s also the limited resources of the hardware too.

LDSJ can support up to 254 16-note phrases and 254 16-phrase chains, this is actually plenty for most needs but with the 9/8 example above as two phrases are needed it’s suddenly halved to 127 phrases and if we then assume all four channels will be used that’s only 31 phrases per instrument, or 558 notes.

LSDJ again offers some tools to work with this limitation, when adding a phrase to a chain there’s the option to transpose the notes so you can maximise phrase re-use for things like key changes.

Stepping up my game

The majority of the songs I’ve composed / programmed in LSDJ have been relatively straight forward, being either in common time or having simple song structures.

However, I decided to give myself a challenge recently and attempt a cover of Culinary Hypersensitivity by Necrophagist, a song that I’ve been learning on guitar for the best part of a year.

My original attempt at the start of the year didn’t get very far due to me being unable to program the song in one session and losing my place after having time to pick it back.

I was learning the guitar parts and transcribing the Gameboy parts from a MIDI file so decided to try and relay the MIDI data from Logic to LDSJ via the USB-Boy interface I have, but I could never get it to work properly (this is more a Logic issue than the interface though).

I’d previously worked with MIDI data and decided to revisit my previous project but instead of visualising MIDI data I’d instead convert it into the notes, phrases and chains needed to transcribe the song to LSDJ.

I’m plotting all the right notes, just not necessarily in the right order

MIDI data is just a series of events with the only means of determining a musical structure by resolving the MIDI ticks to a musical note value such as a semi-quaver (16th note).

1song = midi.read_midifile('song.mid')
3track = song[0]
4crotchet = song.resolution
5quaver = crotchet / 2
6semi_quaver = quaver / 2
7notes_at_tick = {}
9notes = [tick for tick in track if == 'Note On']
10end_of_song = [tick for tick in track if == 'End of Track'][0]
11for tick in range(0, (end_of_song.tick + semi_quaver), semi_quaver):
12 notes_at_tick[tick] = []
14for index, note in enumerate(notes):
15 next_note = notes[index+1] if index != (len(notes)-1) else end_of_song
16 note_delta = next_note.tick - note.tick
17 if note_delta % semi_quaver == 0:
18 notes_at_tick[note.tick].append(note)
20# Order by tick
21processed_notes = OrderedDict(sorted(notes_at_tick.items(), key=lambda t: t[0]))
Creating a dict with the tick values as keys and notes at that tick as values

However music isn’t written as a streaming series of notes, it’s instead constructed of bars which contain a series of notes that resolve to a certain measure.

whiteboard showing notes into phrases
This will probably go down as my favourite bit of maths

Luckily MIDI stores time signature events along with the ticks they occur at so in order to lay out the blue print of song it’s just a case of mapping notes to the phrases/bars they belong in and mapping those phrases/bars to chains.

1time_sigs = [tick for tick in track if == 'Time Signature']
3for index, time_sig in enumerate(time_sigs):
4 next_time_sig = time_sigs[index + 1] if index != (len(time_sigs) - 1) else end_of_song
5 fraction_resolution = 16 / time_sig.denominator
6 notes_per_phrase = (time_sig.numerator * fraction_resolution)
7 time_sig_length = next_time_sig.tick - time_sig.tick
8 time_sig_bars = time_sig_length / (notes_per_phrase * semi_quaver)
9 for phrase_index in range(0, time_sig_bars):
10 start_tick = time_sig.tick + (phrase_index * (notes_per_phrase * semi_quaver))
11 end_tick = start_tick + (notes_per_phrase * semi_quaver)
12 phrase_count = int(math.ceil(notes_per_phrase / 16)) + 1
13 if phrase_count > 1:
14 for offset_index, phrase in enumerate(range(0, phrase_count)):
15 end_offset = end_tick - ((notes_per_phrase % 16) * semi_quaver)
16 new_start_tick = start_tick if offset_index == 0 else end_offset
17 new_end_tick = end_offset if offset_index == 0 else end_tick
18 note_count = 16 if offset_index == 0 else (notes_per_phrase % 16)
19 note_range = range(new_start_tick, new_end_tick, 120)
20 phrase_notes = {k: processed_notes[k] for k in note_range}
21 notes = OrderedDict(sorted(phrase_notes.items(), key=lambda t: t[0]))
22 phrases.append(
23 Phrase(
24 note_count,
25 new_start_tick,
26 new_end_tick,
27 '{0}/{1}'.format(time_sig.numerator, time_sig.denominator),
28 notes
29 )
30 )
31 else:
32 note_range = range(start_tick, end_tick, 120)
33 phrase_notes = {k: processed_notes[k] for k in note_range}
34 notes = OrderedDict(sorted(phrase_notes.items(), key=lambda t: t[0]))
35 phrases.append(
36 Phrase(
37 notes_per_phrase,
38 start_tick,
39 end_tick,
40 '{0}/{1}'.format(time_sig.numerator, time_sig.denominator),
41 notes
42 )
43 )
Mapping the notes to phrases, based on the time signatures

After creating the map of notes to bars, I then de-duplicated the phrases to cut down on the number of phrases that needed to be input and stored in chains.

I did this by creating a base64 string of the notes in the phrase and then using a dict to essentially create a set while also retaining the phrase order in an array.

1b64_phrase_dict = {}
2b64_phrase_keys = []
4for phrase in phrases:
5 phrase_key = phrase.notes_as_b64
6 if not b64_phrase_dict.get(phrase_key, None):
7 b64_phrase_dict[phrase_key] = phrase.notes
8 b64_phrase_keys.append(phrase_key)
11phrase_dict = {}
12phrase_dict_lookup = {}
13phrase_keys = []
15for index, key in enumerate(b64_phrase_dict.keys()):
16 new_index = index + int(args.phrase_offset)
17 phrase_dict[new_index] = b64_phrase_dict[key]
18 phrase_dict_lookup[key] = new_index
20for phrase in b64_phrase_keys:
21 phrase_keys.append(phrase_dict_lookup[phrase])
23chains = list(chunks(phrase_keys, 16))
De-duplicating the phrases and creating the chain mapping

Printing things out

Of course it’s not useful having the song structure in objects, they need to be converted into a means that makes it simple to input them into LSDJ.

The LSDJ phrase screen shows three/four columns (drums has two notes); note(s), instrument & command and 16 rows, although these rows are in hex so 0-F.

Displaying the drums took a little more effort as the MIDI file would return the pitch but LSDJ uses short code for the different drums such as bd for bass drum and chh for close hi-hat.

To show the correct drum I created a map of the different pitches and the drums they related to, additionally drums in LSDJ can play two sounds at once so I extended the phrase view to show both notes.

1import midi
3note_names = {v: k for k, v in midi.NOTE_NAME_MAP_SHARP.items()}
4drum_names = {
5 'C_3': 'BD-',
6 'B_3': 'MT-',
7 'A_3': 'MT-',
8 'G_3': 'LT-',
9 'Gs_3': 'CHH',
10 'D_3': 'SD-',
11 'Cs_4': 'CYM',
12 'C_4': 'HT-',
13 'Ds_4': 'RCY',
14 'D_4': 'HT-',
15 'E_4': 'RCY'
18def safe_list_get(l, idx):
19 try:
20 return l[idx]
21 except IndexError:
22 return '---'
25def get_drum_name(name):
26 if name == '---':
27 return name
28 return drum_names[name]
31def print_drums(notes):
32 """ Print out a LSDJ phrase notes """
33 row_template = '{note_num}{drum_1} {drum_2} {cmd}'
34 rows = []
35 for index, row in enumerate(notes):
36 drums = [note for note in row if note in drum_names.keys()]
37 drum_1 = safe_list_get(drums, 0)
38 drum_2 = safe_list_get(drums, 1)
39 cmds = [note for note in row if note in ['H00']]
40 cmd = safe_list_get(cmds, 0)
41 rows.append(row_template.format(
42 note_num='{:x}'.format(index).ljust(4),
43 drum_1=get_drum_name(drum_1),
44 drum_2=get_drum_name(drum_2),
45 cmd=cmd
46 ))
47 return '\n'.join(rows)
49for phrase in phrase_dict.items():
50 print('{:x}'.format(phrase[0]).rjust(10))
51 print(print_drums(phrase[1]))
Printing the drum phrases
blast beat printing into terminal
A blast beat

In order to make it easier to find my place (there’s a lot of phrases) I decided to reproduce the column and row structure in a very basic way, this worked very well as you can clearly see what was meant to go where and what phrase I was in.

If the phrase would have to end early (due to being in 6/8 etc) then I had to ensure the last row contained a H00 in the command row (this tells LSDJ to jump to the next phrase).

For the chains I did a similar display but only with the row number and phrase.

1for index, chain in enumerate(chains):
2 print(hex(index + int(args.chain_offset)).rjust(10))
3 for phrase_index, phrase in enumerate(chain):
4 print('{ind}{phrase}'.format(
5 ind=hex(phrase_index).ljust(4),
6 phrase=hex(phrase)
7 ))
Printing the chains
chains in terminal
A chain containing the blast beat phrase

Putting it all together

After working out most of the kinks in the script I decided to bite the bullet and actually see if everything I programmed would actual result in something resembling the song… and it did!

An example of the drum beat

Aside from the fact that I had no mental map of what phrases contained what notes (as this was being taken care of by the computer) it took just over 2 hours to program the drums and two pulse channel instruments for the cover and when I pressed play for the first time it worked perfectly.

In order to make it easier to offset the phrases and chains when adding multiple channels I ended up building some command line arguments which handled this but as the LSDJ numbers are in hex I had to do some conversion before putting the numbers in.

If I’m perfectly honest I was absolutely amazed it worked first time, I had expected it to fail but my fingers were glad that I didn’t need to spend another 2 hours programming in notes!

I then set to work on recording a separate bass track and solo track as well and recording the drums and guitar tracks using my half speed gameboy, mixed it all together.

The end result of this process can be found below:

The final song!

Next Steps

The script is by no means perfect and I’ve got a list of improvements I’d love to make:

  • Triplets — Certain songs utilise these (especially in solos) and I’d like to automate this as it requires running the commands at double speed
  • Chords — It’d be great to take all the notes being played at a certain tick and automate the creation of a table to play the chorded notes
  • Multi-channel import — Instead of processing a file at a time instead either process a multi-channel MIDI file or separate files but via a YAML file or something as that will make it easier to use
  • A JSON/HTML display — I’d like to turn the script into a web service where the output is JSON that could then be fed into a React display. My aim would be that anyone can upload a MIDI file and get the LSDJ commands they need to put it on their Gameboy
  • LSDJ save file generation — There’s a Python library for LSDJ save files which I could use, ultimately saving even more time as I just need to add this to my cartridge and press play
  • Publish it — So others can use it!