commit 7ee950b2c5842be131ea018a9b5f68ab8db20d29 Author: Brandon4466 Date: Sun Jan 14 23:41:53 2024 -0800 main application with tuning, playing, and GUI diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..90ff14d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "python.languageServer": "Pylance", + "python.linting.pylintEnabled": false, + "python.analysis.diagnosticSeverityOverrides": { + "reportMissingModuleSource": "none" + }, + "python.analysis.extraPaths": [ + "", + "c:\\Users\\spong\\.vscode\\extensions\\joedevivo.vscode-circuitpython-0.1.20-win32-x64\\stubs", + "c:\\Users\\spong\\AppData\\Roaming\\Code\\User\\globalStorage\\joedevivo.vscode-circuitpython\\bundle\\20240113\\adafruit-circuitpython-bundle-py-20240113\\lib" + ], + "circuitpython.board.version": null +} \ No newline at end of file diff --git a/_detectsong.py b/_detectsong.py new file mode 100644 index 0000000..dbc35bc --- /dev/null +++ b/_detectsong.py @@ -0,0 +1,19 @@ +import sounddevice as sd +import numpy as np +import time +import asyncio +from shazamio import Shazam + +def rec(): + recording = sd.rec((44100 * 5), samplerate=44100, channels=1, detype=np.int16) + sd.wait() + return recording + +async def detect(): + recording = rec() + shazam = Shazam() + out = await shazam.recognize_song(recording) + print(out) + +loop = asyncio.get_event_loop() +loop.run_until_complete(detect()) \ No newline at end of file diff --git a/filtered_audio.wav b/filtered_audio.wav new file mode 100644 index 0000000..67f6c55 Binary files /dev/null and b/filtered_audio.wav differ diff --git a/fm_capture.bin b/fm_capture.bin new file mode 100644 index 0000000..af3480d Binary files /dev/null and b/fm_capture.bin differ diff --git a/fm_capture5.bin b/fm_capture5.bin new file mode 100644 index 0000000..3120edb Binary files /dev/null and b/fm_capture5.bin differ diff --git a/fm_capture6.bin b/fm_capture6.bin new file mode 100644 index 0000000..4f2f172 Binary files /dev/null and b/fm_capture6.bin differ diff --git a/fm_data.raw b/fm_data.raw new file mode 100644 index 0000000..37ef30f Binary files /dev/null and b/fm_data.raw differ diff --git a/fm_data_test.wav b/fm_data_test.wav new file mode 100644 index 0000000..0db78a1 Binary files /dev/null and b/fm_data_test.wav differ diff --git a/fm_data_test1.raw b/fm_data_test1.raw new file mode 100644 index 0000000..97caea2 Binary files /dev/null and b/fm_data_test1.raw differ diff --git a/fm_rewind.wav b/fm_rewind.wav new file mode 100644 index 0000000..15333fb Binary files /dev/null and b/fm_rewind.wav differ diff --git a/fmdemod.py b/fmdemod.py new file mode 100644 index 0000000..18eafd2 --- /dev/null +++ b/fmdemod.py @@ -0,0 +1,27 @@ +from pydub import AudioSegment +from pydub.effects import low_pass_filter +from pydub.playback import play +import sounddevice as sd +import numpy as np + +# Input file path +input_file = "fm_data.raw" + +# Load the audio file +audio = AudioSegment.from_file(input_file, format="raw", sample_width=2, channels=1, frame_rate=170000) + +# Apply low pass filter (if needed) +audio = low_pass_filter(audio, 75000) + +play(audio) + +# Convert to numpy array +# samples = np.array(audio.get_array_of_samples()) + +# # Play the audio using sounddevice +# sd.play(samples, samplerate=audio.frame_rate) +# sd.wait() + +# audio.export("fm_data.wav", format="wav") + +# rx_fm -f 94.7M -M fm -s 170k -A fast -l 0 -E deemp -d driver=hackrf fm_data.raw \ No newline at end of file diff --git a/fmdemod2.py b/fmdemod2.py new file mode 100644 index 0000000..55bdea3 --- /dev/null +++ b/fmdemod2.py @@ -0,0 +1,103 @@ +import numpy as np +import matplotlib.pylab as plt +import time + +# inspired by https://github.com/osmocom/rtl-sdr/blob/master/src/rtl_fm.c + + +# loading in the .wav +signal = np.memmap("HackRF_20240111_025820Z_94700kHz_IQ.wav", offset=44) + +result = np.zeros(len(signal)//2, dtype=np.cfloat) + +downsample = 128 + + +def low_pass(signal): + # simple square window FIR + + lowpassed = signal[:] + + # needs to be go outside this function + now_r = 0 + now_j = 0 + + i=0 + i2=0 + + prev_index = 0 + + while (i < len(lowpassed)): + now_r += lowpassed[i] + now_j += lowpassed[i + 1] + i += 2 + + prev_index += 2 + + if (prev_index < downsample): + continue + + lowpassed[i2] = now_r + lowpassed[i2 + 1] = now_j + prev_index = 0 + now_r = 0 + now_j = 0 + i2 += 2 + + lp_len = i2 + + return lowpassed + +def multiply(ar, aj, br, bj): + cr = ar * br - aj * bj + cj = aj * br + ar * bj + return cr, cj + +def polar_discriminant(ar, aj, br, bj): + cr , cj = multiply(ar, aj, br, -bj) + #print(cr, cj) + angle = np.arctan2(cj, cr) + #print("angle", angle) + # return (angle / np.pi * (1<<14)) + return (angle * 180.0 / np.pi) + +def fm_demod(signal, result): + pre_r = 0 + pre_j = 0 + + i = 0 + pcm = 0 + + lp_len = len(signal) + + # low-passing + lp = low_pass(signal) + #print(lp[0], lp[1], pre_r, pre_j) + + pcm = polar_discriminant(lp[0], lp[1], pre_r, pre_j) + result[0] = pcm + + for i in range(2, lp_len-1, 2): + # being lazy, only case 0 for now... + + # case 0 + pcm = polar_discriminant(lp[i], lp[i + 1], lp[i - 2], lp[i - 1]) + + result[i // 2] = pcm + + pre_r = lp[lp_len - 2] + pre_j = lp[lp_len - 1] + result_len = lp_len // 2 + + #return(result) + + +time1 = time.time() +signal = -127.5 + signal + +fm_demod(signal, result) + +print(time.time() - time1) + +plt.plot(result[::10]) +plt.show() \ No newline at end of file diff --git a/fmdemod3.py b/fmdemod3.py new file mode 100644 index 0000000..afb9d4a --- /dev/null +++ b/fmdemod3.py @@ -0,0 +1,36 @@ +import numpy as np +import sounddevice as sd +from scipy.io import wavfile +from scipy.signal import decimate, butter, filtfilt + +# Load the raw IQ samples from the file +filename = "fm_capture6.bin" +iq_samples = np.fromfile(filename, dtype=np.int8) + +# Define the FM demodulation parameters +sample_rate = int(2e6) # Sample rate of the IQ samples +audio_sample_rate = int(48e3) # Desired audio sample rate +fm_deviation = int(75e3) # FM deviation of the radio signal +channel_width = int(200e3) # Channel width of the low-pass filter + +# Design the low-pass filter +nyquist_freq = sample_rate / 2 +cutoff_freq = channel_width / nyquist_freq +b, a = butter(10, int(75e3), btype='low', fs=int(2e6)) + +# Apply the low-pass filter +filtered_audio = filtfilt(b, a, iq_samples) + +# Demodulate the FM signal +demodulated_audio = np.diff(filtered_audio.astype(np.float32)) * np.conj(filtered_audio[:-1]).astype(np.float32) +demodulated_audio = decimate(demodulated_audio, int(100)) + +# Normalize the audio to the range [-1, 1] +normalized_audio = demodulated_audio / np.max(np.abs(demodulated_audio)) + +# Play the audio +sd.play(normalized_audio, audio_sample_rate) +sd.wait() + +# output_filename = "demodulated_audio.wav" +# wavfile.write(output_filename, audio_sample_rate, normalized_audio) \ No newline at end of file diff --git a/fmdemod_old.py b/fmdemod_old.py new file mode 100644 index 0000000..6128484 --- /dev/null +++ b/fmdemod_old.py @@ -0,0 +1,50 @@ +import numpy as np +import scipy.signal +import wave + +def read_raw_iq_samples(file_path): + with open(file_path, 'rb') as file: + raw_data = file.read() + iq_samples = np.frombuffer(raw_data, dtype=np.int8) + return iq_samples + +def fm_demodulation(iq_samples, sample_rate, fm_deviation=75e3): + # Hilbert transform for FM demodulation + analytic_signal = scipy.signal.hilbert(iq_samples) + instantaneous_phase = np.unwrap(np.angle(analytic_signal)) + demodulated_signal = np.unwrap(np.angle(iq_samples[1:] * np.conj(iq_samples[:-1]))) / (2.0 * np.pi * 1.0/sample_rate * fm_deviation) + return demodulated_signal + +def decimate_audio(signal, decimation_factor): + return scipy.signal.resample(signal, len(signal)//decimation_factor) + +def low_pass_filter(signal, cutoff_frequency, sample_rate): + nyquist = 0.5 * sample_rate + normal_cutoff = cutoff_frequency / nyquist + b, a = scipy.signal.butter(6, normal_cutoff, btype='low', analog=False) + filtered_signal = scipy.signal.filtfilt(b, a, signal) + return filtered_signal + +def save_wav_file(signal, sample_rate, output_file): + scaled_signal = np.int16(signal / np.max(np.abs(signal)) * 32767) + with wave.open(output_file, 'wb') as wav_file: + wav_file.setnchannels(1) + wav_file.setsampwidth(2) + wav_file.setframerate(sample_rate) + wav_file.writeframes(scaled_signal.tobytes()) + +def main(input_file, output_file, sample_rate, channel_width, decimation_factor=10, cutoff_frequency=75e3): + iq_samples = read_raw_iq_samples(input_file) + demodulated_signal = fm_demodulation(iq_samples, sample_rate) + decimated_signal = decimate_audio(demodulated_signal, decimation_factor) + filtered_signal = low_pass_filter(decimated_signal, cutoff_frequency, sample_rate/decimation_factor) + new_sample_rate = sample_rate / decimation_factor + save_wav_file(filtered_signal, new_sample_rate, output_file) + +if __name__ == "__main__": + input_file_path = "fm_capture.bin" + output_wav_path = "output.wav" + sample_rate = 2e6 # Replace with the actual sample rate of your FM stream + channel_width = 200e3 # Replace with the channel width of your FM signal + + main(input_file_path, output_wav_path, sample_rate, channel_width) diff --git a/fmplay.py b/fmplay.py new file mode 100644 index 0000000..fb9b489 --- /dev/null +++ b/fmplay.py @@ -0,0 +1,77 @@ +import subprocess +from time import sleep +import tkinter as ttk +import _detectsong as ds + +root = ttk.Tk() +root.title("Radio Controller") +# root.geometry("1280x400") +root.geometry("800x400") +root.attributes("-topmost", True) +# sv_ttk.use_dark_theme() + +st1_button = ttk.Button(root, text="94.7M", borderwidth=0, command=lambda: tune(94.7)) +st2_button = ttk.Button(root, text="96.9M", borderwidth=0, command=lambda: tune(96.9)) +st3_button = ttk.Button(root, text="98.5M", borderwidth=0, command=lambda: tune(98.5)) +st4_button = ttk.Button(root, text="100.5M", borderwidth=0, command=lambda: tune(100.5)) +st5_button = ttk.Button(root, text="101.1M", borderwidth=0, command=lambda: tune(101.1)) +st6_button = ttk.Button(root, text="102.5M", borderwidth=0, command=lambda: tune(102.5)) +st7_button = ttk.Button(root, text="103.5M", borderwidth=0, command=lambda: tune(103.5)) +st8_button = ttk.Button(root, text="106.5M", borderwidth=0, command=lambda: tune(106.5)) +st9_button = ttk.Button(root, text="107.9M", borderwidth=0, command=lambda: tune(107.9)) + +exit_button = ttk.Button(root, text="Exit", command=lambda: on_close(True)) +volume_slider = ttk.Scale(root, from_=0, to=100, orient=ttk.HORIZONTAL) +stop_button = ttk.Button(root, text="Stop", command=lambda: on_close()) + +# root.protocol("WM_DELETE_WINDOW", lambda e: on_close()) + +st1_button.grid(row=0, column=0) +st2_button.grid(row=0, column=1) +st3_button.grid(row=0, column=2) +st4_button.grid(row=0, column=3) +st5_button.grid(row=0, column=4) +st6_button.grid(row=0, column=5) +st7_button.grid(row=0, column=6) +st8_button.grid(row=0, column=7) +st9_button.grid(row=0, column=8) + +exit_button.grid(row=1, column=0) +volume_slider.grid(row=1, column=1, columnspan=3) +stop_button.grid(row=1, column=4) + +# Input file path +input_file = "fm_data.raw" + +tuner = None +player = None + +def tune(station): + global tuner + global player + # random_num = random.randint(0, 1000000) + # print(random_num) + if tuner is not None: + tuner.kill() + sleep(3) + if player is not None: + player.kill() + tuner = subprocess.Popen(["rx_fm", "-f", f"{station}M", "-M", "fm", "-s", "170k", "-A", "fast", "-l", "0", "-E", "deemp", "-E", "wav", "-d", "driver=hackrf", input_file]) + sleep(2.5) + player = subprocess.Popen(["C:\\Program Files\\VideoLAN\\VLC\\vlc", "--play-and-exit", "C:\\Users\\spong\\Documents\\Python Programs\\PySDR\\FM Radio\\fm_data.raw"]) + +def on_close(halt=False): + if tuner: + tuner.kill() + if player: + player.kill() + if halt == True: + exit() + +def adj_vol(): + pass + +try: + root.mainloop() +except KeyboardInterrupt: + on_close() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..6e045a8 --- /dev/null +++ b/main.py @@ -0,0 +1,47 @@ +import numpy as np +import matplotlib.pyplot as plt +from pysdr import IQFileSource +from pysdr import FMReceiver +from hackrf import HackRfSource + +# Set the HackRF parameters +sample_rate = 2e6 # 2 MHz sample rate +center_freq = 100e6 # 100 MHz center frequency + +# Set the FM receiver parameters +audio_sample_rate = 48e3 # 48 kHz audio sample rate +fm_deviation = 75e3 # FM deviation (typical for broadcast FM) + +# Create HackRF source +hackrf_source = HackRfSource(sample_rate, center_freq) + +# Create IQ file source +iq_file_source = IQFileSource('fm_radio.iq', sample_rate) + +# Create FM receiver +fm_receiver = FMReceiver(sample_rate, audio_sample_rate, fm_deviation) + +# Read samples from the source and process them +try: + while True: + samples = hackrf_source.read_samples(1024) + if len(samples) == 0: + break + + # Process the samples using the FM receiver + audio_samples = fm_receiver.process(samples) + + # Do something with the audio samples (e.g., play them) + # For simplicity, let's just plot the spectrum + plt.specgram(audio_samples, Fs=audio_sample_rate, cmap='viridis') + plt.xlabel('Time (s)') + plt.ylabel('Frequency (Hz)') + plt.title('FM Radio Spectrum') + plt.show() + +except KeyboardInterrupt: + pass + +finally: + # Close the HackRF source + hackrf_source.close() diff --git a/output55.wav b/output55.wav new file mode 100644 index 0000000..76086f8 Binary files /dev/null and b/output55.wav differ diff --git a/output55.zip b/output55.zip new file mode 100644 index 0000000..3305f89 Binary files /dev/null and b/output55.zip differ diff --git a/output66.wav b/output66.wav new file mode 100644 index 0000000..43c99a2 Binary files /dev/null and b/output66.wav differ diff --git a/test_vlc.py b/test_vlc.py new file mode 100644 index 0000000..bd6ac26 --- /dev/null +++ b/test_vlc.py @@ -0,0 +1,11 @@ +import vlc +import time + +instance = vlc.Instance('--no-xlib') + +player = instance.media_player_new() + +media = instance.media_new('C:\\Users\\spong\\Documents\\Python Programs\\PySDR\\FM Radio\\fm_data.raw') +player.set_media(media) + +player.play() \ No newline at end of file diff --git a/todo.txt b/todo.txt new file mode 100644 index 0000000..7e3b768 --- /dev/null +++ b/todo.txt @@ -0,0 +1,5 @@ +implement volume slider to control the VLC volume. + +put stop button in + +get a 10 second slice of audio and use shazamio to get the song name etc \ No newline at end of file