|
| 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() |
0 commit comments