-
Notifications
You must be signed in to change notification settings - Fork 1
/
sofkasim.py
executable file
·158 lines (129 loc) · 4.88 KB
/
sofkasim.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
#!/usr/bin/env python3
import argparse
from devices import get_device
"""
Sofka simulator for MUSYS.
Takes the output lists generated by the simulated
MUSYS compiler, musysim, and simulates the
effects of sending this data to various
hardware audio devices by generating
Nyquist code.
"""
DEBUG = False
MAX_INTERRUPT_FREQ = 16000
PRELUDE = f"(set-control-srate {MAX_INTERRUPT_FREQ})"
def dprint(*s):
if DEBUG:
print(*s)
def freq(pitch):
"""Converts a Nyquist pitch number to Hz."""
return 440 * 2 ** ((pitch - 69) / 12)
class Sofka:
def __init__(self, lists):
self.lists = [b.split(' ') for b in lists.split('\n')]
self.clock = 100 # Interrupts per second
# default implied by example at http://users.encs.concordia.ca/~grogono/Bio/ems.html
self.oscillators = [None] * 3
self.envelopes = [None] * 3
self.current_time = 0
def perform(self):
# TODO this needs to be refactored once a sensible system
# for combining all the audio sources and effects for
# all six lists is settled on.
for b in self.lists:
if not b[0]:
continue
for c in b:
n = int(c[:2], 8)
v = int(c[2:], 8)
device = get_device(int(c[:2], 8))
dprint(device, c)
if n == 62: # Interrupt timer
self.clock = v
if 0 < n < 4: # Osc
dprint('OSC', n)
if self.oscillators[n - 1]:
self.oscillators[n - 1].change(v)
else:
self.oscillators[n - 1] = Oscillator(v)
self.active = n - 1
if 23 < n < 27: # Envelopes
n = n - 24
dprint('Envelope', n + 1)
t = self.current_time
d = self.secs(v)
if self.envelopes[n]:
self.envelopes[n].addstage(t, d)
else:
self.envelopes[n] = Envelope(t, d)
self.current_time += d
self.oscillators[self.active].addtime(d)
if n == 60: # Wait timer
d = self.secs(v)
dprint('WAIT:', d)
self.current_time += d
self.oscillators[self.active].addtime(d)
# Write the generated audio
sources = [o for o in self.oscillators if o]
sources += [e for e in self.envelopes if e]
dprint('SOURCES', sources)
output = ' '.join(['(seq %s)' % ' '.join(s.out()) for s in sources])
return f'(mult {output})'
def secs(self, n):
""" Number of seconds of time with current clock."""
return n * 1 / self.clock
class Envelope:
def __init__(self, current_time, duration):
self.stages = [] # list of (time, duration) for alternating attack / decay
self.addstage(current_time, duration)
self.history = []
def addstage(self, t, d):
"""
t float: current time (seconds)
d float: duration (seconds)
"""
self.stages.append((t, d))
def out(self):
level = 0 # 0: attack, 1: decay
breakpoints = []
for stage in self.stages:
t_prev = breakpoints[-2] if breakpoints else 0
if stage[0] < t_prev:
breakpoints[-2] = stage[0]
breakpoints[-1] = min(level, 0.5)
else:
breakpoints += [stage[0], level]
level = 1 - level
breakpoints += [sum(stage), level]
breakpoints = ' '.join([str(round(v, 3)) for v in breakpoints])
return [f"(pwl-list '({breakpoints}))"]
class Oscillator:
def __init__(self, pitch=32):
# Hypothesised: {Nyquist (MIDI) tone} = {MUSYS tone} + 28
# Middle C = Nyquist 60, MUSYS 32
self.pitch = pitch + 28
self.duration = 0
self.phase = 0
self.history = []
def addtime(self, d):
self.duration += d
def change(self, pitch):
self.history.append(
f"(osc {self.pitch} {round(self.duration, 3)} *table* {round(self.phase, 3)})"
)
self.phase = (self.phase + self.duration * freq(self.pitch)) % 360
self.pitch = pitch + 28
self.duration = 0
def out(self):
self.change(0)
return self.history
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="MUSYS (1973) Sofka simulator.")
#parser.add_argument('file', help='compiled MUSYS data lists to perform')
parser.add_argument('-d', '--debug', help='turn on debug output', action='store_true')
args = parser.parse_args()
DEBUG = args.debug
listfile = 'musys.out'
with open(listfile, 'r') as f:
s = Sofka(f.read())
print(f'{PRELUDE}(play {s.perform()})')