freude/csv2wav.py

97 lines
3.3 KiB
Python
Executable file

#!/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))
)