97 lines
3.3 KiB
Python
Executable file
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))
|
|
)
|