#!/usr/bin/env python import argparse import csv import sys parser = argparse.ArgumentParser() parser.add_argument("input", nargs="+", help="input CSV (pitch, duration, pause)") parser.add_argument("-o", "--output", default="/dev/stdout", help="output file (gcode)") parser.add_argument( "--steps-per-mm", default=5, type=float, help="motor steps per mm (actually an arbitrary scaling factor)", ) parser.add_argument("--bpm", default=60, type=float, help="beats per minute") args = parser.parse_args() # starting from C3 = 130.81 Hz pitches = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"] frequencies = { f"{pitches[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 open(args.output, "w") as f: f.write("G21 ; set units to millimeters\n") f.write("G90 ; use absolute coordinates\n") f.write("G0 Z10 F300\n") f.write("G0 X0 Y0 F3000\n") f.write("G4 S2 ; wait 2 seconds\n") position = 0 for filename in args.input: with open(filename, "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 < 0: sys.exit("duration/pause are integer multiples of 1/16th beat") feedrate = int(frequency * 60 / args.steps_per_mm) length = duration * base_duration / 60 * feedrate position += -length if position > 100 else length if position < 0 or position > 200: sys.exit("does not fit in 200x200") pause_ms = int(pause * base_duration) f.write(f"G0 X{position:.6f} Y{position:.6f} F{feedrate}\n") f.write(f"G4 P{pause_ms}\n") f.write("G4 S2 ; wait 2 seconds\n")