const { app, BrowserWindow, ipcMain, Tray, Menu, dialog } = require('electron'); const path = require('path'); const os = require('os'); const spawn = require('child_process').spawn; require('electron-reload')(__dirname); const fs = require('fs').promises; const chokidar = require('chokidar'); const wavefile = require('wavefile'); const MetadataManager = require('./metatadata'); const { webContents } = require('electron'); // import { app, BrowserWindow, ipcMain, Tray, Menu, dialog } from "electron"; // import path from "path"; // import os from "os"; // import spawn from 'child_process'; // import fs from "fs"; // import chokidar from "chokidar"; // import wavefile from "wavefile"; // import MetadataManager from "./metatadata.cjs"; // import { webContents } from "electron"; let mainWindow; let tray; let audioServiceProcess; const metadataPath = path.join(app.getPath('userData'), 'audio_metadata.json'); const metadataManager = new MetadataManager(metadataPath); async function createPythonService() { const pythonPath = process.platform === 'win32' ? path.join( __dirname, '..', '..', 'audio-service', 'venv', 'Scripts', 'python.exe', ) : path.join(__dirname, '..', 'audio-service', 'venv', 'bin', 'python'); const scriptPath = path.join( __dirname, '..', '..', 'audio-service', 'src', 'main.py', ); // Load settings to pass as arguments const settings = await loadSettings(); // Assuming you have a method to load settings const args = [ scriptPath, '--recording-length', settings.recordingLength.toString(), '--save-path', path.join(settings.outputFolder, 'original'), '--osc-port', settings.oscPort.toString(), // Or make this configurable ]; // Add input device if specified if (settings.inputDevice) { const devices = await listAudioDevices(); args.push( '--input-device', devices.find((device) => device.id === settings.inputDevice)?.name, ); } console.log(args); audioServiceProcess = spawn(pythonPath, args, { detached: false, stdio: 'pipe', }); audioServiceProcess.stdout.on('data', (data) => { console.log(`Audio Service: ${data}`); }); audioServiceProcess.stderr.on('data', (data) => { console.error(`Audio Service Error: ${data}`); }); audioServiceProcess.on('close', (code) => { console.log(`Audio Service process exited with code ${code}`); audioServiceProcess = null; }); } function createTray() { tray = new Tray(path.join(__dirname, 'assets', 'tray-icon.png')); // You'll need to create this icon const contextMenu = Menu.buildFromTemplate([ { label: 'Show', click: () => { mainWindow.show(); }, }, { label: 'Quit', click: () => { // Properly terminate the Python service stopService(); app.quit(); }, }, ]); tray.setToolTip('Audio Trimmer'); tray.setContextMenu(contextMenu); } async function checkNewWavFile(filePath) { // Only process .wav files if (path.extname(filePath).toLowerCase() === '.wav') { try { await metadataManager.addUntrimmedFile(filePath); // Notify renderer if window is ready if (mainWindow) { mainWindow.webContents.send('new-untrimmed-file', filePath); } } catch (error) { console.error('Error adding untrimmed file:', error); } } } function stopService() { if (audioServiceProcess) { try { if (process.platform === 'win32') { spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']); } else { audioServiceProcess.kill('SIGTERM'); } } catch (error) { console.error('Error killing audio service:', error); } } } function restartService() { // Properly terminate the Python service stopService(); //delay for 2 seconds setTimeout(createPythonService, 4000); //createPythonService(); } async function loadSettings() { try { const settingsPath = path.join(app.getPath('userData'), 'settings.json'); const settingsData = await fs.readFile(settingsPath, 'utf8'); return JSON.parse(settingsData); } catch (error) { // If no settings file exists, return default settings return { recordingLength: 30, outputFolder: path.join(os.homedir(), 'AudioTrimmer'), inputDevice: null, }; } } async function listAudioDevices() { try { // Use a webContents to access navigator.mediaDevices const contents = webContents.getAllWebContents()[0]; const devices = await contents.executeJavaScript(` navigator.mediaDevices.enumerateDevices() .then(devices => devices.filter(device => device.kind === 'audioinput')) .then(audioDevices => audioDevices.map(device => ({ id: device.deviceId, name: device.label || 'Unknown Microphone' }))) `); return devices; } catch (error) { console.error('Error getting input devices:', error); return []; } } async function createWindow() { // Initialize metadata await metadataManager.initialize(); mainWindow = new BrowserWindow({ width: 1200, height: 800, autoHideMenuBar: true, titleBarStyle: 'hidden', frame: false, // titleBarOverlay: { // color: '#1e1e1e', // symbolColor: '#ffffff', // height: 30 // }, webPreferences: { nodeIntegration: true, contextIsolation: false, // Add these to help with graphics issues }, // These additional options can help with graphics rendering backgroundColor: '#1e1e1e', ...(process.platform !== 'darwin' ? { titleBarOverlay: { color: '#262626', symbolColor: '#ffffff', height: 30, }, } : {}), }); mainWindow.loadFile('src/index.html'); // Create Python ser const settings = await loadSettings(); // Assuming you have a method to load settings const recordingsPath = path.join(settings.outputFolder, 'original'); // Ensure recordings directory exists try { await fs.mkdir(recordingsPath, { recursive: true }); } catch (error) { console.error('Error creating recordings directory:', error); } // Watch for new WAV files const watcher = chokidar.watch(recordingsPath, { ignored: /(^|[\/\\])\../, // ignore dotfiles persistent: true, depth: 0, awaitWriteFinish: { stabilityThreshold: 2000, pollInterval: 100, }, }); fs.readdir(recordingsPath).then((files) => { files.forEach((file) => { checkNewWavFile(path.join(recordingsPath, file)); }); }); watcher.on('add', async (filePath) => { await checkNewWavFile(filePath); }); ipcMain.handle('get-collections', () => { return metadataManager.getCollections(); }); ipcMain.handle('get-collection-files', (event, collectionPath) => { return metadataManager.getFilesInCollection(collectionPath); }); ipcMain.handle('add-untrimmed-file', (event, filePath) => { return metadataManager.addUntrimmedFile(filePath); }); ipcMain.handle( 'save-trimmed-file', (event, fileName, previousPath, savePath, trimStart, trimEnd, title) => { return metadataManager.saveTrimmedFile( fileName, previousPath, savePath, trimStart, trimEnd, title, ); }, ); ipcMain.handle('restart', (event) => { restartService(); }); ipcMain.handle('delete-old-file', (event, outputFolder, section, title) => { if (section === 'untrimmed') return; const collectionPath = path.join(outputFolder, section); const outputFilePath = path.join(collectionPath, `${title}.wav`); fs.unlink(outputFilePath); }); ipcMain.handle( 'save-trimmed-audio', async ( event, { originalFilePath, outputFolder, collectionName, title, trimStart, trimEnd, }, ) => { try { // Ensure the collection folder exists const collectionPath = path.join(outputFolder, collectionName); await fs.mkdir(collectionPath, { recursive: true }); // Generate output file path const outputFilePath = path.join(collectionPath, `${title}.wav`); // Read the original WAV file const originalWaveFile = new wavefile.WaveFile( await fs.readFile(originalFilePath), ); // Calculate trim points in samples const sampleRate = originalWaveFile.fmt.sampleRate; const startSample = Math.floor(trimStart * sampleRate); const endSample = Math.floor(trimEnd * sampleRate); // Extract trimmed audio samples const originalSamples = originalWaveFile.getSamples(false); const trimmedSamples = [ originalSamples[0].slice(startSample, endSample), originalSamples[1].slice(startSample, endSample), ]; // Normalize samples if they are Int16 or Int32 let normalizedSamples; const bitDepth = originalWaveFile.fmt.bitsPerSample; // if (bitDepth === 16) { // // For 16-bit audio, convert to Float32 // normalizedSamples = [new Float32Array(trimmedSamples[0].length),new Float32Array(trimmedSamples[0].length)]; // for (let i = 0; i < trimmedSamples[0].length; i++) { // normalizedSamples[0][i] = trimmedSamples[0][i] / 32768.0; // normalizedSamples[1][i] = trimmedSamples[1][i] / 32768.0; // } // } else if (bitDepth === 32) { // // For 32-bit float audio, just convert to Float32 // normalizedSamples = [new Float32Array(trimmedSamples[0]),new Float32Array(trimmedSamples[1])]; // } else { // throw new Error(`Unsupported bit depth: ${bitDepth}`); // } // Create a new WaveFile with normalized samples const trimmedWaveFile = new wavefile.WaveFile(); trimmedWaveFile.fromScratch( originalWaveFile.fmt.numChannels, sampleRate, bitDepth, // Always use 32-bit float trimmedSamples, ); // Write the trimmed WAV file await fs.writeFile(outputFilePath, trimmedWaveFile.toBuffer()); return { success: true, filePath: outputFilePath, }; } catch (error) { console.error('Error saving trimmed audio:', error); return { success: false, error: error.message, }; } }, ); ipcMain.handle('delete-file', async (event, filePath) => { try { const settings = await loadSettings(); return metadataManager.deletefile(filePath, settings.outputFolder); } catch (error) { console.error('Error Deleting file:', error); throw error; } }); ipcMain.handle('add-new-collection', (event, collectionName) => { try { return metadataManager.addNewCollection(collectionName); } catch (error) { console.error('Error adding collection:', error); throw error; } }); ipcMain.handle('get-trim-info', (event, collectionName, filePath) => { return metadataManager.getTrimInfo(collectionName, filePath); }); ipcMain.handle( 'set-trim-info', (event, collectionName, filePath, trim_info) => { return metadataManager.setTrimInfo(collectionName, filePath, trim_info); }, ); // Add these IPC handlers ipcMain.handle('select-output-folder', async (event) => { const result = await dialog.showOpenDialog({ properties: ['openDirectory'], }); return result.filePaths[0] || ''; }); ipcMain.handle('get-default-settings', () => { return { recordingLength: 30, outputFolder: path.join(os.homedir(), 'AudioTrimmer'), inputDevice: null, }; }); ipcMain.handle('save-settings', async (event, settings) => { try { // Ensure output folder exists await fs.mkdir(settings.outputFolder, { recursive: true }); // Save settings to a file const settingsPath = path.join(app.getPath('userData'), 'settings.json'); await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2)); restartService(); return true; } catch (error) { console.error('Error saving settings:', error); return false; } }); ipcMain.handle('load-settings', async () => { return loadSettings(); }); ipcMain.handle('get-input-devices', async () => { return await listAudioDevices(); }); // Minimize to tray instead of closing mainWindow.on('close', (event) => { event.preventDefault(); mainWindow.hide(); }); // Create system tray createTray(); // Launch Python audio service createPythonService(); } app.disableHardwareAcceleration(); app.whenReady().then(createWindow); app.on('window-all-closed', () => { // Do nothing - we handle closing via tray }); // Ensure Python service is killed when app quits app.on('before-quit', () => { if (audioServiceProcess) { try { if (process.platform === 'win32') { spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']); } else { audioServiceProcess.kill('SIGTERM'); } } catch (error) { console.error('Error killing audio service:', error); } } }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } });