/
Main.py
240 lines (191 loc) · 8.03 KB
/
Main.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
import sys
import my_wave as wave
import math
import struct
import random
import argparse
import pyaudio
import PySimpleGUI as sg
import multiprocess
import queue as Queue
import logging
from threading import Thread
from itertools import count, cycle, islice, zip_longest
mpl = multiprocess.log_to_stderr()
mpl.setLevel(logging.DEBUG)
#multiprocess.set_start_method("spawn")
# from here https://zach.se/generate-audio-with-python/
# python -u "/Users/mwholey/Documents/git_repos/chord-tuner/Main.py"
def clip_amplitude(amplitude):
return float(1.0 if amplitude > 1.0 else 0.0 if amplitude < 0.0 else amplitude)
def sin_wave_old(frequency=440.0, samplerate=44100, amplitude=0.5):
amplitude = clip_amplitude(amplitude)
angular_frequency = 2.0 * math.pi * float(frequency)
sin_generator = (float(amplitude) * math.sin(angular_frequency * (float(i) / float(samplerate))) for i in count(0))
return sin_generator
# computes a sin wave for the period of the wave, then returns the infinite generator of that period looped
# can think of this as computing the y value for amplitude given x
# the x is the generator of the i/samplerate for i in period
def sin_wave(frequency=440.0, samplerate=44100, amplitude=0.5):
period = int(samplerate / frequency)
amplitude = clip_amplitude(amplitude)
lookup = [float(amplitude) * math.sin(2.0*math.pi*float(frequency)*(float(i%period)/float(samplerate))) for i in range(period)]
return (lookup[i%period] for i in count(0))
# returns random value generator
def white_noise_generator(amplitude=0.5):
return (clip_amplitude(amplitude) * random.uniform(-1, 1) for _ in count(0))
def white_noise(amplitude=0.5):
noise = cycle(islice(white_noise_generator(amplitude), 44100))
return noise
def compute_samples(channels, nsamples=None):
# zip(*channel) this combines all the channel components into a single channel list with tuples for each timeslice containing each component
# map(sum, zip(*channel)) for channel in channels - this sums all the channel components into a single list for each channel
# zip(*(map...)) takes the channel audio streams, and combines them into a single stream of tuples one element for each original channel
# islice cuts this infinite stream at the sample size desired
return islice(zip(*(map(sum, zip(*channel)) for channel in channels)), nsamples)
### sum function could be changed for interesting stuff
### statistics normalization powers
def grouper(n, iterable, fillvalue=None):
# "grouper(3, 'ABCDEFG', 'x') --> zip_longest((A,B,C), (D,E,F), (G,x,x))"
# creates an array of n identical iterables, when one iterable is used, that action is
# replicated across all other iterables in the array as they are all actually the same memory address
args = [iter(iterable)] * n
# grabs first element from each iterator and places into a tuple, then second, and so on
# however since they're all the same iterator it actually grabs first from first, second from second, and so on
# iterating through each iterator like we were just stepping through a single array because in essence that is what we're doing
# since zip creates a list of n-tuples, this effectively just groups the infinite generator into an infinite list of n-tuples
return zip_longest(fillvalue=fillvalue, *args)
def write_wavefile(filename, samples, nframes=44100, nchannels=2, sampwidth=2, framerate=44100, bufsize=2048):
w = wave.open(filename, 'w')
w.setparams((nchannels, sampwidth, framerate, nframes, 'NONE', 'not compressed'))
max_amplitude = float(int((2 ** (sampwidth * 8)) / 2) - 1)
totalbuf = 0
for chunk in grouper(bufsize, samples):
totalbuf += bufsize
if totalbuf > 200000:
break
frames = b''.join(b''.join(struct.pack('h', int(max_amplitude * sample)) for sample in channels) for channels in chunk if channels is not None)
w.writeframesraw(frames)
w.close()
return filename
def write_audiostream(queue, nchannels=2, sampwidth=2, framerate=44100, bufsize=2048):
message = queue.get(block=False)
samples_stream = message
max_amplitude = float(int((2 ** (sampwidth * 8)) / 2) - 1) / 100
pya = pyaudio.PyAudio()
stream = pya.open(format = pya.get_format_from_width(width=sampwidth), channels=nchannels, rate=framerate, output=True)
STOP = False
# if compute samples has no sample length, there'll be an infinite number of chunks
# which are bufsize length n-tuples of samples generator
# since it's an infinite generator this means samples are continuous with no need to reset or break, just keeps chugging along infinitely
while not STOP:
for chunk in samples_stream:
frames = b''.join(b''.join(struct.pack('h', int(max_amplitude * sample)) for sample in channels) for channels in chunk if channels is not None)
try:
mpl.info(stream.get_write_available())
stream.write(frames)
except Exception as e:
mpl.error(e)
try:
message = queue.get(block=False)
if message is not None:
samples = message
break
except Queue.Empty:
pass
def update_samples(values):
channel = ()
for i in range(50):
if f'Osc{i}Freq' in values and f'Osc{i}Vol' in values:
# construct sound, then convert tuple to list, append our sound, back to tuple
sound = sin_wave(frequency=values[f'Osc{i}Freq'], amplitude=values[f'Osc{i}Vol'])
# operations cannot be combined or NoneType error
channel = list(channel)
channel.append(sound)
channel = tuple(channel)
# mono for now
channels = (
channel,
channel
)
return compute_samples(channels)
def main():
num_channels = 2
sampwidth = 2
framerate = 44100
pya = pyaudio.PyAudio()
stream = pya.open(format = pya.get_format_from_width(width=sampwidth), channels=num_channels, rate=framerate, output=True)
### GUI
layout = [
[
sg.Text('Oscilator 1'),
sg.Slider(
key="Osc1Freq",
range=(20.0,500.0),
default_value=440.0,
orientation='h'),
sg.Slider(
key="Osc1Vol",
range=(0.0,1.0),
default_value=0.5,
resolution=0.01,
orientation='h')
],
[
sg.Text('Oscilator 2'),
sg.Slider(
key="Osc2Freq",
range=(20.0,500.0),
default_value=440.0,
orientation='h'),
sg.Slider(
key="Osc2Vol",
range=(0.0,1.0),
default_value=0.5,
resolution=0.01,
orientation='h')
],
[sg.Output(size=(250,50))],
[sg.Submit(), sg.Cancel()]
]
window = sg.Window('Shit', layout)
event_old, values_old = None, None
stream_queue = multiprocess.Queue()
while True:
event, values = window.read(timeout = 50)
if event in (None, 'Exit', 'Cancel'):
exit()
if values_old != values:
mpl.info(event, values)
event_old, values_old = event, values
samples = update_samples(values)
# sound functions here, it just takes control away while in infinite loop
# write_audiostream(stream, samples)
zip_gen = grouper(2048, samples)
big_stream = []
count = 0
for i in zip_gen:
big_stream.append(list(i))
count += 1
if count >= 50:
break
stream_queue.put(big_stream)
# if not 'audio_proc' in locals():
# audio_proc = multiprocess.Process(target=write_audiostream, args=(stream_queue,), daemon=True)
# audio_proc.start()
# else:
# stream_queue.put(big_stream)
# audio_proc.terminate()
# audio_proc = multiprocess.Process(target=write_audiostream, args=(stream, samples), daemon=True)
# audio_proc.start()
## THREAD think i'm getting GIL issues, getting stuck and unable to interact with the ui as the stream loop is holding on too tight
if not 'audio_thread' in locals():
audio_thread = Thread(target=write_audiostream, args=(stream, samples))
audio_thread.daemon = True
audio_thread.run()
else:
audio_thread._stop()
audio_thread = Thread(target=write_audiostream, args=(stream, samples))
audio_thread.daemon = True
audio_thread.run()
main()