Skip to content

Commit 3231551

Browse files
committed
Added midi conversion script
Added script for converting midi to the tone format used by the jingle player Signed-off-by: Jared Baumann <[email protected]>
1 parent ca65359 commit 3231551

File tree

2 files changed

+178
-0
lines changed

2 files changed

+178
-0
lines changed

scripts/midi_tone_bin.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import argparse, os, struct, re
2+
from time import sleep
3+
from tqdm import tqdm
4+
import pretty_midi as pm
5+
import serial
6+
7+
MIN_NOTE = 147
8+
9+
# General midi instrument table
10+
general_midi_table = {
11+
0: "Acoustic Grand", 32: "Acoustic Bass", 64: "Soprano Sax", 96: "FX 1 (rain)",
12+
1: "Bright Acoustic", 33: "Electric Bass(finger)", 65: "Alto Sax", 97: "FX 2 (soundtrack)",
13+
2: "Electric Grand", 34: "Electric Bass(pick)", 66: "Tenor Sax", 98: "FX 3 (crystal)",
14+
3: "Honky-Tonk", 35: "Fretless Bass", 67: "Baritone Sax", 99: "FX 4 (atmosphere)",
15+
4: "Electric Piano 1", 36: "Slap Bass 1", 68: "Oboe", 100: "FX 5 (brightness)",
16+
5: "Electric Piano 2", 37: "Slap Bass 2", 69: "English Horn", 101: "FX 6 (goblins)",
17+
6: "Harpsichord", 38: "Synth Bass 1", 70: "Bassoon", 102: "FX 7 (echoes)",
18+
7: "Clav", 39: "Synth Bass 2", 71: "Clarinet", 103: "FX 8 (sci-fi)",
19+
8: "Celesta", 40: "Violin", 72: "Piccolo", 104: "Sitar",
20+
9: "Glockenspiel", 41: "Viola", 73: "Flute", 105: "Banjo",
21+
10: "Music Box", 42: "Cello", 74: "Recorder", 106: "Shamisen",
22+
11: "Vibraphone", 43: "Contrabass", 75: "Pan Flute", 107: "Koto",
23+
12: "Marimba", 44: "Tremolo Strings", 76: "Blown Bottle", 108: "Kalimba",
24+
13: "Xylophone", 45: "Pizzicato Strings", 77: "Shakuhachi", 109: "Bagpipe",
25+
14: "Tubular Bells", 46: "Orchestral Harp", 78: "Whistle", 110: "Fiddle",
26+
15: "Dulcimer", 47: "Timpani", 79: "Ocarina", 111: "Shanai",
27+
16: "Drawbar Organ", 48: "String Ensemble 1", 80: "Lead 1 (square)", 112: "Tinkle Bell",
28+
17: "Percussive Organ", 49: "String Ensemble 2", 81: "Lead 2 (sawtooth)", 113: "Agogo",
29+
18: "Rock Organ", 50: "SynthStrings 1", 82: "Lead 3 (calliope)", 114: "Steel Drums",
30+
19: "Church Organ", 51: "SynthStrings 2", 83: "Lead 4 (chiff)", 115: "Woodblock",
31+
20: "Reed Organ", 52: "Choir Aahs", 84: "Lead 5 (charang)", 116: "Taiko Drum",
32+
21: "Accordion", 53: "Voice Oohs", 85: "Lead 6 (voice)", 117: "Melodic Tom",
33+
22: "Harmonica", 54: "Synth Voice", 86: "Lead 7 (fifths)", 118: "Synth Drum",
34+
23: "Tango Accordion", 55: "Orchestra Hit", 87: "Lead 8 (bass+lead)", 119: "Reverse Cymbal",
35+
24: "Acoustic Guitar(nylon)", 56: "Trumpet", 88: "Pad 1 (new age)", 120: "Guitar Fret Noise",
36+
25: "Acoustic Guitar(steel)", 57: "Trombone", 89: "Pad 2 (warm)", 121: "Breath Noise",
37+
26: "Electric Guitar(jazz)", 58: "Tuba", 90: "Pad 3 (polysynth)", 122: "Seashore",
38+
27: "Electric Guitar(clean)", 59: "Muted Trumpet", 91: "Pad 4 (choir)", 123: "Bird Tweet",
39+
28: "Electric Guitar(muted)", 60: "French Horn", 92: "Pad 5 (bowed)", 124: "Telephone Ring",
40+
29: "Overdriven Guitar", 61: "Brass Section", 93: "Pad 6 (metallic)", 125: "Helicopter",
41+
30: "Distortion Guitar", 62: "SynthBrass 1", 94: "Pad 7 (halo)", 126: "Applause",
42+
31: "Guitar Harmonics", 63: "SynthBrass 2", 95: "Pad 8 (sweep)", 127: "Gunshot",
43+
}
44+
45+
parser = argparse.ArgumentParser(prog='midi_tone_bin')
46+
47+
parser.add_argument('action', choices=['list', 'convert'])
48+
parser.add_argument('filename')
49+
parser.add_argument('-i', '--instruments', default=None, help='Indices of instruments to include')
50+
parser.add_argument('-o', '--output', default=None, help='Output file name')
51+
parser.add_argument('-s', '--serial', default=None, help='Serial device to write the file to')
52+
parser.add_argument('-b', '--baud', default=9600, help='Serial baud rate')
53+
54+
args = parser.parse_args()
55+
56+
if not os.path.exists(args.filename):
57+
print(f"No such file {args.filename}")
58+
parser.print_usage()
59+
exit(1)
60+
61+
mid = pm.PrettyMIDI(args.filename)
62+
63+
instruments = None
64+
65+
if args.instruments != None and args.instruments != '*':
66+
instruments = [int(i) for i in args.instruments.split(',')]
67+
68+
def list_instruments(mid : pm.PrettyMIDI):
69+
for i, ins in enumerate(mid.instruments):
70+
ins : pm.Instrument
71+
print('{:3}) Instrument {:03}: [{}] ({} Notes) {}'.format(
72+
i, ins.program, general_midi_table.get(ins.program, 'UNKNOWN'),
73+
len(ins.notes), '(is drum)' if ins.is_drum else ''
74+
))
75+
76+
if args.action == 'list':
77+
list_instruments(mid)
78+
exit(0)
79+
80+
def midi_note_to_freq(m : int) -> float:
81+
return (2 ** ((m - 69) / 12)) * 440
82+
83+
flattened_tracks = []
84+
# Flatten tracks
85+
86+
if instruments != None:
87+
used_instruments = [mid.instruments[i] for i in instruments]
88+
else:
89+
used_instruments = mid.instruments
90+
91+
used_instruments : list[pm.Instrument]
92+
all_notes : list[tuple[pm.Note, pm.Instrument]] = []
93+
for instrument in used_instruments:
94+
all_notes += [(n, instrument) for n in instrument.notes]
95+
96+
all_notes = sorted(all_notes, key=lambda x: x[0].start)
97+
note_n = 0
98+
99+
flattened_song : list[tuple[int,int]] = []
100+
101+
while note_n < len(all_notes):
102+
103+
n, i = all_notes[note_n]
104+
if i.is_drum:
105+
hcn = -1
106+
else:
107+
hcn = n.pitch
108+
dur = n.duration
109+
110+
# Prefer the highest note if multiple notes arrive at once
111+
while note_n + 1 < len(all_notes) and all_notes[note_n][0].start == all_notes[note_n + 1][0].start:
112+
note_n += 1
113+
n, i = all_notes[note_n]
114+
if not i.is_drum:
115+
if n.pitch > hcn:
116+
dur = n.duration
117+
hcn = n.pitch
118+
# If we're a drum, emulate with a short-pulsed low note
119+
if hcn == -1:
120+
f = MIN_NOTE
121+
dur = 20 / 1000
122+
else:
123+
f = midi_note_to_freq(hcn)
124+
f = max(MIN_NOTE, f)
125+
126+
n, i = all_notes[note_n]
127+
time = n.start
128+
129+
# Determine time till next note, pause/adjust duration of current note accordingly
130+
nnt = all_notes[note_n + 1][0].start if note_n + 1 < len(all_notes) else time + dur
131+
dur = min(nnt - time, dur)
132+
pdur = nnt - time - dur
133+
134+
# Convert durations to integer milliseconds and frequency to integer Hz
135+
pdur = int(pdur * 1000)
136+
flattened_song.append((int(f), int(dur * 1000)))
137+
138+
# If a pause should occur, insert it
139+
if pdur:
140+
flattened_song.append((0, pdur))
141+
142+
note_n += 1
143+
144+
# Convert to the double short representation, big endian
145+
song_ns = [struct.pack('!2H', i, j) for i, j in flattened_song]
146+
147+
output_file = args.output
148+
149+
#If no serial is specified, right to file on the local system
150+
if not args.serial:
151+
output_file = "out.bin" if output_file == None else output_file
152+
with open(output_file, 'wb+') as f:
153+
for i in song_ns:
154+
f.write(i)
155+
else: # Otherwise, right to the device's file system
156+
num_notes = len(song_ns)
157+
output_file = "/tmo/out.bin" if output_file == None else output_file
158+
with serial.Serial(args.serial, args.baud, timeout=5) as port:
159+
print("Writing binary to board...")
160+
bar = tqdm(total=num_notes, unit='Note(s)')
161+
port.write(f'fs rm "{output_file}"\r'.encode('ascii'))
162+
base_command = f'fs write "{output_file}" %s'
163+
port.write(b'\r')
164+
port.flushInput()
165+
166+
for ns in song_ns:
167+
command = base_command % ' '.join(re.findall('..?', ns.hex()))
168+
port.write(command.encode('ascii'))
169+
port.write(b'\r')
170+
while not port.in_waiting:
171+
sleep(.001)
172+
while port.in_waiting:
173+
port.readline()
174+
bar.update(1)
175+
bar.close()

scripts/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pyserial
2+
pretty_midi
3+
tqdm

0 commit comments

Comments
 (0)