484 lines
13 KiB
JavaScript
484 lines
13 KiB
JavaScript
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();
|
|
}
|
|
});
|