-
Notifications
You must be signed in to change notification settings - Fork 0
/
audio.py
269 lines (232 loc) · 9.05 KB
/
audio.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#from MIDI import MIDIFile
from time import sleep
import signal
import sys
import argparse
from pathlib import Path
DEBUG = False
NOPWM = False
pwm = None
class PWMAudio:
chip = 0
device = 0
PWMEXPORT = "/sys/class/pwm/pwmchip%d/export"
PWMCLASS = "/sys/class/pwm/pwmchip%d/pwm%d/%s"
ENABLE = "enable"
PERIOD = "period"
DUTY_CYCLE = "duty_cycle"
DC = 0.5 # fixed
enabled = False
def __init__(self, chip, device):
self.chip = chip
self.device = device
self.export()
self.disable()
def pwmdevice(self, end):
return self.PWMCLASS % (self.chip, self.device, end)
def export(self):
# check if exists
pwmpath = Path(self.PWMEXPORT[:-6] % (self.chip) + "/pwm%d" % (self.device))
if pwmpath.is_dir():
return
with open(self.PWMEXPORT % self.chip, 'wb') as f:
f.write(b"%d" % self.device)
f.flush()
def enable(self, enable=True):
self.enabled = enable
if self.period == 0: # period needs to be set otherwise errors will be thrown
self.set(1000)
with open(self.pwmdevice(self.ENABLE), "wb") as f:
f.write(b"1" if enable else b"0")
f.flush
def disable(self):
self.enable(enable=False)
@property
def period(self):
with open(self.pwmdevice(self.PERIOD), "rb") as f:
return int(f.read())
@period.setter
def period(self, period):
with open(self.pwmdevice(self.PERIOD), "wb") as f:
f.write(b"%d" % period)
f.flush()
@property
def duty_cycle(self):
with open(self.pwmdevice(self.DUTY_CYCLE), "rb") as f:
return int(f.read())
@duty_cycle.setter
def duty_cycle(self, dc):
with open(self.pwmdevice(self.DUTY_CYCLE), "wb") as f:
f.write(b"%d" % dc)
f.flush()
def set(self, frequency):
period = 1000000000 / frequency
dc = int(period * self.DC)
if period < self.duty_cycle:
self.duty_cycle = dc
self.period = period
else:
self.period = period
self.duty_cycle = dc
# needed due to how it's played
# def midinote(note, reference=440):
# frequency = midinumber_to_frequency(note, reference)
# self.set(frequency)
def midinumber_to_frequency(number, reference=440, pitch=0):
if pitch == 0:
return (reference / 32) * (2 ** ((number - 9) / 12))
elif pitch < 0:
freq = (reference / 32) * (2 ** ((number - 9) / 12))
down = (reference / 32) * (2 ** ((number - 9 - 12) / 12))
return freq - ((freq - down) * (pitch / 8192))
else:
up = (reference / 32) * (2 ** ((number - 9 + 12) / 12))
freq = (reference / 32) * (2 ** ((number - 9) / 12))
return freq + ((up - freq) * (pitch / 8192))
def midinote_to_number(note, octave):
m = {
'cb': -1,
'c': 0,
'c#': 1,
'db': 1,
'd': 2,
'd#': 3,
'eb': 3,
'e': 4,
'f': 5,
'f#': 6,
'gb': 6,
'g': 7,
'g#': 8,
'ab': 8,
'a': 9,
'a#': 10,
'bb': 10,
'b': 11,
}
return m[note] + (octave+1) * 12
def main():
parser = argparse.ArgumentParser(
prog="FF AD5M Audio 'player'",
description='This program can either play a midi file, single track, single note, single chanel or a frequency for a specific duration',
epilog="See https://github.com/xblax/flashforge_adm5_klipper_mod and https://github.com/consp/flashforge_adm5_audio")
parser.add_argument('mode', type=str, help="Either midi or freq", choices=['midi', 'freq', 'disable'])
parser.add_argument('-f', '--frequency', type=int, help="Frequency", default=440)
parser.add_argument('-d', '--duration', type=float, help="Duration of frequency", default=1.0)
parser.add_argument('-c', '--channel', type=str, help="Channel of track to play", default="0")
parser.add_argument('-m', '--midifile', type=str, help="Midi filename")
parser.add_argument('-p', '--pwm', type=int, help="pwm device to use", default=6)
parser.add_argument('-v', '--verbose', action="store_true", default=False, help="Be verbose (might slow down playback in case of heavy pitch changes)")
parser.add_argument('-s', '--skip', action='store_true', default=False, help="Skip start rest")
parser.add_argument('--nopwm', action='store_true', default=False, help="Disable PWM driver, used for testing midi file reading")
args = parser.parse_args()
global DEBUG
global NOPWM
DEBUG = args.verbose
NOPWM = args.nopwm
if not NOPWM:
if DEBUG:
print("Opening pwm ", args.pwm)
pwm = PWMAudio(0, args.pwm)
else:
if DEBUG:
print("PWM driver disabled")
pwm = None
def signal_handler(sig, frame):
pwm.disable()
sys.exit(0)
for sig in ('TERM', 'HUP', 'INT'):
signal.signal(getattr(signal, 'SIG'+sig), signal_handler)
if args.mode == "disable" and not NOPWM:
pwm.disable()
elif args.mode == "freq" and not NOPWM:
pwm.set(args.frequency)
pwm.enable()
sleep(args.duration)
pwm.disable()
elif args.mode == 'midi':
# lazy load
def play(filename, channel, pwm=None, skip_start=False):
import mido
import threading
def work(filename, channel, pwm, skip_start):
midi = mido.MidiFile(filename)
ticks_per_beat = midi.ticks_per_beat
tracks = midi.tracks
tracks_to_play = []
tempo = 500000
if DEBUG:
print("Looking for track %s" % (str(channel)))
if "," in channel:
channels = [int(x) for x in channel.split(",")]
else:
channels = [int(channel)]
for track in tracks:
for msg in track:
if msg.type in ['control_change', 'note_on', 'note_off'] and msg.channel in channels:
tracks_to_play.append(track)
break
elif hasattr(msg, 'channel') and msg.channel not in channels:
break
if len(tracks_to_play) == 0:
print("Channel %s not found" % channel)
return
started = False
# play index track and track to play
pitch = 0
note = 0
started = False
for event in mido.merge_tracks(tracks_to_play):
# if hasattr(event, 'channel') and event.channel not in channels:
# continue
interval = mido.tick2second(event.time, ticks_per_beat, tempo)
if DEBUG and interval > 0:
print("Rest: ", interval)
if not skip_start or (skip_start and started):
sleep(interval)
if event.type == 'copyright':
print("Copyright ", event.text)
elif event.type == "track_name":
print("Track: ", event.name)
elif event.type == 'set_tempo':
tempo = event.tempo
if DEBUG:
print("Tempo change: %d %d %d" % (ticks_per_beat, tempo, interval))
elif event.type == 'note_on':
started = True
note = event.note
if DEBUG:
print("%d Note ON: %d" % (event.channel, event.note))
if pwm:
pwm.set(midinumber_to_frequency(note, pitch=pitch))
pwm.enable()
elif event.type == 'note_off':
if DEBUG:
print("%d Note OFF: %d" % (event.channel, event.note))
if pwm:
pwm.disable()
elif event.type == "pitchwheel":
pitch = event.pitch
if DEBUG:
print("%d Pitch change by %d" % (event.channel, event.pitch))
if pwm:
pwm.set(midinumber_to_frequency(note, pitch=pitch))
else:
if DEBUG:
print(event)
# silence
pwm.disable()
t = threading.Thread(target=work, args=(filename, channel, pwm, skip_start))
t.start()
if args.midifile is None:
print("--midifile/-m needs to be set")
exit(1)
print("Loading %s ..." % args.midifile)
try:
play(args.midifile, args.channel, pwm, skip_start=args.skip)
except Exception as e:
if pwm:
pwm.disable()
raise
if __name__ == '__main__':
main()