""" This file is part of IDRB Project. IDRB Project is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. IDRB Project is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with IDRB Project. If not, see . """ import queue import time from datetime import datetime import cv2 import dearpygui.dearpygui as dpg import threading import socket import requests import pickle import pyaudio import zmq from pyogg import OpusDecoder import configparser import ctypes import lz4.frame from utils import * import appcomponent class App: def __init__(self): self.RDS = None self.config = configparser.ConfigParser() self.config.read("config.ini") self.device_name_output = self.config["audio"]["device"] self.buffersize = int(self.config["network"]["buffersize"]) self.working = False self.readchannel = 1 self.firstrun = True self.firststart = True self.device_index_output = 0 self.ccdecryptpassword = None self.ccisencrypt = None self.ccisdecrypt = None self.ccisdecryptpassword = None self.paudio = pyaudio.PyAudio() self.cprotocol = None self.cciswaitlogoim = True self.ccthreadlogorecisworking = False self.ccserversecount = 0 self.lsitem = None self.ccconwithpubselect = False self.buffer = queue.Queue(maxsize=self.buffersize) def connecttoserverwithpubselect(self, sender, data): self.ccconwithpubselect = True dpg.configure_item("pubserverselectwindow", show=False) dpg.configure_item("connectbuttonpubserverselect", show=False) self.connecttoserver(None, None) def connecttoserver(self, sender, data): dpg.configure_item("connectservergroup", show=False) dpg.configure_item("serverstatus", default_value='connecting...', color=(255, 255, 0)) if self.ccconwithpubselect: serverlabel = str(dpg.get_item_configuration(self.lsitem)["label"]) protocol = serverlabel.split("|")[0].strip() ip = serverlabel.split("|")[1].split(":")[0].strip() port = serverlabel.split("|")[1].split(":")[1].strip() self.ccconwithpubselect = False else: protocol = dpg.get_value("serverprotocol").strip() ip = dpg.get_value("serverip").strip() port = dpg.get_value("serverport") self.cprotocol = protocol if protocol == "TCP": s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: s.connect((ip, port)) except: dpg.configure_item("connectbutton", show=True) elif protocol == "ZeroMQ": context = zmq.Context() s = context.socket(zmq.SUB) s.connect(f"tcp://{ip}:{port}") s.setsockopt_string(zmq.SUBSCRIBE, "") elif protocol == "ZeroMQ (WS)": context = zmq.Context() s = context.socket(zmq.SUB) s.connect(f"ws://{ip}:{port}") s.setsockopt_string(zmq.SUBSCRIBE, "") self.cprotocol = "ZeroMQ" self.working = True self.device_index_output = 0 for i in range(self.paudio.get_device_count()): dev = self.paudio.get_device_info_by_index(i) if dev['name'] == self.device_name_output: self.device_index_output = dev['index'] break thread = threading.Thread(target=self.stream, args=(s, )) thread.start() def disconnectserver(self, sender=None, data=None): dpg.configure_item("disconnectbutton", show=False) dpg.configure_item("serverstatus", default_value='disconnecting...', color=(255, 255, 0)) self.working = False dpg.configure_item("serverinfobutton", show=False) dpg.configure_item("mediachannelselect", show=False) dpg.configure_item("morerdsbutton", show=False) dpg.configure_item("station_logo_config", show=False) dpg.configure_item("RDSinfo", show=False) dpg.configure_item("disconnectbutton", show=False) dpg.configure_item("logostatus", show=False) self.firstrun = True self.firststart = True self.ccdecryptpassword = None self.ccisencrypt = None self.ccisdecrypt = None self.ccisdecryptpassword = None self.cciswaitlogoim = True self.ccthreadlogorecisworking = False # clear buffer def RDSshow(self): try: dpg.configure_item("RDSinfo", default_value=f'{self.RDS["PS"]} ({self.RDS["ContentInfo"]["Codec"].upper()} {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: " + limit_string_in_line(self.RDS["RT"], 120)) 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')) try: if self.RDS["images"]["logo"]["lazy"] and not self.ccthreadlogorecisworking: if not self.RDS["images"]["logo"]["contents"] == b'': print(self.RDS["images"]["logo"]["contents"] == b'', self.RDS["images"]["logo"]["part"]["total"] == \ self.RDS["images"]["logo"]["part"]["current"], self.RDS["images"]["logo"]["part"]["current"] > 0) self.ccthreadlogorecisworking = True logoreciveprocessingthread = threading.Thread(target=self.RDSlogorecivelazy) logoreciveprocessingthread.start() else: if not self.RDS["images"]["logo"]["lazy"]: dpg.set_value("station_logo", CV22DPG(cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"]["contents"], np.uint8), cv2.IMREAD_COLOR))) dpg.configure_item("logostatus", show=False) except Exception as e: dpg.configure_item("station_logo_config", show=False) #print(e) except Exception as e: pass def RDSlogorecivelazy(self): try: received_data = b"" received_data_current_past = b"" received_data_current = b"" try: print(self.RDS["images"]["logo"]["part"]["current"], self.RDS["images"]["logo"]["part"]["total"]) while not self.RDS["images"]["logo"]["part"]["current"] == self.RDS["images"]["logo"]["part"]["total"]: currentprocess = self.RDS["images"]["logo"]["part"]["current"] totalprocess = self.RDS["images"]["logo"]["part"]["total"] received_data_current = self.RDS["images"]["logo"]["contents"] if received_data_current != received_data_current_past: print(received_data_current) received_data_current_past = received_data_current received_data += received_data_current dpg.configure_item("logostatus", color=(255, 255, 0), default_value=f"Receiving... ({currentprocess}/{totalprocess})") dpg.set_value("station_logo", CV22DPG(cv2.imdecode(np.frombuffer(received_data_current, np.uint8), cv2.IMREAD_COLOR))) dpg.configure_item("logostatus", color=(0, 255, 0), default_value=f"Received logo! waiting for new image...") dpg.configure_item("station_logo_config", show=True) self.ccthreadlogorecisworking = False except Exception as e: print("receive error", e) dpg.configure_item("logostatus", color=(255, 0, 0), default_value=f"Receive logo error! waiting for new image...") dpg.configure_item("station_logo_config", show=False) self.ccthreadlogorecisworking = False except Exception as e: pass def changechannel(self, sender, data): dpg.configure_item("serverstatus", default_value='please wait...', color=(255, 255, 0)) dpg.configure_item("station_logo_config", show=False) self.readchannel = int(dpg.get_value(sender).split(" ")[0]) self.firstrun = True self.ccdecryptpassword = None self.ccisencrypt = None self.ccisdecrypt = None self.ccisdecryptpassword = None self.device_index_output = 0 for i in range(self.paudio.get_device_count()): dev = self.paudio.get_device_info_by_index(i) if dev['name'] == self.device_name_output: self.device_index_output = dev['index'] break def submitpassworddecrypt(self, sender, data): dpg.configure_item("requestpasswordpopup", show=False) self.ccdecryptpassword = dpg.get_value("requestpasswordinputpopup") self.ccisdecryptpassword = True def changeaudiodevice(self, sender, data): self.device_name_output = dpg.get_value(sender) self.config["audio"]["device"] = dpg.get_value(sender) self.config.write(open('config.ini', 'w')) def changebuffersize(self, sender, data): self.buffersize = int(dpg.get_value(sender)) self.config["network"]["buffersize"] = str(dpg.get_value(sender)) self.config.write(open('config.ini', 'w')) def pubserverselectone(self, sender, data): if data == False: dpg.configure_item("connectbuttonpubserverselect", show=False) return else: dpg.configure_item("connectbuttonpubserverselect", show=True) if self.lsitem == None: self.lsitem = sender if self.lsitem != sender: dpg.set_value(self.lsitem, False) self.lsitem = sender if dpg.get_item_configuration(self.lsitem)["label"] == "N/A": dpg.configure_item("connectbuttonpubserverselect", show=False) def pubserverselectsearch(self, serversearch="", limit=10): self.lsitem = None if serversearch == "": dpg.configure_item("pubserverselectstatus", default_value="Please wait...", color=(255, 255, 0)) else: dpg.configure_item("pubserverselectstatus", default_value="Searching...", color=(255, 255, 0)) if self.ccserversecount != 0: # clear list for i in range(self.ccserversecount): dpg.delete_item(f"pubserverid{i}") self.ccserversecount = 0 if serversearch == "": response = requests.get(f"https://thaisdr.damp11113.xyz/api/idrbdir/getallstation?limit={limit}") else: response = requests.get(f"https://thaisdr.damp11113.xyz/api/idrbdir/getallstation?limit={limit}&serversearch={serversearch}") # Check if the request was successful (status code 200) if response.status_code == 200: # Parse JSON data allstationdata = response.json() # Iterate over each station for server_id, server_details in allstationdata.items(): with dpg.table_row(parent="pubserverlist", tag=f"pubserverid{self.ccserversecount}"): if server_details['ServerURL'] == "" or server_details['ServerPort'] == "" or server_details['ServerProtocol'] == "": dpg.add_selectable(label=f"N/A", span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) else: dpg.add_selectable(label=f"{server_details['ServerProtocol']} | {server_details['ServerURL']}:{server_details['ServerPort']}", span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) dpg.add_selectable(label=server_details["ServerName"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) dpg.add_selectable(label=server_details["ServerDesc"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) dpg.add_selectable(label=server_details["ConnectionUser"], span_columns=True, disable_popup_close=True, callback=self.pubserverselectone) self.ccserversecount += 1 dpg.configure_item("pubserverselectstatus", default_value=f"Founded {self.ccserversecount} server", color=(0, 255, 0)) def pubserverselectopen(self, sender, data): dpg.configure_item("pubserverselectwindow", show=True) self.pubserverselectsearch() def streambuffer(self, socket): while self.working: try: if self.cprotocol == "TCP": tempdata = b'' # data = socket.recv(1580152) while True: part = socket.recv(1024) tempdata += part if len(part) < 1024: # either 0 or end of data break self.buffer.put(tempdata, timeout=0.1) elif self.cprotocol == "ZeroMQ": self.buffer.put(socket.recv(), timeout=0.1) else: self.buffer.put(b"") dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}', color=(0, 255, 0)) except queue.Full: dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}', color=(255, 0, 0)) while not self.buffer.empty(): self.buffer.get() dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}', color=(255, 255, 0)) dpg.configure_item("serverstatus", default_value='disconnected', color=(255, 0, 0)) dpg.configure_item("connectservergroup", show=True) def stream(self, socket): opus_decoder = None streamoutput = None tfrpx = list(range(250)) altfrpy = [0] * 250 adcctfrpy = [0] * 250 imcctfrpy = [0] * 2500 bytesconunt = 0 bytesconunt_frame = 0 codecbytesconunt = 0 start_time = time.time() evaluation_audio_X = None decodecodec = None SBT = threading.Thread(target=self.streambuffer, args=(socket,)) SBT.start() while True: try: if self.working: if self.buffer.not_empty: data = self.buffer.get() else: dpg.configure_item("serverstatus", default_value='Buffering...', color=(255, 255, 0)) continue bytesconunt += len(data) if bytesconunt_frame >= 10: stoptime = time.time() speed_kbps = calculate_speed(start_time, stoptime, bytesconunt) dpg.configure_item("serverstatus", default_value=f'connected {int(speed_kbps)}Kbps ({len(data)})', color=(0, 255, 0)) codec_kbps = calculate_throughput(start_time, stoptime, codecbytesconunt) dpg.configure_item("codecbitratestatus", default_value=f'{self.RDS["ContentInfo"]["Codec"].upper()} {self.RDS["ContentInfo"]["bitrate"] / 1000}/{codec_kbps:.2f} Kbps') dpg.configure_item("bufferstatus", default_value=f'Buffer: {self.buffer.qsize()}/{self.buffersize}') start_time = time.time() bytesconunt_frame = 0 bytesconunt = 0 codecbytesconunt = 0 if len(altfrpy) > 250: altfrpy.pop(0) altfrpy.append(len(data)) dpg.set_value('transferatealldataplot', [tfrpx, altfrpy]) if len(data) == 0: dpg.configure_item("serverstatus", default_value='lost connected', color=(255, 0, 0)) socket.close() if self.cprotocol == "TCP": socket.close() elif self.cprotocol == "ZeroMQ": try: message = socket.recv(zmq.NOBLOCK) if message is None: break # No more messages # Process the received message if needed print(f"Received message: {message.decode()}") except zmq.error.ZMQError as e: if e.errno == zmq.EAGAIN: break # No more messages else: raise socket.close() else: socket.close() self.disconnectserver() break try: decompressed_data = lz4.frame.decompress(data) datadecoded = pickle.loads(decompressed_data) except: pass if len(imcctfrpy) > 250: imcctfrpy.pop(0) imcctfrpy.append(len(str(datadecoded["channel"][self.readchannel]["RDS"]["images"]))) dpg.set_value('transferateimagesoncchannelplot', [tfrpx, imcctfrpy]) try: if datadecoded["channel"][self.readchannel]["RDS"] != self.RDS: self.RDS = datadecoded["channel"][self.readchannel]["RDS"] rdshow = threading.Thread(target=self.RDSshow) rdshow.start() dpg.configure_item("ServerListener", default_value="Listener: " + str(datadecoded["serverinfo"]["Listener"]) + " Users") dpg.configure_item("ServerNamedisp", default_value="Server: " + str(datadecoded["serverinfo"]["RDS"]["ServerName"])) dpg.configure_item("ServerDescdisp", default_value="Description: " + str(datadecoded["serverinfo"]["RDS"]["ServerDesc"])) except: pass if self.firstrun: decodecodec = datadecoded["channel"][self.readchannel]["RDS"]["ContentInfo"]["Codec"] if decodecodec.upper() == "OPUS": opus_decoder = OpusDecoder() opus_decoder.set_channels(self.RDS["ContentInfo"]["channel"]) opus_decoder.set_sampling_frequency(self.RDS["ContentInfo"]["samplerates"]) streamoutput = self.paudio.open(format=pyaudio.paInt16, channels=self.RDS["ContentInfo"]["channel"], rate=self.RDS["ContentInfo"]["samplerates"], output=True, output_device_index=self.device_index_output) evaluation_audio_X = np.fft.fftfreq(1024, 1.0 / self.RDS["ContentInfo"]["samplerates"])[:1024 // 2] if len(datadecoded["channel"]) > 1: channel_info = [] for i in range(1, len(datadecoded["channel"]) + 1): channel_info.append(f'{i} {"[Encrypt]" if datadecoded["channel"][i]["Encrypt"] else "[No Encrypt]"} {datadecoded["channel"][i]["Station"]} ({datadecoded["channel"][i]["RDS"]["ContentInfo"]["Codec"]} {datadecoded["channel"][i]["RDS"]["ContentInfo"]["bitrate"] / 1000}Kbps {datadecoded["channel"][i]["RDS"]["AudioMode"]})') dpg.configure_item("mediachannelselect", show=True, items=channel_info) dpg.configure_item("morerdsbutton", show=True) dpg.configure_item("serverinfobutton", show=True) dpg.configure_item("logostatus", show=True) try: if self.RDS["images"]["logo"]["lazy"] and not self.ccthreadlogorecisworking: if not self.RDS["images"]["logo"]["contents"] == b'' or \ self.RDS["images"]["logo"]["part"]["total"] == \ self.RDS["images"]["logo"]["part"]["current"] or \ self.RDS["images"]["logo"]["part"]["current"] > 0: self.ccthreadlogorecisworking = True logoreciveprocessingthread = threading.Thread(target=self.RDSlogorecivelazy) logoreciveprocessingthread.start() else: if not self.RDS["images"]["logo"]["lazy"]: dpg.set_value("station_logo", CV22DPG( cv2.imdecode(np.frombuffer(self.RDS["images"]["logo"]["contents"], np.uint8), cv2.IMREAD_COLOR))) dpg.configure_item("station_logo_config", show=True) dpg.configure_item("logostatus", show=False) except: dpg.configure_item("station_logo_config", show=False) dpg.configure_item("disconnectbutton", show=True) dpg.configure_item("RDSPI", default_value=f"PI: {hex(self.RDS['PI'])[2:].upper()}") if self.firststart and len(datadecoded["channel"]) > 1: self.readchannel = datadecoded["mainchannel"] dpg.configure_item("mediachannelselect", show=True, default_value=channel_info[self.readchannel - 1]) elif self.firststart: self.readchannel = datadecoded["mainchannel"] # check if channel is encrypted if datadecoded["channel"][self.readchannel]["Encrypt"]: dpg.configure_item("requestpasswordpopup", show=True) dpg.configure_item("serverstatus", default_value='connected', color=(0, 255, 0)) self.ccisencrypt = True else: dpg.configure_item("serverstatus", default_value='connected --Kbps (----)', color=(0, 255, 0)) self.firstrun = False self.firststart = False if not self.firstrun: data = datadecoded["channel"][self.readchannel]["Content"] if len(data) == 0: dpg.configure_item("serverstatus", default_value=f'connected but no audio', color=(255, 0, 0)) if self.ccisdecryptpassword and self.ccisencrypt: try: # decrypt data encryptdata = data.split(b'|||||')[0] salt = data.split(b'|||||')[1] iv = data.split(b'|||||')[2] data = decrypt_data(encryptdata, self.ccdecryptpassword, salt, iv) if data == b'': self.ccisdecrypt = False self.ccdecryptpassword = None else: self.ccisdecrypt = True except: dpg.configure_item("serverstatus", default_value="Decrypt Error", color=(255, 0, 0)) if self.ccisdecrypt or not self.ccisencrypt: codecbytesconunt += len(data) if decodecodec.upper() == "OPUS": decoded_pcm = opus_decoder.decode(memoryview(bytearray(data))) else: # pcm decoded_pcm = data # 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()) audioL = pcm_to_write[::2] audioR = pcm_to_write[1::2] Lnormalized_data = audioL * np.hanning(len(audioL)) Lfft_data = np.abs(np.fft.fft(Lnormalized_data))[:1024 // 2] Rnormalized_data = audioR * np.hanning(len(audioR)) Rfft_data = np.abs(np.fft.fft(Rnormalized_data))[:1024 // 2] dpg.set_value('audioinfoleftplot', [evaluation_audio_X, Lfft_data]) dpg.set_value('audioinforightplot', [evaluation_audio_X, Rfft_data]) else: print("Decoded PCM is empty") if len(adcctfrpy) > 250: adcctfrpy.pop(0) adcctfrpy.append(len(datadecoded["channel"][self.readchannel]["Content"])) dpg.set_value('transferateaudiodataoncchannelplot', [tfrpx, adcctfrpy]) bytesconunt_frame += 1 else: SBT.join() streamoutput.close() if self.cprotocol == "TCP": socket.close() elif self.cprotocol == "ZeroMQ": try: message = socket.recv(zmq.NOBLOCK) if message is None: break # No more messages # Process the received message if needed print(f"Received message: {message.decode()}") except zmq.error.ZMQError as e: if e.errno == zmq.EAGAIN: break # No more messages else: raise socket.close() else: socket.close() self.disconnectserver() 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) try: streamoutput.close() except: pass socket.close() if self.cprotocol == "TCP": socket.close() elif self.cprotocol == "ZeroMQ": try: message = socket.recv(zmq.NOBLOCK) if message is None: break # No more messages # Process the received message if needed print(f"Received message: {message.decode()}") except zmq.error.ZMQError as e: if e.errno == zmq.EAGAIN: break # No more messages else: raise socket.close() else: socket.close() self.disconnectserver() break def init(self): if self.config["debug"]["hideconsole"] == "true": ctypes.windll.user32.ShowWindow(ctypes.windll.kernel32.GetConsoleWindow(), 0) ctypes.CDLL("opus.dll") dpg.create_context() dpg.create_viewport(title=f'IDRB Client v1.6.2 Beta', width=1280, height=720, large_icon="IDRBfavicon.ico", clear_color=(43, 45, 48)) # set viewport window dpg.setup_dearpygui() # -------------- add code here -------------- noimage_texture_data = [] for i in range(0, 128 * 128): noimage_texture_data.append(20 / 255) noimage_texture_data.append(0) noimage_texture_data.append(20 / 255) noimage_texture_data.append(20 / 255) with dpg.texture_registry(): dpg.add_raw_texture(128, 128, noimage_texture_data, tag="station_logo", format=dpg.mvFormat_Float_rgb) width, height, channels, data = dpg.load_image("IDRBlogo.png") dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo") dpg.add_static_texture(width=512, height=256, default_value=data, tag="app_logo_background") with dpg.window(no_background=True, no_title_bar=True, no_move=True, no_resize=True, tag="backgroundviewportlogo"): dpg.add_image("app_logo_background") dpg.add_text("ThaiSDR Solutions", pos=(230, 230)) appcomponent.window(self) appcomponent.menubar(self) num_devices = self.paudio.get_device_count() output_devices = [] for i in range(num_devices): device_info = self.paudio.get_device_info_by_index(i) if device_info['maxOutputChannels'] > 0: output_devices.append(device_info['name']) dpg.configure_item("selectaudiooutputdevicecombo", items=output_devices, default_value=self.config["audio"]["device"]) dpg.configure_item("buffersizeintinput", default_value=int(self.config["network"]["buffersize"])) # ------------------------------------------- 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() try: dpg.fit_axis_data("x_axis") dpg.fit_axis_data("y_axis1") dpg.fit_axis_data("y_axis2") dpg.fit_axis_data("y_axis3") dpg.fit_axis_data("x_axis_1") dpg.configure_item("backgroundviewportlogo", pos=(dpg.get_viewport_width() - 550, dpg.get_viewport_height() - 300)) except: pass def exit(self): dpg.destroy_context() app = App() app.init()