add midi2csv.py and csv2wav.py

This commit is contained in:
Thomas Lindner 2024-12-29 17:58:27 +01:00
parent f5515a4666
commit 4c6e790127
2 changed files with 141 additions and 0 deletions

96
csv2wav.py Executable file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python
import argparse
import csv
from math import pi, sin
import sys
import wave
parser = argparse.ArgumentParser()
parser.add_argument("input", help="input CSV (pitch, duration, pause)")
parser.add_argument("-o", "--output", default="/dev/stdout", help="output file (wav)")
parser.add_argument("--framerate", default=48000, type=int, help="framerate")
parser.add_argument("--bpm", default=60, type=float, help="beats per minute")
args = parser.parse_args()
# starting from C3 = 130.81 Hz
octave = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"]
frequencies = {
f"{octave[i % 12]}{i // 12 + 3}": 2 ** (i / 12) * 130.81 for i in range(36)
}
# duration of 1/16th beat
base_duration = 60 / args.bpm / 16
with wave.open(args.output, "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(1)
wav.setframerate(args.framerate)
with open(args.input, "r") as notes:
for pitch, duration, pause in csv.reader(notes):
if pitch == "":
# skip comment
continue
try:
frequency = frequencies[pitch]
except KeyError:
sys.exit("pitch goes from c3 to b5")
duration = int(duration)
pause = int(pause)
if duration < 1 or pause < -1:
sys.exit("duration/pause are integer multiples of 1/16th beat")
attack_samples = int(0.1 * args.framerate)
duration_samples = int(
max(0, duration * base_duration - 0.2) * args.framerate
)
wav.writeframes(
bytes(
[
int(
127
* i
* sin(2 * pi * frequency * i / args.framerate)
/ attack_samples
) % 256
for i in range(attack_samples)
]
)
)
wav.writeframes(
bytes(
[
int(
127
* sin(
2
* pi
* frequency
* (i + attack_samples)
/ args.framerate
)
) % 256
for i in range(duration_samples)
]
)
)
wav.writeframes(
bytes(
[
int(
127
* (attack_samples - i)
* sin(
2
* pi
* frequency
* (i + attack_samples + duration_samples)
/ args.framerate
)
/ attack_samples
) % 256
for i in range(attack_samples)
]
)
)
if pause != -1:
wav.writeframes(
bytes([0] * int(pause * base_duration * args.framerate))
)

45
midi2csv.py Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
import argparse
import csv
import mido
import sys
parser = argparse.ArgumentParser()
parser.add_argument("input", help="input file (midi)")
parser.add_argument(
"-o", "--output", default="/dev/stdout", help="output CSV (pitch, duration, pause)"
)
parser.add_argument("-t", "--track", default=0, type=int, help="track number")
parser.add_argument("-c", "--channel", default=0, type=int, help="channel number")
args = parser.parse_args()
octave = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"]
pitches = [f"{octave[i % 12]}{i // 12 + 3}" for i in range(36)]
midifile = mido.MidiFile(args.input)
time = 0
note = 0
time_on = 0
time_off = 0
with open(args.output, "w") as notes:
writer = csv.writer(notes)
for event in midifile.tracks[args.track]:
writer.writerow(["", "", event])
# convert time to 1/16th beats
time += event.time * 16 // midifile.ticks_per_beat
if event.type == "note_on" and event.channel == args.channel:
# if mulitple notes start at the same time, play the highest
if event.velocity != 0 and (time_on != time or note < event.note):
if time_off <= time_on:
time_off = time
if note != 0 and time_off - time_on > 0:
writer.writerow(
[pitches[note - 60], time_off - time_on, time - time_off]
)
note = event.note
if note - 60 < 0 or note - 60 >= len(pitches):
sys.exit("note out of range c3 to b5")
time_on = time
elif note == event.note:
time_off = time
writer.writerow([pitches[note - 60], time - time_on, 0])