ChiptuneSAK Examples¶
Contents
Chirp Examples¶
MS-DOS Game MIDI Example¶
In this example a midi file captured from an MS-DOS game is processed and turned into sheet music as well as exported to GoatTracker.
Usually, midi captured from DOS games results in messy midi files that don’t include keys, time signatures, or even reliable ticks per quarter notes.
So first use the FitPPQ.py script to estimate the true note lengths and adjust them to a ppq of 960. From the tools directory, run:
FitPPQ.py -s 4.0 ../examples/data/mercantile/betrayalKrondorMercantile.mid ../examples/data/mercantile/tmp.mid
This should generate the following output:
Reading file ../examples/data/mercantile/betrayalKrondorMercantile.mid
Finding initial parameters...
Refining...
scale_factor = 5.8900000, offset = 2398, total error = 4082.2 ticks (22.51 ticks/note for ppq = 960)
Writing file ../examples/data/mercantile/tmp.mid
It is a good idea to do a sanity check on the output file, as the algorithm in FitPPQ often fails to give the best solution. A general algorithm to find the beats in a midi file is a daunting task!
In fact, an ideal method now is to use the output obtained from FitPPQ, open the resulting file and adjust the first beat of the final measure to lie exactly at the start of the final measure. If we do this with tmp.mid, we find that the first note of the final measure is at MIDI tick 226,588 for measure 60. For a PPQ of 960 and 4 quarter notes per measure, the last measure should start at tick 960 * 59 * 4 = 226,560, so we are coming in only 28 ticks late. Since we plan to quantize to a 16th note (960 / 4 = 240 ticks) then the value we found should be fine.
Now you can use those parameters (5.89 and 2398) to scale the mercantile file in the Python script, which generates Lilypond sheet music and a GoatTracker SNG file. Note that because you need to move the music to an earlier time, the offset you give to the move_ticks() method will be negative.
import subprocess
import chiptunesak
import chiptunesak.base
from chiptunesak.constants import project_to_absolute_path
"""
This example processes a MIDI file captured from Betrayal at Krondor to both sheet music and
a GoatTracker song.
It is an example of extremely complex music processing, done entirely in ChiptuneSAK.
A program called MidiEditor (windows / linux, https://www.midieditor.org/), was used to
inspect the MIDI file, evaluate and plan the required transformations, and verify the results.
It shows the steps needed for this conversion:
1. Remove unused tracks, reorder and rename tracks to use
2. Consolidate two tracks into one, changing instruments partway through
3. Scale, move and adjust the note data to correspond to musical notes and durations
4. Set minimum note lengths, quantize the song, and remove polyphony
5. Truncate the captured song to a reasonable stopping point
6. Convert the ChirpSong to an MChirpSong
7. Use the Lilypond I/O object to write lilypond markup for the piece
8. Convert the ChirpSong to an RChirpSong
9. Assign GoatTracker instruments to the voices
10. Find repeated loops and compress the song
11. Export the GoatTracker .sng file
"""
output_folder = str(project_to_absolute_path('examples\\data\\mercantile')) + '\\'
input_folder = output_folder
input_file = str(project_to_absolute_path(input_folder + 'betrayalKrondorMercantile.mid'))
output_midi_file = str(project_to_absolute_path(output_folder + 'mercantile.mid'))
output_ly_file = str(project_to_absolute_path(output_folder + 'mercantile.ly'))
output_gt_file = str(project_to_absolute_path(output_folder + 'mercantile.sng'))
# Read in the original MIDI to Chirp
chirp_song = chiptunesak.MIDI().to_chirp(input_file)
# First thing, we rename the song
chirp_song.metadata.name = "Betrayal at Krondor - Mercantile Theme"
chirp_song.metadata.composer = "Jan Paul Moorhead"
print(f'Original song:')
print(f'#tracks = {len(chirp_song.tracks)}')
print(f' ppq = {chirp_song.metadata.ppq}')
print(f' tempo = {chirp_song.metadata.qpm} qpm')
print('Track names:')
print('\n'.join(f'{i+1}: {t.name}' for i, t in enumerate(chirp_song.tracks)))
print()
# Truncate to 4 tracks and re-order from melody to bass
chirp_song.tracks = [chirp_song.tracks[j] for j in [3, 1, 2, 0]]
# Truncate the notes in track 3 when the bass line starts
chirp_song.tracks[2].truncate(9570)
# Get rid of any superfluous program changes in the tracks
for t in chirp_song.tracks:
t.set_program(t.program_changes[-1].program)
# Change the program to the bass at that point
tmp_program = chirp_song.tracks[3].program_changes[0]
new_program = chiptunesak.base.ProgramEvent(9700, tmp_program.program)
chirp_song.tracks[2].program_changes.append(new_program)
# Now move the notes from track 4 into track 3
chirp_song.tracks[2].notes.extend(chirp_song.tracks[3].notes)
# This is a 1-SID song, so only three voices allowed.
# Delete any extra tracks and name the rest.
chirp_song.tracks = chirp_song.tracks[:3]
chirp_song.tracks[0].name = 'Ocarina'
chirp_song.tracks[1].name = 'Guitar'
chirp_song.tracks[2].name = 'Strings/Bass'
# At this point, with the tracks arranged, run the FitPPQ.py program in the tools directory.
# Result, after some fiddling (and FitPPQ can be *very* fiddly):
# best fit scale_factor = 5.89, offset = 2398
chirp_song.move_ticks(-2398)
chirp_song.scale_ticks(5.89000)
chirp_song.metadata.ppq = 960
# Now get rid of the very weird short notes in the flute part; set minimum length to an eighth note
chirp_song.tracks[0].set_min_note_len(480)
# Quantize the whole song to eighth notes
chirp_song.quantize_from_note_name('8')
# Now we can safely remove any polyphony
chirp_song.remove_polyphony()
# The song is repetitive. Pick a spot to truncate.
chirp_song.truncate(197280)
# Set the key (D minor)
chirp_song.set_key_signature('Dm')
print(f'Modified song:')
print(f'#tracks = {len(chirp_song.tracks)}')
print(f' ppq = {chirp_song.metadata.ppq}')
print(f' tempo = {chirp_song.metadata.qpm} qpm')
print('Track names:')
print('\n'.join(f'{i+1}: {t.name}' for i, t in enumerate(chirp_song.tracks)))
print()
# Save the result to a MIDi file.
chiptunesak.MIDI().to_file(chirp_song, output_midi_file)
# Convert to MChirp
mchirp_song = chirp_song.to_mchirp()
# Make sheet music output with Lilypond
ly = chiptunesak.Lilypond()
ly.to_file(mchirp_song, output_ly_file)
# If you have Lilypond installed, generate the pdf
# If you do not have Lilypond installed, comment the following line out
subprocess.call('lilypond -o %s %s' % (output_folder, output_ly_file), shell=True)
# Now convert the song to RChirp
rchirp_song = chirp_song.to_rchirp(arch='PAL-C64')
# Let's see what programs are used
# print(rchirp_song.program_map)
# Gives {79: 1, 24: 2, 48: 3, 32: 4}
# From General Midi,
# 79 = Ocarina Flute.ins
# 24 = Acoustic Guitar (Nylon) MuteGuitar.ins
# 48 = String Ensemble 1 SimpleTriangle.ins
# 32 = Acoustic Bass SoftBass.ins
#
instruments = ['Flute', 'MuteGuitar', 'SimpleTriangle', 'SoftBass']
# Perform loop-finding to compress the song and to take advantage of repetition
# The best minimum pattern length depends on the particular song.
print('Compressing RChirp')
compressor = chiptunesak.OnePassLeftToRight()
rchirp_song = compressor.compress(rchirp_song, min_length=16)
# Now export the compressed song to goattracker format.
print(f'Writing {output_gt_file}')
GT = chiptunesak.GoatTracker()
GT.to_file(rchirp_song, output_gt_file, instruments=instruments)
Chord Splitting¶
In this example, the midi music with chord-based polyphony in one track is turned into a stereo GoatTracker song.
Using the same method as above, the scale factor and offset are determined and the chirp is scaled to make the notes fit into measures. One of the tracks has chords made of 3 notes, so the ChirpSong.explode_polyphony() method is used to turn the single track into three tracks without polyphony.
These three tracks are the used along with the two other original tracks to form a song with 5-voice polyphony, which is then exported to a stereo GoatTracker song.
import copy
import chiptunesak
from chiptunesak.constants import project_to_absolute_path
"""
This example processes a MIDI file captured from Secret of Monkey Island to a GoatTracker song.
It shows the steps needed for this conversion:
1. Scale and adjust the note data to correspond to musical notes and durations
2. Split a track with chords into 3 separate tracks
3. Assign GoatTracker instruments to the voices
4. Export the 5-track to a stereo GoatTracker .sng file
"""
input_file = str(project_to_absolute_path('examples/data/lechuck/MonkeyIsland_LechuckTheme.mid'))
output_midi_file = str(project_to_absolute_path('examples/data/lechuck/LeChuck.mid'))
output_gt_file = str(project_to_absolute_path('examples/data/lechuck/LeChuck.sng'))
chirp_song = chiptunesak.MIDI().to_chirp(input_file)
print(f'Original song:')
print(f'#tracks = {len(chirp_song.tracks)}')
print(f' ppq = {chirp_song.metadata.ppq}')
print(f' tempo = {chirp_song.metadata.qpm} qpm')
print('Track names:')
print('\n'.join(f'{i+1}: {t.name}' for i, t in enumerate(chirp_song.tracks)))
print()
# First thing, we rename the song
chirp_song.metadata.name = "Monkey Island - LeChuck Theme"
print('Truncating original song...')
chirp_song.truncate(21240)
# Now select and order the tracks the way we want them, which is the reverse of the midi we got.
print('Selecting and ordering tracks...')
tracks = [copy.copy(chirp_song.tracks[i]) for i in [3, 2, 1]]
chirp_song.tracks = tracks
print(f'Now {len(chirp_song.tracks)} tracks')
# Now given the tracks the names we want them to have, because the track names in the original midi were
# used for information that is supposed to go elsewhere in midi files.
print('Renaming tracks...')
for t, n in zip(chirp_song.tracks, ['Lead', 'Chord', 'Bass']):
t.name = n
print('Tracks:')
print('\n'.join(f' {t.name}' for t in chirp_song.tracks))
print()
print('Adjusting ppq and tempo...')
# Experimentally determine ticks per measure
# - Counted measures by hand listening to the music. We identified the note at the start of measure 21
# (the later the better to give a good average) which was at tick 19187
# - 19187 / 20 = 959.35
# Very close to 960 ticks/measure.
# Any small error here will be fixed by our quantization later
# We desire our new song to use a standard 960 ppq and 4 notes per measure, so we scale the ticks by 4
# (assuming 9 quarter notes per measure)
chirp_song.scale_ticks(4.0)
chirp_song.metadata.ppq = 960 # The original ppq is meaningless; it was just the ppq of the midi capture program
# New tempo: original tempo was 240 qpm where ppq was given as 192 which makes 240 * 192 / 60 = 768 ticks/sec
# We scaled the ticks (and the tempo) by a factor of 4 so now we need 768 * 4 = 3072 ticks/sec
# For a quarter note = 960 ticks that comes out to 3072 / 960 = 3.2 qps * 60 = 192
chirp_song.set_qpm(192)
# Looking at the midi and listening to the song, the best quantization appears to be eighth notes.
chirp_song.quantize_from_note_name('8')
# Track 2 has chords in it that have 3 notes at a time. We need to move those to separate voices, so
# we split that track:
print('Exploding polyphony of chord track...')
chirp_song.explode_polyphony(1)
print(f'Now {len(chirp_song.tracks)} tracks')
print('Tracks:')
print('\n'.join(f' {t.name}' for t in chirp_song.tracks))
print()
# Any other polyphony is unintentional so make sure it is all gone (in particular, one note in the bass line
# seems to make a chord, but it's not real.
print('Removing remaining polyphony')
chirp_song.remove_polyphony()
# Now export the modified chirp to a new midi file, which can be viewed and should look nice and neat
print(f'Writing to MIDI file {output_midi_file}')
chiptunesak.MIDI().to_file(chirp_song, output_midi_file)
# Now set the instrument numbers for the GoatTracker song.
# Since we want control over the instruments we specify the GT ones in track order.
print(f'Setting GoatTracker instruments...')
for i, program in enumerate([1, 2, 2, 2, 3]):
chirp_song.tracks[i].set_program(program)
# Now that everything is C64 compatible, we convert the song to RChirp format.
print(f'Converting ChirpSong to RChirpSong...')
rchirp_song = chiptunesak.RChirpSong(chirp_song)
# Perform loop-finding to compress the song and to take advantage of repetition
# The best minimum pattern length depends on the particular song. For this one we chose 16 rows.
print('Compressing RChirp')
compressor = chiptunesak.OnePassLeftToRight()
rchirp_song = compressor.compress(rchirp_song, min_length=16)
# Now export the compressed song to goattracker format.
print(f'Writing GoatTracker file {output_gt_file}')
GT = chiptunesak.GoatTracker()
GT.set_options(instruments=['LeChuckLead', 'C128Xylophone', 'LeChuckBass'])
GT.to_file(rchirp_song, output_gt_file)
Lilypond Sheet Music Examples¶
Lilypond Song to PDF¶
In this example a MIDI song is read in and output to a multi-page PDF document.
As mentioned above, midi ripped from MS-DOS games results in messy midi files. This example workflow shows how to turn such music into Lilypond-generated sheet music, and will use a piece of music from an MS-DOS RPG Betrayal At Krondor (Sierra On-Line, 1993).
import os
import subprocess
import chiptunesak
from chiptunesak.constants import project_to_absolute_path
"""
This example shows how to process a song into PDF file using Lilypond using the following steps:
1. Import the song to chirp format from a MIDI file, quantizing the notes to the nearest 32nd note
2. Convert the song to mchirp format
3. Save the lilypond source
4. Run the lilypond converter from within python to generate the PDF file.
"""
output_folder = str(project_to_absolute_path('examples\\data\\lilypond')) + '\\'
input_folder = str(project_to_absolute_path('examples\\data\\common')) + '\\'
input_mid_file = input_folder + 'BWV_799.mid'
output_ly_file = output_folder + 'BWV_799.ly'
# Read in the MIDI song and quantize
chirp_song = chiptunesak.MIDI().to_chirp(input_mid_file, quantization='32', polyphony=False)
# It's in A minor, 3/8 time
chirp_song.set_key_signature('Am')
chirp_song.set_time_signature(3, 8)
# Convert to mchirp
mchirp_song = chirp_song.to_mchirp()
# Write it straight to a file using the Lilypond class with format 'song' for the entire song.
chiptunesak.Lilypond().to_file(mchirp_song, output_ly_file, format='song')
# Change directory to the data directory so we don't fill the source directory with intermediate files.
os.chdir(output_folder)
# Adjust the path the the file
ly_file = os.path.basename(output_ly_file)
# Run lilypond
subprocess.call('lilypond -o %s %s' % (output_folder, output_ly_file), shell=True)
Lilypond Measures to PNG¶
In this example a MIDI song is read, and a snippet of measures is converted to a PNG image.
Often, you’d like to turn a small clip from a song into an image to use as an illustration for a document. In this case, you may not want the entire piece exported as a pdf file, but just the clip.
Currently, ChiptuneSAK can only extract measures for a clip from a single voice.
This example gives the following output:
import os
import subprocess
import chiptunesak
from chiptunesak.constants import project_to_absolute_path
"""
This example shows how to process a clip of a song into a PNG file using Lilypond using the following steps:
1. Import the song to chirp format from a MIDI file, quantizing the notes to the nearest 16th note
2. Convert the song to mchirp format
3. Select the measures for the clip
4. Save the lilypond source
5. Run the lilypond converter from within python to generate the PNG file.
"""
output_folder = str(project_to_absolute_path('examples\\data\\lilypond')) + '\\'
input_folder = output_folder
input_file = input_folder + 'BWV_775.mid'
output_ly_file = output_folder + 'BWV_775.ly'
# Read in the MIDI song and quantize
chirp_song = chiptunesak.MIDI().to_chirp(input_file, quantization='16', polyphony=False)
# Convert to mchirp
mchirp_song = chirp_song.to_mchirp()
# Create the LilyPond I/O object
lp = chiptunesak.Lilypond()
# Set the format to do a clip and set the measures to the clip we want
lp.set_options(format='clip', measures=mchirp_song.tracks[0].measures[3:8])
# Write it straight to a file
lp.to_file(mchirp_song, output_ly_file)
# Change directory to the data directory so we don't fill the source directory with intermediate files.
os.chdir(output_folder)
# Adjust the path the the file
ly_file = os.path.basename(output_ly_file)
# Run lilypond
args = ['lilypond', '-ddelete-intermediate-files', '-dbackend=eps', '-dresolution=600', '--png', ly_file]
subprocess.call(args, shell=True)
C128 Basic Example¶
In this example a MIDI song is read and converted to C128 BASIC:
import chiptunesak
from chiptunesak.constants import project_to_absolute_path
"""
This example shows how to convert a 3-voice song to C128 Basic:
1. Import the song to chirp format from a MIDI file, quantizing the notes to the nearest 32nd note
2. Since C128 BASIC cannot do notes shorter than a 16th note, perform a metric modulation to double note lengths
3. Convert the song to mchirp format
3. Save the BASIC as source
4. Save the BASIC as a prg file
"""
output_folder = str(project_to_absolute_path('examples\\data\\C128')) + '\\'
input_folder = str(project_to_absolute_path('examples\\data\\common')) + '\\'
input_mid_file = input_folder + 'BWV_799.mid'
output_bas_file = output_folder + 'BWV_799.bas'
output_prg_file = output_folder + 'BWV_799.prg'
# Read in the MIDI song and quantize
chirp_song = chiptunesak.MIDI().to_chirp(input_mid_file, quantization='32', polyphony=False)
# When imported, the shortest note is a 32nd note, which is too fast for C128 BASIC.
# Perform a metric modulation by making every note length value twice as long, but
# increasing the tempo by the same factor so it sounds the same. Now the shortest
# note will be a 16th note which the C128 BASIC can play.
print('Modulating music...')
chirp_song.modulate(2, 1)
# Convert to mchirp
print('Converting to MChirp...')
mchirp_song = chirp_song.to_mchirp()
# Write .bas and .prg files
exporter = chiptunesak.C128Basic()
exporter.set_options(instruments=['trumpet', 'guitar', 'guitar'])
print(f'Writing {output_bas_file}...')
exporter.to_file(mchirp_song, output_bas_file, format='bas')
print(f'Writing {output_prg_file}...')
exporter.to_file(mchirp_song, output_prg_file, format='prg')
Metric Modulation Examples¶
Fix too-short note durations¶
examples/data/C128/BWV_799.mid is a three-part Bach invention. It contains a few 32nd notes near the end.
Unfortunately, C128 BASIC only supports notes down to 16th notes, so exporting this piece to C128 BASIC without loss of those notes is not possible without metric modulation.
In the C128 Basic Example, the line
chirp_song.modulate(2, 1)
Makes all the notes 2/1 = 2X as long, so the 32nd notes turn into 16th notes. The tempo is changed to compensate so the song sounds correct. Exporting the song to C128 BASIC now works correctly.
Eliminate triplets¶
Many chiptunes music players do not support triplets. Here we show you how to use metric modulation to eliminate triplets.
It may seem a little surprising, but modulation by a factor of 3/2 eliminates all triplets.
As an example, consider the following excerpt from a Chopin waltz:
This excerpt could not be processed by tools that only allow binary note divisions. But if we modulate by a factor of 3/2, the excerpt becomes:
The shortest note is now a sixteenth note, which means this music can now be rendered by a system that only accepts factor-of-two note values!