-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathpysynth_beeper.py
executable file
·113 lines (89 loc) · 3.7 KB
/
pysynth_beeper.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
import logging
import math
import struct
import wave
LOG = logging.getLogger("pysynth_beeper")
SAMPLING_RATE = 44100
PITCHHZ = {}
keys_s = ('a', 'a#', 'b', 'c', 'c#', 'd', 'd#', 'e', 'f', 'f#', 'g', 'g#')
for k in range(88):
freq = 27.5 * 2. ** (k / 12.)
oct = (k + 9) // 12
note = '%s%u' % (keys_s[k % 12], oct)
PITCHHZ[note] = freq
def make_wav(song, tempo=120, transpose=0, fn="out.wav"):
f = wave.open(fn, 'w')
f.setnchannels(1)
f.setsampwidth(2)
f.setframerate(SAMPLING_RATE)
f.setcomptype('NONE', 'Not Compressed')
# Define a waveform that looks something like this
# \ /
#__\_____ /__
# \ /\/
# \/
# Format: [(start, end, start_level, end_level), ...]
waveform = [(0.0, 0.3, 1.0, -1.0),
(0.3, 0.5, -1.0, 0.0),
(0.5, 0.6, 0.0, -0.5),
(0.6, 1.0, -0.5, 1.0)]
# BPM is "quarter notes per minute"
full_notes_per_second = float(tempo) / 60 / 4
full_note_in_samples = SAMPLING_RATE / full_notes_per_second
def sixteenbit(sample):
return struct.pack('h', round(32000 * sample))
def beep_single_period(period):
asin = lambda x: math.sin(2. * math.pi * x)
period_waveform = []
for x in range(period):
# Position inside current period, 0..1
pos = float(x) / period
# Synth 1, using sine waves
level1 = (asin(pos) + asin(pos * 2)) / 2
# Synth 2, discrete, using waveform definition
for start, finish, start_level, finish_level in waveform:
if pos >= start and pos <= finish:
localpos = (pos - start) / (finish - start)
level2 = (finish_level - start_level) * localpos + start_level
break
# Put both samples together, apply fadein/fadeout
level = (level1 + level2) / 2
period_waveform.append(level)
#period_waveform_packed.append(sixteenbit(level))
return period_waveform, b"".join(sixteenbit(l) for l in period_waveform)
def beep(freq, duration, sink):
ow = b""
period = int(SAMPLING_RATE / 4 / freq)
period_waveform, period_waveform_packed = beep_single_period(period)
x = 0
while x < duration:
if x < 100 or duration - x < 100:
# At borders we do fade in and fade out
fade_multiplier = min(x, duration - x) / 100.0
ow += sixteenbit(period_waveform[x % period] * fade_multiplier)
x += 1
else:
if x % period == 0:
# Optimization:
# We're aligned with waveform, can fill ow in batches!
while x + period + 100 < duration:
ow += period_waveform_packed
x += period
# Go sample-by-sample
ow += sixteenbit(period_waveform[x % period])
x += 1
sink.writeframesraw(ow)
def silence(duration, sink):
sink.writeframesraw(sixteenbit(0) * int(duration))
for note_pitch, note_duration in song:
# note_duration is 1, 2, 4, 8, ... and actually means 1, 1/2, 1/4, ...
duration = int(full_note_in_samples / note_duration)
if note_pitch == "r":
LOG.debug("Silence for %d samples" % duration)
silence(duration, f)
else:
freq = PITCHHZ[note_pitch]
freq *= 2 ** transpose
LOG.debug("%d Hz for %d samples" % (freq, duration))
beep(freq, duration, f)
f.close()