diff --git a/audio-service/settings.json b/audio-service/settings.json index 3e9bf0b..ed12104 100644 --- a/audio-service/settings.json +++ b/audio-service/settings.json @@ -1,16 +1,17 @@ { "input_device": { - "index": 49, - "name": "Microphone (Logi C615 HD WebCam)", - "max_input_channels": 1, - "default_samplerate": 48000.0 + "channels": 2, + "default_samplerate": 48000, + "index": 55, + "name": "VM Mic mix (VB-Audio Voicemeeter VAIO)" }, "save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings", "recording_length": 30, "output_device": { + "default_samplerate": 48000, "index": 40, - "name": "Speakers (Realtek(R) Audio)", "max_output_channels": 2, - "default_samplerate": 48000.0 - } + "name": "Speakers (Realtek(R) Audio)" + }, + "http_port": 5010 } \ No newline at end of file diff --git a/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc b/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc index e036ec8..d61d721 100644 Binary files a/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc and b/audio-service/src/__pycache__/audio_recorder.cpython-313.pyc differ diff --git a/audio-service/src/__pycache__/settings.cpython-313.pyc b/audio-service/src/__pycache__/settings.cpython-313.pyc index ace0d6d..04c508e 100644 Binary files a/audio-service/src/__pycache__/settings.cpython-313.pyc and b/audio-service/src/__pycache__/settings.cpython-313.pyc differ diff --git a/audio-service/src/__pycache__/windows_audio.cpython-313.pyc b/audio-service/src/__pycache__/windows_audio.cpython-313.pyc index dfd54e2..972b381 100644 Binary files a/audio-service/src/__pycache__/windows_audio.cpython-313.pyc and b/audio-service/src/__pycache__/windows_audio.cpython-313.pyc differ diff --git a/audio-service/src/audio_recorder.py b/audio-service/src/audio_recorder.py index 243271b..d66ed94 100644 --- a/audio-service/src/audio_recorder.py +++ b/audio-service/src/audio_recorder.py @@ -10,7 +10,7 @@ class AudioRecorder: def __new__(cls, *args, **kwargs): if cls._instance is None: - print("Creating new AudioRecorder instance") + # print("Creating new AudioRecorder instance") cls._instance = super().__new__(cls) cls._instance.init() return cls._instance @@ -22,7 +22,7 @@ class AudioRecorder: :param sample_rate: Audio sample rate (if None, use default device sample rate) :param channels: Number of audio channels """ - print(f"Initializing AudioRecorder") + # print(f"Initializing AudioRecorder") self.duration = 30 self.sample_rate = 44100 self.channels = 2 @@ -42,7 +42,7 @@ class AudioRecorder: self.stream.stop() self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32) - print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}") + # print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}") self.stream = sd.InputStream( callback=self.record_callback ) @@ -63,7 +63,8 @@ class AudioRecorder: :param status: Recording status """ if status: - print(f"Recording status: {status}") + # print(f"Recording status: {status}") + pass # Circular buffer implementation self.buffer = np.roll(self.buffer, -frames, axis=0) @@ -128,9 +129,9 @@ class AudioRecorder: Start continuous audio recording with circular buffer. """ if(self.stream.active): - print("Already recording") + # print("Already recording") return - print('number of channels', self.channels) + # print('number of channels', self.channels) self.stream.start() @@ -139,7 +140,7 @@ class AudioRecorder: Stop continuous audio recording with circular buffer. """ if(not self.stream.active): - print("Already stopped") + # print("Already stopped") return self.stream.stop() diff --git a/audio-service/src/main.py b/audio-service/src/main.py index 7774405..adc432d 100644 --- a/audio-service/src/main.py +++ b/audio-service/src/main.py @@ -46,23 +46,20 @@ def main(): app.register_blueprint(device_bp) app.register_blueprint(metadata_bp) app.register_blueprint(settings_bp) - app.run(host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True) + app.run(host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=True) # socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True) # Run the OSC server - try: - print(f"Starting OSC Recording Server on port {args.osc_port}") - - - - # osc_server.run_server() - except KeyboardInterrupt: - print("\nServer stopped by user.") - except Exception as e: - print(f"Error starting server: {e}") - sys.exit(1) + # try: + # print(f"Starting OSC Recording Server on port {args.osc_port}") + # # osc_server.run_server() + # except KeyboardInterrupt: + # print("\nServer stopped by user.") + # except Exception as e: + # print(f"Error starting server: {e}") + # sys.exit(1) if __name__ == "__main__": diff --git a/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc b/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc index 6fa02b0..9bf59f7 100644 Binary files a/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc and b/audio-service/src/routes/__pycache__/metadata.cpython-313.pyc differ diff --git a/audio-service/src/routes/__pycache__/recording.cpython-313.pyc b/audio-service/src/routes/__pycache__/recording.cpython-313.pyc index d45df46..7ed3f26 100644 Binary files a/audio-service/src/routes/__pycache__/recording.cpython-313.pyc and b/audio-service/src/routes/__pycache__/recording.cpython-313.pyc differ diff --git a/audio-service/src/routes/__pycache__/settings.cpython-313.pyc b/audio-service/src/routes/__pycache__/settings.cpython-313.pyc index bbb536e..8953471 100644 Binary files a/audio-service/src/routes/__pycache__/settings.cpython-313.pyc and b/audio-service/src/routes/__pycache__/settings.cpython-313.pyc differ diff --git a/audio-service/src/routes/metadata.py b/audio-service/src/routes/metadata.py index 8cbb5da..1987031 100644 --- a/audio-service/src/routes/metadata.py +++ b/audio-service/src/routes/metadata.py @@ -91,7 +91,7 @@ def edit_clip_in_collection(): meta_manager = MetaDataManager() collection_name = request.json.get('name') clip_metadata = request.json.get('clip') - print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}") + # print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}") try: meta_manager.edit_clip_in_collection(collection_name, clip_metadata) collections = meta_manager.collections diff --git a/audio-service/src/routes/recording.py b/audio-service/src/routes/recording.py index e5254b0..c9a907d 100644 --- a/audio-service/src/routes/recording.py +++ b/audio-service/src/routes/recording.py @@ -15,14 +15,14 @@ def start_recording(): @recording_bp.route('/record/stop', methods=['POST']) def stop_recording(): recorder = AudioRecorder() - print('HTTP: Stopping audio recording') + # print('HTTP: Stopping audio recording') recorder.stop_recording() return jsonify({'status': 'recording stopped'}) @recording_bp.route('/record/save', methods=['POST']) def save_recording(): recorder = AudioRecorder() - print('HTTP: Saving audio recording') + # print('HTTP: Saving audio recording') saved_file = recorder.save_last_n_seconds() return jsonify({'status': 'recording saved', 'file': saved_file}) @@ -30,7 +30,7 @@ def save_recording(): @recording_bp.route('/record/status', methods=['GET']) def recording_status(): recorder = AudioRecorder() - print('HTTP: Checking recording status') + # print('HTTP: Checking recording status') status = 'recording' if recorder.is_recording() else 'stopped' return jsonify({'status': status}) @@ -45,7 +45,7 @@ def recording_delete(): @recording_bp.route('/playback/start', methods=['POST']) def playback_start(): - print('HTTP: Starting audio playback') + # print('HTTP: Starting audio playback') try: # os.remove(filename) return jsonify({'status': 'success'}) diff --git a/audio-service/src/routes/settings.py b/audio-service/src/routes/settings.py index d012cec..0d153ab 100644 --- a/audio-service/src/routes/settings.py +++ b/audio-service/src/routes/settings.py @@ -16,10 +16,16 @@ def get_setting(name): else: return jsonify({'status': 'error', 'message': f'Setting "{name}" not found'}), 404 -@settings_bp.route('/settings/', methods=['POST']) -def set_setting(name): - value = request.json.get('value') - if value is None: - return jsonify({'status': 'error', 'message': 'Value is required'}), 400 - SettingsManager().set_settings(name, value) - return jsonify({'status': 'success', 'name': name, 'value': value}) \ No newline at end of file +@settings_bp.route('/settings/update', methods=['POST']) +def set_all_settings(): + settings = request.json.get('settings') + print (f"Received settings update: {settings}") + if settings is None: + return jsonify({'status': 'error', 'message': 'Settings are required'}), 400 + try: + for name, value in settings.items(): + print(f"Updating setting '{name}' to '{value}'") + SettingsManager().set_settings(name, value) + return jsonify({'status': 'success', 'settings': settings}) + except ValueError as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 \ No newline at end of file diff --git a/audio-service/src/settings.py b/audio-service/src/settings.py index 56672e4..f385c9d 100644 --- a/audio-service/src/settings.py +++ b/audio-service/src/settings.py @@ -13,6 +13,7 @@ class SettingsManager: return cls._instance def init(self): # read settings file from executing directory + print("Initializing SettingsManager", os.getcwd()) self.settings_file = os.path.join(os.getcwd(), "settings.json") if os.path.exists(self.settings_file): with open(self.settings_file, "r") as f: @@ -46,18 +47,23 @@ class SettingsManager: #see if input device is in "devices", if not set to the first index if input is not None and any(d['name'] == input["name"] for d in input_devices): - print(f"Using saved input device index: {input}") + # print(f"Using saved input device index: {input}") + pass else: input = input_devices[0] if input_devices else None self.settings["input_device"] = input #see if output device is in "devices", if not set to the first index if output is not None and any(d['name'] == output["name"] for d in output_devices): - print(f"Using saved output device index: {output}") + # print(f"Using saved output device index: {output}") + pass else: output = output_devices[0] if output_devices else None self.settings["output_device"] = output + if not "http_port" in self.settings: + self.settings["http_port"] = 5010 + self.save_settings() @@ -71,6 +77,8 @@ class SettingsManager: return self.settings def set_settings(self, name, value): + if(name not in self.settings): + raise ValueError(f"Setting '{name}' not found.") self.settings[name] = value self.save_settings() diff --git a/audio-service/src/windows_audio.py b/audio-service/src/windows_audio.py index e9a574a..652d779 100644 --- a/audio-service/src/windows_audio.py +++ b/audio-service/src/windows_audio.py @@ -25,11 +25,11 @@ class WindowsAudioManager: wasapi_device_indexes = api['devices'] break # print(f"Host APIs: {host_apis}") - print(f"WASAPI Device Indexes: {wasapi_device_indexes}") + # print(f"WASAPI Device Indexes: {wasapi_device_indexes}") wasapi_device_indexes = set(wasapi_device_indexes) if wasapi_device_indexes is not None else set() self.devices = [dev for dev in sd.query_devices() if dev['index'] in wasapi_device_indexes] # self.devices = sd.query_devices() - print(f"devices: {self.devices}") + # print(f"devices: {self.devices}") self.default_input = sd.default.device[0] self.default_output = sd.default.device[1] diff --git a/electron-ui/settings.json b/electron-ui/settings.json new file mode 100644 index 0000000..17e6c51 --- /dev/null +++ b/electron-ui/settings.json @@ -0,0 +1,16 @@ +{ + "input_device": { + "index": 49, + "name": "Microphone (Logi C615 HD WebCam)", + "channels": 1, + "default_samplerate": 48000.0 + }, + "output_device": { + "index": 40, + "name": "Speakers (Realtek(R) Audio)", + "channels": 2, + "default_samplerate": 48000.0 + }, + "save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\electron-ui\\recordings", + "recording_length": 15 +} \ No newline at end of file diff --git a/electron-ui/src/ipc/audio/channels.ts b/electron-ui/src/ipc/audio/channels.ts index ba3dda9..cceef3b 100644 --- a/electron-ui/src/ipc/audio/channels.ts +++ b/electron-ui/src/ipc/audio/channels.ts @@ -1,5 +1,7 @@ const AudioChannels = { LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer', + GET_PORT: 'audio:getPort', + RESTART_SERVICE: 'audio:restartService', } as const; export default AudioChannels; diff --git a/electron-ui/src/ipc/audio/main.ts b/electron-ui/src/ipc/audio/main.ts index caecc76..5def87a 100644 --- a/electron-ui/src/ipc/audio/main.ts +++ b/electron-ui/src/ipc/audio/main.ts @@ -2,6 +2,7 @@ import { ipcMain } from 'electron'; import fs from 'fs'; import AudioChannels from './channels'; import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types'; +import PythonSubprocessManager from '../../main/service'; export default function registerAudioIpcHandlers() { ipcMain.handle( @@ -15,4 +16,25 @@ export default function registerAudioIpcHandlers() { } }, ); + + ipcMain.handle(AudioChannels.GET_PORT, async () => { + try { + if (PythonSubprocessManager.instance?.portNumber) { + return { port: PythonSubprocessManager.instance.portNumber }; + } + + return { error: 'Port number not available yet.' }; + } catch (err: any) { + return { error: err.message }; + } + }); + + ipcMain.handle(AudioChannels.RESTART_SERVICE, async () => { + try { + PythonSubprocessManager.instance?.restart(); + return { success: true }; + } catch (err: any) { + return { success: false, error: err.message }; + } + }); } diff --git a/electron-ui/src/ipc/audio/types.ts b/electron-ui/src/ipc/audio/types.ts index c0cdd0a..49443dd 100644 --- a/electron-ui/src/ipc/audio/types.ts +++ b/electron-ui/src/ipc/audio/types.ts @@ -6,3 +6,22 @@ export interface LoadAudioBufferResult { buffer?: Buffer; error?: string; } + +export interface GetPortResult { + port?: number; + error?: string; +} + +export interface SetPortArgs { + port: number; +} + +export interface SetPortResult { + success: boolean; + error?: string; +} + +export interface RestartServiceResult { + success: boolean; + error?: string; +} diff --git a/electron-ui/src/main/main.ts b/electron-ui/src/main/main.ts index badfdee..caf9382 100644 --- a/electron-ui/src/main/main.ts +++ b/electron-ui/src/main/main.ts @@ -16,6 +16,7 @@ import log from 'electron-log'; import MenuBuilder from './menu'; import { resolveHtmlPath } from './util'; import registerFileIpcHandlers from '../ipc/audio/main'; +import PythonSubprocessManager from './service'; class AppUpdater { constructor() { @@ -110,6 +111,10 @@ const createWindow = async () => { }); registerFileIpcHandlers(); + + const pythonManager = new PythonSubprocessManager('src/main.py'); + + pythonManager.start(); // Remove this if your app does not use auto updates // eslint-disable-next-line new AppUpdater(); diff --git a/electron-ui/src/main/preload.ts b/electron-ui/src/main/preload.ts index 0b15825..1487c23 100644 --- a/electron-ui/src/main/preload.ts +++ b/electron-ui/src/main/preload.ts @@ -1,8 +1,7 @@ // Disable no-unused-vars, broken for spread args /* eslint no-unused-vars: off */ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; -import FileChannels from '../ipc/audio/channels'; -import { LoadAudioBufferArgs, ReadTextArgs } from '../ipc/audio/types'; +import { LoadAudioBufferArgs } from '../ipc/audio/types'; import AudioChannels from '../ipc/audio/channels'; // import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API @@ -41,10 +40,8 @@ const audioHandler = { filePath, } satisfies LoadAudioBufferArgs), - readText: (filePath: string) => - ipcRenderer.invoke(AudioChannels.READ_TEXT, { - filePath, - } satisfies ReadTextArgs), + getPort: () => ipcRenderer.invoke(AudioChannels.GET_PORT), + restartService: () => ipcRenderer.invoke(AudioChannels.RESTART_SERVICE), }; contextBridge.exposeInMainWorld('audio', audioHandler); diff --git a/electron-ui/src/main/service.ts b/electron-ui/src/main/service.ts index e69de29..5013be9 100644 --- a/electron-ui/src/main/service.ts +++ b/electron-ui/src/main/service.ts @@ -0,0 +1,79 @@ +import { spawn, ChildProcessWithoutNullStreams } from 'child_process'; +import path from 'path'; + +export default class PythonSubprocessManager { + // eslint-disable-next-line no-use-before-define + public static instance: PythonSubprocessManager | null = null; + + private process: ChildProcessWithoutNullStreams | null = null; + + private scriptPath: string; + + private working_dir: string = path.join( + __dirname, + '..', + '..', + '..', + 'audio-service', + ); + + public portNumber: number | null = null; + + constructor(scriptPath: string) { + this.scriptPath = scriptPath; + PythonSubprocessManager.instance = this; + } + + start(args: string[] = []): void { + if (this.process) { + throw new Error('Process already running.'); + } + console.log(`Using Python working directory at: ${this.working_dir}`); + console.log(`Starting Python subprocess with script: ${this.scriptPath}`); + this.process = spawn( + 'venv/Scripts/python.exe', + [this.scriptPath, ...args], + { + cwd: this.working_dir, + detached: false, + stdio: 'pipe', + }, + ); + this.process.stdout.on('data', (data: Buffer) => { + console.log(`Python stdout: ${data.toString()}`); + }); + this.process.stderr.on('data', (data: Buffer) => { + console.error(`Python stderr: ${data.toString()}`); + const lines = data.toString().split('\n'); + // eslint-disable-next-line no-restricted-syntax + for (const line of lines) { + const match = line.match(/Running on .*:(\d+)/); + if (match) { + const port = parseInt(match[1], 10); + console.log(`Detected port: ${port}`); + this.portNumber = port; + } + } + }); + this.process.on('exit', () => { + console.log('Python subprocess exited.'); + this.process = null; + }); + } + + stop(): void { + if (this.process) { + this.process.kill(); + this.process = null; + } + } + + restart(args: string[] = []): void { + this.stop(); + this.start(args); + } + + isHealthy(): boolean { + return !!this.process && !this.process.killed; + } +} diff --git a/electron-ui/src/renderer/App.tsx b/electron-ui/src/renderer/App.tsx index e3d8501..2106d9d 100644 --- a/electron-ui/src/renderer/App.tsx +++ b/electron-ui/src/renderer/App.tsx @@ -14,7 +14,7 @@ import { useAppDispatch, useAppSelector } from './hooks'; import { store } from '../redux/main'; import { useNavigate } from 'react-router-dom'; import SettingsPage from './Settings'; -import { apiFetch } from './api'; +import apiFetch from './api'; function MainPage() { const dispatch = useAppDispatch(); diff --git a/electron-ui/src/renderer/Settings.tsx b/electron-ui/src/renderer/Settings.tsx index 4f2cfec..748f60b 100644 --- a/electron-ui/src/renderer/Settings.tsx +++ b/electron-ui/src/renderer/Settings.tsx @@ -5,7 +5,7 @@ import './App.css'; import TextField from '@mui/material/TextField'; import Select from '@mui/material/Select'; import MenuItem from '@mui/material/MenuItem'; -import { apiFetch } from './api'; +import apiFetch from './api'; type AudioDevice = { index: number; @@ -57,10 +57,26 @@ async function fetchSettings(): Promise { }); } -const sendSettingsToBackend = (settings: Settings) => { +const sendSettingsToBackend = async (settings: Settings) => { // Replace with actual backend call // Example: window.api.updateSettings(settings); console.log('Settings updated:', settings); + await apiFetch('settings/update', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ settings }), + }) + .then((res) => res.json()) + .then((data) => { + console.log('Settings update response:', data); + if (data.status === 'success') { + window.audio.restartService(); + } + return data; + }) + .catch((error) => { + console.error('Error updating settings:', error); + }); }; export default function SettingsPage() { @@ -95,11 +111,10 @@ export default function SettingsPage() { }); }, []); - useEffect(() => { - sendSettingsToBackend(settings); - }, [settings]); + useEffect(() => {}, [settings]); const handleChange = () => { + sendSettingsToBackend(settings); // const { name, value } = e.target; // setSettings((prev) => ({ // ...prev, @@ -142,7 +157,7 @@ export default function SettingsPage() { type="text" name="httpPort" value={settings.http_port} - onBlur={() => console.log('port blur')} + onBlur={() => handleChange()} onChange={(e) => { if (!Number.isNaN(Number(e.target.value))) { setSettings((prev) => ({ @@ -168,8 +183,12 @@ export default function SettingsPage() { if (newDevice) { setSettings((prev) => ({ ...prev, - inputDevice: newDevice, + input_device: newDevice, })); + sendSettingsToBackend({ + ...settings, + input_device: newDevice, + }); } }} className="ml-2 w-64" @@ -196,6 +215,10 @@ export default function SettingsPage() { ...prev, output_device: newDevice, })); + sendSettingsToBackend({ + ...settings, + output_device: newDevice, + }); } }} className="ml-2 w-64" @@ -222,6 +245,7 @@ export default function SettingsPage() { })); } }} + onBlur={() => handleChange()} className="ml-2 w-[150px]" /> diff --git a/electron-ui/src/renderer/api.ts b/electron-ui/src/renderer/api.ts index a98a033..3b87142 100644 --- a/electron-ui/src/renderer/api.ts +++ b/electron-ui/src/renderer/api.ts @@ -1,13 +1,13 @@ -const getBaseUrl = () => { +const getBaseUrl = async () => { + const port = await window.audio.getPort(); + if (port.error || !port.port) { + return `http://localhost:5010`; + } // You can store the base URL in localStorage, a config file, or state - return localStorage.getItem('baseUrl') || 'http://localhost:5010'; + return `http://localhost:${port.port}`; }; -export function apiFetch(endpoint: string, options = {}) { - const url = `${getBaseUrl()}/${endpoint}`; +export default async function apiFetch(endpoint: string, options = {}) { + const url = `${await getBaseUrl()}/${endpoint}`; return fetch(url, options); } - -export function setBaseUrl(baseUrl: string) { - localStorage.setItem('baseUrl', baseUrl); -} diff --git a/electron-ui/src/renderer/components/ClipList.tsx b/electron-ui/src/renderer/components/ClipList.tsx index 7e450fd..80f1f0d 100644 --- a/electron-ui/src/renderer/components/ClipList.tsx +++ b/electron-ui/src/renderer/components/ClipList.tsx @@ -15,7 +15,7 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import AudioTrimmer from './AudioTrimer'; import { ClipMetadata } from '../../redux/types'; import { useAppDispatch, useAppSelector } from '../hooks'; -import { apiFetch } from '../api'; +import apiFetch from '../api'; export interface ClipListProps { collection: string; diff --git a/electron-ui/src/renderer/preload.d.ts b/electron-ui/src/renderer/preload.d.ts index 792d060..2f6ee95 100644 --- a/electron-ui/src/renderer/preload.d.ts +++ b/electron-ui/src/renderer/preload.d.ts @@ -1,10 +1,10 @@ -import { ElectronHandler, FileHandler } from '../main/preload'; +import { ElectronHandler, AudioHandler } from '../main/preload'; declare global { // eslint-disable-next-line no-unused-vars interface Window { electron: ElectronHandler; - audio: FileHandler; + audio: AudioHandler; } }