diff --git a/client.py b/client.py new file mode 100644 index 0000000..d4e2958 --- /dev/null +++ b/client.py @@ -0,0 +1,184 @@ +import asyncio +from datetime import datetime +import dearpygui.dearpygui as dpg +import threading +import socket +import numpy as np +import pickle +import pyaudio +import websockets +from pyogg import OpusDecoder + +class App: + def __init__(self): + self.RDS = None + self.device_name_output = "Speakers (4- USB Audio DAC )" + self.working = False + + def connecttoserver(self, sender, data): + dpg.configure_item("connectbutton", show=False) + protocol = dpg.get_value("serverprotocol") + dpg.configure_item("serverstatus", default_value='connecting...', color=(255, 255, 0)) + if protocol == "Websocket": + asyncio.create_task(self.WSstream()) + return + + ip = dpg.get_value("serverip") + port = dpg.get_value("serverport") + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + s.connect((ip, port)) + except: + dpg.configure_item("connectbutton", show=True) + self.working = True + p = pyaudio.PyAudio() + + device_index_output = 0 + for i in range(p.get_device_count()): + dev = p.get_device_info_by_index(i) + if dev['name'] == self.device_name_output: + device_index_output = dev['index'] + break + + thread = threading.Thread(target=self.stream, args=(s, device_index_output)) + thread.start() + + def disconnectserver(self, sender, data): + dpg.configure_item("disconnectbutton", show=False) + dpg.configure_item("serverstatus", default_value='disconnecting...', color=(255, 255, 0)) + self.working = False + + def RDSshow(self): + try: + dpg.configure_item("RDSinfo", + default_value=f'{self.RDS["PS"]} ({self.RDS["ContentInfo"]["Codec"]} {self.RDS["ContentInfo"]["bitrate"] / 1000}Kbps {self.RDS["AudioMode"]})', + show=True) + dpg.configure_item("RDSPS", default_value="PS: " + self.RDS["PS"]) + dpg.configure_item("RDSRT", default_value="RT: " + self.RDS["RT"]) + dpg.configure_item("RDSCTlocal", default_value="Time Local: " + datetime.fromtimestamp(self.RDS["CT"]["Local"]).strftime('%H:%M:%S')) + dpg.configure_item("RDSCTUTC", default_value="Time UTC: " + datetime.fromtimestamp(self.RDS["CT"]["UTC"]).strftime('%H:%M:%S')) + dpg.configure_item("RDSListener", default_value="Listener: " + str(self.RDS["Listener"]) + " Users") + + except Exception as e: + pass + + def stream(self, socket, deviceindex): + opus_decoder = OpusDecoder() + streamoutput = None + firstrun = True + while True: + try: + if self.working: + data = socket.recv(650000) + + if len(data) == 0: + dpg.configure_item("serverstatus", default_value='lost connected', color=(255, 0, 0)) + socket.close() + dpg.configure_item("showRDS", show=False) + dpg.configure_item("RDSinfo", show=False) + dpg.configure_item("disconnectbutton", show=False) + dpg.configure_item("connectbutton", show=True) + break + + try: + datadecoded = pickle.loads(data) + except: + pass + + if datadecoded["RDS"] != self.RDS: + self.RDS = datadecoded["RDS"] + rdshow = threading.Thread(target=self.RDSshow) + rdshow.start() + + if firstrun: + p = pyaudio.PyAudio() + opus_decoder.set_channels(self.RDS["ContentInfo"]["channel"]) + opus_decoder.set_sampling_frequency(self.RDS["ContentInfo"]["samplerates"]) + streamoutput = p.open(format=pyaudio.paInt16, channels=self.RDS["ContentInfo"]["channel"], rate=self.RDS["ContentInfo"]["samplerates"], output=True, output_device_index=deviceindex) + dpg.configure_item("showRDS", show=True) + dpg.configure_item("serverstatus", default_value='connected', color=(0, 255, 0)) + dpg.configure_item("disconnectbutton", show=True) + firstrun = False + + decoded_pcm = opus_decoder.decode(memoryview(bytearray(datadecoded["Content"]))) + + # Check if the decoded PCM is empty or not + if len(decoded_pcm) > 0: + pcm_to_write = np.frombuffer(decoded_pcm, dtype=np.int16) + + streamoutput.write(pcm_to_write.tobytes()) + else: + print("Decoded PCM is empty") + else: + streamoutput.close() + socket.close() + self.working = False + dpg.configure_item("showRDS", show=False) + dpg.configure_item("RDSinfo", show=False) + dpg.configure_item("connectbutton", show=True) + dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0)) + break + except Exception as e: + if str(e) == "An error occurred while decoding an Opus-encoded packet: corrupted stream": + dpg.configure_item("serverstatus", default_value="Unable to decode audio data", color=(255, 0, 0)) + else: + print("connection lost", e) + self.working = False + streamoutput.close() + socket.close() + dpg.configure_item("showRDS", show=False) + dpg.configure_item("RDSinfo", show=False) + dpg.configure_item("connectbutton", show=True) + dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0)) + break + + def window(self): + with dpg.window(label="IDRB", width=320): + dpg.add_text("", tag="RDSinfo", show=False) + dpg.add_input_text(label="server ip", tag="serverip", default_value="localhost") + dpg.add_input_int(label="port", tag="serverport", max_value=65535, default_value=6980) + dpg.add_combo(["TCP", "Websocket"], label="protocol", tag="serverprotocol", default_value="TCP") + dpg.add_button(label="connect", callback=self.connecttoserver, tag="connectbutton") + dpg.add_button(label="disconnect", callback=self.disconnectserver, tag="disconnectbutton", show=False) + dpg.add_spacer() + dpg.add_text("not connect", tag="serverstatus", color=(255, 0, 0)) + dpg.add_spacer() + with dpg.collapsing_header(label="RDS", show=False, tag="showRDS"): + dpg.add_text("PS: ...", tag="RDSPS") + dpg.add_text("RT: ...", tag="RDSRT") + dpg.add_text("Listener: ...", tag="RDSListener") + dpg.add_text("Time Local: ...", tag="RDSCTlocal") + dpg.add_text("Time UTC: ...", tag="RDSCTUTC") + + + + def init(self): + dpg.create_context() + dpg.create_viewport(title='[ThaiSDR] IDRB (Internet Digital Radio Broadcasting) V1 Beta', width=1280, height=720, small_icon="favicon.ico") # set viewport window + dpg.setup_dearpygui() + # -------------- add code here -------------- + self.window() + # ------------------------------------------- + dpg.show_viewport() + # Start a separate thread for a task + self.thread_stop_event = threading.Event() + + while dpg.is_dearpygui_running(): + self.render() + dpg.render_dearpygui_frame() + + # Signal the thread to stop and wait for it to finish + self.thread_stop_event.set() + dpg.destroy_context() + + def render(self): + # insert here any code you would like to run in the render loop + # you can manually stop by using stop_dearpygui() or self.exit() + pass + + def exit(self): + dpg.destroy_context() + + +app = App() +app.init()