Skip to content

Commit ca15bbd

Browse files
committed
Rework theremin script
1 parent 23e6aa4 commit ca15bbd

File tree

1 file changed

+167
-71
lines changed

1 file changed

+167
-71
lines changed

client/pyscripts/theremin.py

Lines changed: 167 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,177 @@
11
#!/usr/bin/python3
22

3-
### Parameters
3+
import os
4+
import subprocess
5+
import signal
6+
import numpy as np
7+
from pyaudio import PyAudio, paFloat32, paContinue
8+
49
# Sound output parameters
510
volume = 1.0
6-
sample_buf_size = 44
7-
sampling_freq = 44100 #Hz
11+
sampling_freq = 44100 # Hz
812

913
# Frequency generator parameters
10-
min_freq = 200 #Hz
11-
max_freq = 2000 #Hz
14+
min_freq = 100 # Hz
15+
max_freq = 6000 # Hz
1216

1317
# Proxmark3 parameters
14-
pm3_client="/usr/local/bin/proxmark3"
15-
pm3_reader_dev_file="/dev/ttyACM0"
16-
pm3_tune_cmd="hf tune"
17-
18-
19-
### Modules
20-
import numpy
21-
import pyaudio
22-
from select import select
23-
from subprocess import Popen, DEVNULL, PIPE
24-
25-
26-
### Main program
27-
p = pyaudio.PyAudio()
28-
29-
# For paFloat32 sample values must be in range [-1.0, 1.0]
30-
stream = p.open(format=pyaudio.paFloat32,
31-
channels=1,
32-
rate=sampling_freq,
33-
output=True)
34-
35-
# Initial voltage to frequency values
36-
min_v = 100.0
37-
max_v = 0.0
38-
v = 0
39-
out_freq = min_freq
40-
41-
# Spawn the Proxmark3 client
42-
pm3_proc = Popen([pm3_client, pm3_reader_dev_file, "-c", pm3_tune_cmd], bufsize=0, env={}, stdin=DEVNULL, stdout=PIPE, stderr=DEVNULL)
43-
mv_recbuf = ""
44-
45-
# Read voltages from the Proxmark3, generate the sine wave, output to soundcard
46-
sample_buf = [0.0 for x in range(0, sample_buf_size)]
47-
i = 0
48-
sinev = 0
49-
while True:
50-
51-
# Read Proxmark3 client's stdout and extract voltage values
52-
if(select([pm3_proc.stdout], [], [], 0)[0]):
53-
54-
b = pm3_proc.stdout.read(256).decode("ascii")
55-
if "Done" in b:
56-
break;
57-
for c in b:
58-
if c in "0123456789 mV":
59-
mv_recbuf += c
60-
else:
61-
mv_recbuf = ""
62-
if mv_recbuf[-3:] == " mV":
63-
v = int(mv_recbuf[:-3]) / 1000
64-
if v < min_v:
65-
min_v = v - 0.001
66-
if v > max_v:
67-
max_v = v
18+
pm3_client = "pm3"
19+
pm3_tune_cmd = "hf tune --value"
20+
21+
frequency = 440
22+
buffer = []
23+
24+
25+
def find_zero_crossing_index(array):
26+
for i in range(1, len(array)):
27+
if array[i-1] < 0 and array[i] >= 0:
28+
return i
29+
return None # Return None if no zero-crossing is found
30+
31+
32+
def generate_sine_wave(frequency, sample_rate, duration, frame_count):
33+
"""Generate a sine wave at a given frequency."""
34+
t = np.linspace(0, duration, int(sample_rate * duration), endpoint=False)
35+
wave = np.sin(2 * np.pi * frequency * t)
36+
return wave[:frame_count]
37+
38+
39+
# PyAudio Callback function
40+
def pyaudio_callback(in_data, frame_count, time_info, status):
41+
# if in_data is None:
42+
# return (in_data, pyaudio.paContinue)
43+
global frequency
44+
global buffer
45+
wave = generate_sine_wave(frequency, sampling_freq, 0.01, frame_count*2)
46+
i = find_zero_crossing_index(buffer)
47+
if i is None:
48+
buffer = wave
49+
else:
50+
buffer = np.concatenate((buffer[:i], wave))
51+
data = (buffer[:frame_count] * volume).astype(np.float32).tobytes()
52+
buffer = buffer[frame_count:]
53+
return (data, paContinue)
54+
# pyaudio.paComplete
55+
56+
57+
def silent_pyaudio():
58+
"""
59+
Lifted and adapted from https://stackoverflow.com/questions/67765911/
60+
PyAudio is noisy af every time you initialise it, which makes reading the
61+
log output rather difficult. The output appears to be being made by the
62+
C internals, so we can't even redirect the logs with Python's logging
63+
facility. Therefore the nuclear option was selected: swallow all stderr
64+
and stdout for the duration of PyAudio's use.
65+
"""
66+
67+
# Open a pair of null files
68+
null_fds = [os.open(os.devnull, os.O_RDWR) for x in range(2)]
69+
# Save the actual stdout (1) and stderr (2) file descriptors.
70+
save_fds = [os.dup(1), os.dup(2)]
71+
# Assign the null pointers to stdout and stderr.
72+
os.dup2(null_fds[0], 1)
73+
os.dup2(null_fds[1], 2)
74+
pyaudio = PyAudio()
75+
os.dup2(save_fds[0], 1)
76+
os.dup2(save_fds[1], 2)
77+
# Close all file descriptors
78+
for fd in null_fds + save_fds:
79+
os.close(fd)
80+
return pyaudio
81+
82+
83+
def run_pm3_cmd(callback):
84+
# Start the process
85+
process = subprocess.Popen(
86+
[pm3_client, '-c', pm3_tune_cmd],
87+
stdout=subprocess.PIPE,
88+
stderr=subprocess.PIPE,
89+
text=True,
90+
bufsize=1, # Line buffered
91+
shell=False
92+
)
93+
94+
# Read the output line by line as it comes
95+
try:
96+
with process.stdout as pipe:
97+
for line in pipe:
98+
# Process each line
99+
l = line.strip() # Strip to remove any extraneous newline characters
100+
callback(l)
101+
except Exception as e:
102+
print(f"An error occurred: {e}")
103+
finally:
104+
# Ensure the subprocess is properly terminated
105+
process.terminate()
106+
process.wait()
107+
108+
109+
def linear_to_exponential_freq(v, min_v, max_v, min_freq, max_freq):
110+
# First, map v to a range between 0 and 1
111+
if max_v != min_v:
112+
normalized_v = (v - min_v) / (max_v - min_v)
113+
else:
114+
normalized_v = 0.5
115+
normalized_v = 1 - normalized_v
116+
117+
# Calculate the ratio of the max frequency to the min frequency
118+
freq_ratio = max_freq / min_freq
119+
120+
# Calculate the exponential frequency using the mapped v
121+
freq = min_freq * (freq_ratio ** normalized_v)
122+
return freq
123+
124+
125+
class foo():
126+
def __init__(self):
127+
self.p = silent_pyaudio()
128+
# For paFloat32 sample values must be in range [-1.0, 1.0]
129+
self.stream = self.p.open(format=paFloat32,
130+
channels=1,
131+
rate=sampling_freq,
132+
output=True,
133+
stream_callback=pyaudio_callback)
134+
135+
# Initial voltage to frequency values
136+
self.min_v = 50000.0
137+
self.max_v = 0.0
138+
139+
# Setting the signal handler for SIGINT (Ctrl+C)
140+
signal.signal(signal.SIGINT, self.signal_handler)
141+
142+
# Start the stream
143+
self.stream.start_stream()
144+
145+
def __exit__(self):
146+
self.stream.stop_stream()
147+
self.stream.close()
148+
self.p.terminate()
149+
150+
def signal_handler(self, sig, frame):
151+
print("\nYou pressed Ctrl+C! Press Enter")
152+
self.__exit__()
153+
154+
def callback(self, line):
155+
if 'mV' not in line:
156+
return
157+
v = int(line.split(' ')[1])
158+
if v == 0:
159+
return
160+
self.min_v = min(self.min_v, v)
161+
self.max_v = max(self.max_v, v)
68162

69163
# Recalculate the audio frequency to generate
70-
out_freq = (max_freq - min_freq) * (max_v - v) / (max_v - min_v) \
71-
+ min_freq
72-
73-
# Generate the samples and write them to the soundcard
74-
sinevs = out_freq / sampling_freq * numpy.pi * 2
75-
sample_buf[i] = sinev
76-
sinev += sinevs
77-
sinev = sinev if sinev < numpy.pi * 2 else sinev - numpy.pi * 2
78-
i = (i + 1) % sample_buf_size
79-
if not i:
80-
stream.write((numpy.sin(sample_buf) * volume).
81-
astype(numpy.float32).tobytes())
164+
global frequency
165+
frequency = linear_to_exponential_freq(v, self.min_v, self.max_v, min_freq, max_freq)
166+
167+
# frequency = max_freq - ((max_freq - min_freq) * (v - self.min_v) / (self.max_v - self.min_v) + min_freq)
168+
#frequency = (frequency + new_frequency)/2
169+
170+
171+
def main():
172+
f = foo()
173+
run_pm3_cmd(f.callback)
174+
175+
176+
if __name__ == "__main__":
177+
main()

0 commit comments

Comments
 (0)