diff --git a/src/midi.py b/src/midi.py index 14370b3..6c3f805 100644 --- a/src/midi.py +++ b/src/midi.py @@ -1,132 +1,253 @@ import mido import asyncio -import networking # <--- This will now correctly import your module +import networking +import socket +import json +import logging # Added logging import +# Configure logging +DEBUG_MODE = True # Set to False for INFO level logging +logging.basicConfig(level=logging.DEBUG if DEBUG_MODE else logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -async def midi_to_websocket_listener(midi_port_index: int, websocket_uri: str): - """ - Listens to a specific MIDI port and sends data to a WebSocket server - when Note 32 (and 33) is pressed. - """ - delay = 100 # Default delay value +# TCP Server Configuration +TCP_HOST = "127.0.0.1" +TCP_PORT = 65432 - # 1. Get MIDI port name +# Sound Control Server Configuration (for sending reset) +SOUND_CONTROL_HOST = "127.0.0.1" +SOUND_CONTROL_PORT = 65433 + +class MidiHandler: + def __init__(self, midi_port_index: int, websocket_uri: str): + self.midi_port_index = midi_port_index + self.websocket_uri = websocket_uri + self.ws_client = networking.WebSocketClient(websocket_uri) + self.delay = 100 # Default delay value, controlled by MIDI controller + self.brightness = 100 # Default brightness value, controlled by MIDI controller + self.tcp_host = TCP_HOST + self.tcp_port = TCP_PORT + self.beat_sending_enabled = True # New: Local flag for beat sending + self.sound_control_host = SOUND_CONTROL_HOST + self.sound_control_port = SOUND_CONTROL_PORT + + async def _send_reset_to_sound(self): + try: + reader, writer = await asyncio.open_connection(self.sound_control_host, self.sound_control_port) + cmd = "RESET_TEMPO\n".encode('utf-8') + writer.write(cmd) + await writer.drain() + resp = await reader.read(100) + logging.info(f"[MidiHandler - Control] Sent RESET_TEMPO, response: {resp.decode().strip()}") + writer.close() + await writer.wait_closed() + except Exception as e: + logging.error(f"[MidiHandler - Control] Failed to send RESET_TEMPO: {e}") + + async def _handle_tcp_client(self, reader, writer): + addr = writer.get_extra_info('peername') + logging.info(f"[MidiHandler - TCP Server] Connected by {addr}") # Changed to info + + try: + while True: + data = await reader.read(4096) # Read up to 4KB of data + if not data: + logging.info(f"[MidiHandler - TCP Server] Client {addr} disconnected.") # Changed to info + break + + message = data.decode().strip() + logging.debug(f"[MidiHandler - TCP Server] Received from {addr}: {message}") # Changed to debug + + if self.beat_sending_enabled: + try: + # Attempt to parse as float (BPM) from sound.py + bpm_value = float(message) + # Construct JSON message using the current MIDI-controlled delay and brightness + json_message = { + "names": ["0"], + "settings": { + "pattern": "pulse", + "delay": self.delay, # Use MIDI-controlled delay + "colors": ["#00ff00"], + "brightness": self.brightness, + "num_leds": 200, + }, + } + logging.debug(f"[MidiHandler - TCP Server] Forwarding BPM-derived JSON message to WebSocket with delay {self.delay}, brightness {self.brightness}: {json_message}") # Changed to debug + await self.ws_client.send_data(json_message) + except ValueError: + logging.warning(f"[MidiHandler - TCP Server] Received non-BPM message from {addr}, not forwarding: {message}") # Changed to warning + except Exception as e: + logging.error(f"[MidiHandler - TCP Server] Error processing received message from {addr}: {e}") # Changed to error + else: + logging.debug(f"[MidiHandler - TCP Server] Beat received from {addr} but sending to WebSocket is disabled: {message}") # Changed to debug + + except asyncio.CancelledError: + logging.info(f"[MidiHandler - TCP Server] Client handler for {addr} cancelled.") # Changed to info + except Exception as e: + logging.error(f"[MidiHandler - TCP Server] Error handling client {addr}: {e}") # Changed to error + finally: + logging.info(f"[MidiHandler - TCP Server] Closing connection for {addr}") # Changed to info + writer.close() + await writer.wait_closed() + + async def _midi_tcp_server(self): + server = await asyncio.start_server( + lambda r, w: self._handle_tcp_client(r, w), self.tcp_host, self.tcp_port) + + addrs = ', '.join(str(sock.getsockname()) for sock in server.sockets) + logging.info(f"[MidiHandler - TCP Server] Serving on {addrs}") # Changed to info + + async with server: + await server.serve_forever() + + async def _midi_listener(self): + logging.info("Midi function") # Changed to info + """ + Listens to a specific MIDI port and sends data to a WebSocket server + when Note 32 (and 33) is pressed. + """ + + # 1. Get MIDI port name + port_names = mido.get_input_names() + if not port_names: + logging.warning("No MIDI input ports found. Please connect your device.") # Changed to warning + return + if not (0 <= self.midi_port_index < len(port_names)): + logging.error(f"Error: MIDI port index {self.midi_port_index} out of range. Available ports: {port_names}") # Changed to error + logging.info("Available ports:") # Changed to info + for i, name in enumerate(port_names): + logging.info(f" {i}: {name}") # Changed to info + return + + midi_port_name = port_names[self.midi_port_index] + logging.info(f"Selected MIDI input port: {midi_port_name}") # Changed to info + + try: + with mido.open_input(midi_port_name) as port: + logging.info(f"MIDI port '{midi_port_name}' opened. Press Ctrl+C to stop.") # Changed to info + while True: + msg = port.receive(block=False) # Non-blocking read + if msg: + logging.debug(msg) # Changed to debug + match msg.type: + case 'note_on': + logging.debug(f" Note ON: Note={msg.note}, Velocity={msg.velocity}, Channel={msg.channel}") # Changed to debug + pattern_name = "Unknown" + + match msg.note: + case 48: # Original Note 48 for 'pulse' + pattern_name = "pulse" + await self.ws_client.send_data({ + "names": ["0"], + "settings": { + "pattern": pattern_name, + "delay": self.delay, # Use MIDI-controlled delay + "colors": ["#00ff00"], + "brightness": self.brightness, + "num_leds": 120, + } + }) + case 49: # Original Note 49 for 'theater_chase' + pattern_name = "theater_chase" + await self.ws_client.send_data({ + "names": ["0"], + "settings": { + "pattern": pattern_name, + "delay": self.delay, # Use MIDI-controlled delay + "colors": ["#00ff00"], + "brightness": self.brightness, + "num_leds": 120, + "on_width": 10, + "off_width": 10, + "n1": 0, + "n2": 100 + } + }) + case 50: # Original Note 50 for 'alternating' + pattern_name = "alternating" + logging.debug("Triggering Alternating Pattern") # Changed to debug + await self.ws_client.send_data({ + "names": ["0"], + "settings": { + "pattern": pattern_name, + "delay": self.delay, # Use MIDI-controlled delay + "colors": ["#00ff00", "#0000ff"], + "brightness": self.brightness, + "num_leds": 120, + "n1": 10, + "n2": 10 + } + }) + + case 'control_change': + match msg.control: + case 36: + self.delay = msg.value * 4 # Update instance delay + logging.info(f"Delay set to {self.delay} ms by MIDI controller") # Changed to info + case 27: + if msg.value == 127: + self.beat_sending_enabled = True + logging.info("[MidiHandler - Listener] Beat sending ENABLED by MIDI control.") # Changed to info + elif msg.value == 0: + self.beat_sending_enabled = False + logging.info("[MidiHandler - Listener] Beat sending DISABLED by MIDI control.") # Changed to info + case 29: + if msg.value == 127: + logging.info("[MidiHandler - Listener] RESET_TEMPO requested by control 29.") + await self._send_reset_to_sound() + case 37: + # Map 0-127 to 0-100 brightness scale + self.brightness = round((msg.value / 127) * 100) + logging.info(f"Brightness set to {self.brightness} by MIDI controller") + + await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop + + except mido.PortsError as e: + logging.error(f"Error opening MIDI port '{midi_port_name}': {e}") # Changed to error + except asyncio.CancelledError: + logging.info(f"MIDI listener cancelled.") # Changed to info + except Exception as e: + logging.error(f"An unexpected error occurred in MIDI listener: {e}") # Changed to error + + async def run(self): + try: + await self.ws_client.connect() + logging.info(f"[MidiHandler] WebSocket client connected to {self.ws_client.uri}") # Changed to info + + await asyncio.gather( + self._midi_listener(), + self._midi_tcp_server() + ) + except mido.PortsError as e: + logging.error(f"[MidiHandler] Error opening MIDI port: {e}") # Changed to error + except asyncio.CancelledError: + logging.info("[MidiHandler] Tasks cancelled due to program shutdown.") # Changed to info + except KeyboardInterrupt: + logging.info("\n[MidiHandler] Program interrupted by user.") # Changed to info + finally: + logging.info("[MidiHandler] Main program finished. Closing WebSocket client...") # Changed to info + await self.ws_client.close() + logging.info("[MidiHandler] WebSocket client closed.") # Changed to info + +def print_midi_ports(): + logging.info("\n--- Available MIDI Input Ports ---") # Changed to info port_names = mido.get_input_names() if not port_names: - print("No MIDI input ports found. Please connect your device.") - return - if not (0 <= midi_port_index < len(port_names)): - print(f"Error: MIDI port index {midi_port_index} out of range. Available ports: {port_names}") - print("Available ports:") + logging.warning("No MIDI input ports found.") # Changed to warning + else: for i, name in enumerate(port_names): - print(f" {i}: {name}") - return - - midi_port_name = port_names[midi_port_index] - print(f"Selected MIDI input port: {midi_port_name}") - - # 2. Initialize WebSocket client (using your actual networking.py) - ws_client = networking.WebSocketClient(websocket_uri) - - try: - # 3. Connect WebSocket - await ws_client.connect() - print(f"WebSocket client connected to {ws_client.uri}") - - # 4. Open MIDI port and start listening loop - with mido.open_input(midi_port_name) as port: - print(f"MIDI port '{midi_port_name}' opened. Press Ctrl+C to stop.") - while True: - msg = port.receive(block=False) # Non-blocking read - if msg: - match msg.type: - case 'note_on': - print(f" Note ON: Note={msg.note}, Velocity={msg.velocity}, Channel={msg.channel}") - # Add pattern_name variable to update a GUI later if needed. - pattern_name = "Unknown" - - match msg.note: - case 48: # Original Note 48 for 'pulse' - pattern_name = "pulse" - await ws_client.send_data({ - "names": ["1"], - "settings": { - "pattern": pattern_name, - "delay": delay, - "colors": ["#00ff00"], - "brightness": 100, - "num_leds": 120, # Corrected to 120 - } - }) - case 49: # Original Note 49 for 'theater_chase' - pattern_name = "theater_chase" - await ws_client.send_data({ - "names": ["1"], - "settings": { - "pattern": pattern_name, - "delay": delay, - "colors": ["#00ff00"], - "brightness": 100, - "num_leds": 120, - "on_width": 10, - "off_width": 10, - "n1": 0, - "n2": 100 - } - }) - case 50: # Original Note 50 for 'alternating' - pattern_name = "alternating" - print("Triggering Alternating Pattern") - await ws_client.send_data({ - "names": ["1"], - "settings": { - "pattern": pattern_name, - "delay": delay, - "colors": ["#00ff00", "#0000ff"], - "brightness": 100, - "num_leds": 120, - "n1": 10, - "n2": 10 - } - }) - # Potentially add logic here to use pattern_name if a GUI update is desired in the future - - case 'control_change': - match msg.control: - case 36: - - delay = msg.value * 4 - print(f"Delay set to {delay} ms") - - await asyncio.sleep(0.001) # Important: Yield control to asyncio event loop - - except mido.PortsError as e: - print(f"Error opening MIDI port '{midi_port_name}': {e}") - except asyncio.CancelledError: - print(f"MIDI listener cancelled.") - except Exception as e: - print(f"An unexpected error occurred: {e}") - finally: - # 5. Disconnect WebSocket and clean up - # This assumes your WebSocketClient has a ._connected attribute or similar way to check state. - # If your client's disconnect method is safe to call even if not connected, you can simplify. - await ws_client.close() - print("MIDI listener stopped and cleaned up.") - + logging.info(f" {i}: {name}") # Changed to info + logging.info("----------------------------------") # Changed to info async def main(): + print_midi_ports() # --- Configuration --- MIDI_PORT_INDEX = 1 # <--- IMPORTANT: Change this to the correct index for your device WEBSOCKET_SERVER_URI = "ws://192.168.4.1:80/ws" # --- End Configuration --- - try: - await midi_to_websocket_listener(MIDI_PORT_INDEX, WEBSOCKET_SERVER_URI) - except KeyboardInterrupt: - print("\nProgram interrupted by user.") - finally: - print("Main program finished.") - + midi_handler = MidiHandler(MIDI_PORT_INDEX, WEBSOCKET_SERVER_URI) + await midi_handler.run() if __name__ == "__main__": asyncio.run(main()) \ No newline at end of file diff --git a/src/sound.py b/src/sound.py index 6b405bb..7c2a0b8 100644 --- a/src/sound.py +++ b/src/sound.py @@ -4,121 +4,207 @@ import pyaudio import aubio import numpy as np from time import sleep -import websocket # pip install websocket-client import json +import socket +import time +import logging # Added logging import +import asyncio # Re-added asyncio import +import threading # Added threading for control server -seconds = 10 # how long this script should run (if not using infinite loop) +# Configure logging +DEBUG_MODE = True # Set to False for INFO level logging +logging.basicConfig(level=logging.DEBUG if DEBUG_MODE else logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') -bufferSize = 512 -windowSizeMultiple = 2 # or 4 for higher accuracy, but more computational cost +# TCP Server Configuration (assuming midi.py runs this) +MIDI_TCP_HOST = "127.0.0.1" +MIDI_TCP_PORT = 65432 -audioInputDeviceIndex = 7 # use 'arecord -l' to check available audio devices -audioInputChannels = 1 +# Sound Control Server Configuration (for midi.py to control sound.py) +SOUND_CONTROL_HOST = "127.0.0.1" +SOUND_CONTROL_PORT = 65433 -pa = pyaudio.PyAudio() +class SoundBeatDetector: + def __init__(self, tcp_host: str, tcp_port: int): + self.tcp_host = tcp_host + self.tcp_port = tcp_port + self.tcp_socket = None + self.connected_to_midi = False + self.reconnect_delay = 1 # seconds + # Note: beat_sending_enabled is not used in this simplified flow -print("Available audio input devices:") -info = pa.get_host_api_info_by_index(0) -num_devices = info.get('deviceCount') -found_device = False -for i in range(0, num_devices): - device_info = pa.get_device_info_by_host_api_device_index(0, i) - if (device_info.get('maxInputChannels')) > 0: - print(f" Input Device id {i} - {device_info.get('name')}") - if i == audioInputDeviceIndex: - found_device = True + self.bufferSize = 512 + self.windowSizeMultiple = 2 + self.audioInputDeviceIndex = 7 + self.audioInputChannels = 1 -if not found_device: - print(f"Warning: Audio input device index {audioInputDeviceIndex} not found or has no input channels.") - # Consider exiting or picking a default if necessary + self.pa = pyaudio.PyAudio() -try: - audioInputDevice = pa.get_device_info_by_index(audioInputDeviceIndex) - audioInputSampleRate = int(audioInputDevice['defaultSampleRate']) -except Exception as e: - print(f"Error getting audio device info for index {audioInputDeviceIndex}: {e}") - pa.terminate() - exit() + logging.info("Available audio input devices:") + info = self.pa.get_host_api_info_by_index(0) + num_devices = info.get('deviceCount') + found_device = False + for i in range(0, num_devices): + device_info = self.pa.get_device_info_by_host_api_device_index(0, i) + if (device_info.get('maxInputChannels')) > 0: + logging.info(f" Input Device id {i} - {device_info.get('name')}") + if i == self.audioInputDeviceIndex: + found_device = True -# create the aubio tempo detection: -hopSize = bufferSize -winSize = hopSize * windowSizeMultiple -tempoDetection = aubio.tempo(method='default', buf_size=winSize, hop_size=hopSize, samplerate=audioInputSampleRate) + if not found_device: + logging.warning(f"Audio input device index {self.audioInputDeviceIndex} not found or has no input channels.") -# --- WebSocket Setup --- -websocket_url = "ws://192.168.4.1:80/ws" -ws = None -try: - ws = websocket.create_connection(websocket_url) - print(f"Successfully connected to WebSocket at {websocket_url}") -except Exception as e: - print(f"Failed to connect to WebSocket: {e}. Data will not be sent over WebSocket.") -# --- End WebSocket Setup --- + try: + audioInputDevice = self.pa.get_device_info_by_index(self.audioInputDeviceIndex) + self.audioInputSampleRate = int(audioInputDevice['defaultSampleRate']) + except Exception as e: + logging.error(f"Error getting audio device info for index {self.audioInputDeviceIndex}: {e}") + self.pa.terminate() + exit() -# this function gets called by the input stream, as soon as enough samples are collected from the audio input: -def readAudioFrames(in_data, frame_count, time_info, status): - global ws # Allow modification of the global ws variable + self.hopSize = self.bufferSize + self.winSize = self.hopSize * self.windowSizeMultiple + self.tempoDetection = aubio.tempo(method='default', buf_size=self.winSize, hop_size=self.hopSize, samplerate=self.audioInputSampleRate) - signal = np.frombuffer(in_data, dtype=np.float32) + self.inputStream = None + self._control_thread = None - beat = tempoDetection(signal) - if beat: - bpm = tempoDetection.get_bpm() - print(f"beat! (running with {bpm:.2f} bpm)") # Use f-string for cleaner formatting, removed extra bells - data_to_send = { - "names": ["1"], - "settings": { - "pattern": "pulse", - "delay": 10, - "colors": ["#00ff00"], - "brightness": 10, - "num_leds": 200, - }, - } + self._connect_to_midi_server() + self._start_control_server() # Start control server in background - if ws: # Only send if the websocket connection is established + def reset_tempo_detection(self): + """Re-initializes the aubio tempo detection object.""" + logging.info("[SoundBeatDetector] Resetting tempo detection.") + self.tempoDetection = aubio.tempo(method='default', buf_size=self.winSize, hop_size=self.hopSize, samplerate=self.audioInputSampleRate) + + def _control_server_loop(self): + srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + srv.bind((SOUND_CONTROL_HOST, SOUND_CONTROL_PORT)) + srv.listen(5) + logging.info(f"[SoundBeatDetector - Control] Listening on {SOUND_CONTROL_HOST}:{SOUND_CONTROL_PORT}") + while True: + conn, addr = srv.accept() + with conn: + logging.info(f"[SoundBeatDetector - Control] Connection from {addr}") + try: + data = conn.recv(1024) + if not data: + continue + command = data.decode().strip() + logging.debug(f"[SoundBeatDetector - Control] Received command: {command}") + if command == "RESET_TEMPO": + self.reset_tempo_detection() + response = "OK: Tempo reset\n" + else: + response = "ERROR: Unknown command\n" + conn.sendall(response.encode('utf-8')) + except Exception as e: + logging.error(f"[SoundBeatDetector - Control] Error handling control message: {e}") + except Exception as e: + logging.error(f"[SoundBeatDetector - Control] Server error: {e}") + finally: try: - ws.send(json.dumps(data_to_send)) - # print("Sent data over WebSocket") # Optional: for debugging - except websocket.WebSocketConnectionClosedException: - print("WebSocket connection closed, attempting to reconnect...") - ws = None # Mark as closed, connection will need to be re-established if desired - except Exception as e: - print(f"Error sending over WebSocket: {e}") + srv.close() + except Exception: + pass - return (in_data, pyaudio.paContinue) + def _start_control_server(self): + if self._control_thread and self._control_thread.is_alive(): + return + self._control_thread = threading.Thread(target=self._control_server_loop, daemon=True) + self._control_thread.start() + def _connect_to_midi_server(self): + if self.tcp_socket: + self.tcp_socket.close() + self.tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.tcp_socket.settimeout(self.reconnect_delay) + try: + logging.info(f"[SoundBeatDetector] Attempting to connect to MIDI TCP server at {self.tcp_host}:{self.tcp_port}...") + self.tcp_socket.connect((self.tcp_host, self.tcp_port)) + self.tcp_socket.setblocking(0) + self.connected_to_midi = True + logging.info(f"[SoundBeatDetector] Successfully connected to MIDI TCP server.") + except (socket.error, socket.timeout) as e: + logging.error(f"[SoundBeatDetector] Failed to connect to MIDI TCP server: {e}") + self.connected_to_midi = False -# create and start the input stream -try: - inputStream = pa.open(format=pyaudio.paFloat32, - input=True, - channels=audioInputChannels, - input_device_index=audioInputDeviceIndex, - frames_per_buffer=bufferSize, - rate=audioInputSampleRate, - stream_callback=readAudioFrames) + # Removed _handle_control_client and _control_server (replaced by simple threaded server) - inputStream.start_stream() - print("\nAudio stream started. Detecting beats. Press Ctrl+C to stop.") + def readAudioFrames(self, in_data, frame_count, time_info, status): + signal = np.frombuffer(in_data, dtype=np.float32) - # Loop to keep the script running, allowing graceful shutdown - while inputStream.is_active(): - sleep(0.1) # Small delay to prevent busy-waiting + beat = self.tempoDetection(signal) + if beat: + bpm = self.tempoDetection.get_bpm() + logging.debug(f"beat! (running with {bpm:.2f} bpm)") # Changed to debug + bpm_message = str(bpm) -except KeyboardInterrupt: - print("\nKeyboardInterrupt: Stopping script gracefully.") -except Exception as e: - print(f"An error occurred with the audio stream: {e}") -finally: - # Ensure streams and resources are closed - if 'inputStream' in locals() and inputStream.is_active(): - inputStream.stop_stream() - if 'inputStream' in locals() and not inputStream.is_stopped(): - inputStream.close() - pa.terminate() - if ws: - print("Closing WebSocket connection.") - ws.close() + if self.connected_to_midi and self.tcp_socket: + try: + message_bytes = (bpm_message + "\n").encode('utf-8') + self.tcp_socket.sendall(message_bytes) + logging.debug(f"[SoundBeatDetector] Sent BPM to MIDI TCP server: {bpm_message}") # Changed to debug + except socket.error as e: + logging.error(f"[SoundBeatDetector] Error sending BPM to MIDI TCP server: {e}. Attempting to reconnect...") + self.connected_to_midi = False + self._connect_to_midi_server() + elif not self.connected_to_midi: + logging.warning("[SoundBeatDetector] Not connected to MIDI TCP server, attempting to reconnect...") # Changed to warning + self._connect_to_midi_server() + else: + logging.warning("[SoundBeatDetector] TCP socket not initialized, cannot send BPM.") # Changed to warning -print("Script finished.") \ No newline at end of file + return (in_data, pyaudio.paContinue) + + def start_stream(self): + try: + self.inputStream = self.pa.open(format=pyaudio.paFloat32, + input=True, + channels=self.audioInputChannels, + input_device_index=self.audioInputDeviceIndex, + frames_per_buffer=self.bufferSize, + rate=self.audioInputSampleRate, + stream_callback=self.readAudioFrames) + + self.inputStream.start_stream() + logging.info("\nAudio stream started. Detecting beats. Press Ctrl+C to stop.") + + while self.inputStream.is_active(): + sleep(0.1) + + except KeyboardInterrupt: + logging.info("\nKeyboardInterrupt: Stopping script gracefully.") + except Exception as e: + logging.error(f"An error occurred with the audio stream: {e}") + finally: + self.stop_stream() + + def stop_stream(self): + if self.inputStream and self.inputStream.is_active(): + self.inputStream.stop_stream() + if self.inputStream and not self.inputStream.is_stopped(): + self.inputStream.close() + self.pa.terminate() + if self.tcp_socket and self.connected_to_midi: + logging.info("[SoundBeatDetector] Closing TCP socket.") + self.tcp_socket.close() + self.connected_to_midi = False + logging.info("SoundBeatDetector stopped.") + + # Removed async def run(self) + +if __name__ == "__main__": + # TCP Server Configuration (should match midi.py) + MIDI_TCP_HOST = "127.0.0.1" + MIDI_TCP_PORT = 65432 + + sound_detector = SoundBeatDetector(MIDI_TCP_HOST, MIDI_TCP_PORT) + logging.info("Starting SoundBeatDetector...") + try: + sound_detector.start_stream() + except KeyboardInterrupt: + logging.info("\nProgram interrupted by user.") + except Exception as e: + logging.error(f"An error occurred during main execution: {e}") \ No newline at end of file