|
1 | 1 | #!/usr/bin/python3 |
2 | 2 |
|
3 | | -### Parameters |
| 3 | +import os |
| 4 | +import subprocess |
| 5 | +import signal |
| 6 | +import numpy as np |
| 7 | +from pyaudio import PyAudio, paFloat32, paContinue |
| 8 | + |
4 | 9 | # Sound output parameters |
5 | 10 | volume = 1.0 |
6 | | -sample_buf_size = 44 |
7 | | -sampling_freq = 44100 #Hz |
| 11 | +sampling_freq = 44100 # Hz |
8 | 12 |
|
9 | 13 | # Frequency generator parameters |
10 | | -min_freq = 200 #Hz |
11 | | -max_freq = 2000 #Hz |
| 14 | +min_freq = 100 # Hz |
| 15 | +max_freq = 6000 # Hz |
12 | 16 |
|
13 | 17 | # 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) |
68 | 162 |
|
69 | 163 | # 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