5 Commits

Author SHA1 Message Date
michalcourson
1106c82eab fix device index floating 2026-03-28 07:11:31 -04:00
michalcourson
d3d5270889 closes #6 2026-03-03 17:47:45 -05:00
michalcourson
017a2ae5a4 multiline clip name 2026-03-01 17:47:59 -05:00
michalcourson
791abef1ef better auto focus for delete 2026-03-01 17:35:24 -05:00
michalcourson
31cc3079a8 kinda bad, but functional delete on enter 2026-03-01 17:31:14 -05:00
15 changed files with 304 additions and 50 deletions

View File

@@ -98,8 +98,8 @@ class SettingsManager:
recorder.recordings_dir = self.get_settings('save_path')
audio_manager = WindowsAudioManager()
audio_manager.set_default_input_device(self.get_settings('input_device')['index'])
audio_manager.set_default_output_device(self.get_settings('output_device')['index'])
audio_manager.set_default_input_device(self.get_settings('input_device'))
audio_manager.set_default_output_device(self.get_settings('output_device'))
recorder.refresh_streams()

View File

@@ -75,34 +75,46 @@ class WindowsAudioManager:
}
]
def set_default_input_device(self, device_index):
if(device_index is None):
def set_default_input_device(self, device):
if(device is None):
return 0
"""
Set the default input audio device.
:param device_index: Index of the audio device
:param device: Audio device information
:return: Sample rate of the selected device
"""
sd.default.device[0] = device_index
self.default_input = device_index
corrected_device = None
# set corrected device index based on the name of the device matching the provided device name
for dev in self.devices:
if dev['name'] == device['name']:
corrected_device = dev['index']
break
sd.default.device[0] = corrected_device
self.default_input = corrected_device
# Get the sample rate of the selected device
device_info = sd.query_devices(device_index)
device_info = sd.query_devices(corrected_device)
return device_info['default_samplerate']
def set_default_output_device(self, device_index):
if(device_index is None):
def set_default_output_device(self, device):
if(device is None):
return self.get_current_output_device_sample_rate()
"""
Set the default output audio device.
:param device_index: Index of the audio device
:param device: Audio device information
:return: Sample rate of the selected device
"""
sd.default.device[1] = device_index
self.default_output = device_index
corrected_device = None
# set corrected device index based on the name of the device matching the provided device name
for dev in self.devices:
if dev['name'] == device['name']:
corrected_device = dev['index']
break
sd.default.device[1] = corrected_device
self.default_output = corrected_device
# Get the sample rate of the selected device
device_info = sd.query_devices(device_index)
device_info = sd.query_devices(corrected_device)
return device_info['default_samplerate']

206
audio-service/test.json Normal file
View File

@@ -0,0 +1,206 @@
[
{
"name": "Speakers (Realtek(R) Audio)",
"index": 38,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.003,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "Headset Earphone (4- Arctis 7 Chat)",
"index": 39,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 1,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.003,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "Line (Voicemod Virtual Audio Device (WDM))",
"index": 40,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.003,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "Headphones (Oculus Virtual Audio Device)",
"index": 41,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.003,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "VM to Headset (VB-Audio Voicemeeter VAIO)",
"index": 42,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.002,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "VM to Discord (VB-Audio Voicemeeter VAIO)",
"index": 43,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.002,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "VM to OBS (VB-Audio Voicemeeter VAIO)",
"index": 44,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.002,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "Headphones (4- Arctis 7 Game)",
"index": 45,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.003,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "Speakers (2- Focusrite USB Audio)",
"index": 46,
"hostapi": 2,
"max_input_channels": 0,
"max_output_channels": 2,
"default_low_input_latency": 0.0,
"default_low_output_latency": 0.003,
"default_high_input_latency": 0.0,
"default_high_output_latency": 0.01,
"default_samplerate": 48000.0
},
{
"name": "Headset Microphone (4- Arctis 7 Chat)",
"index": 47,
"hostapi": 2,
"max_input_channels": 1,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
},
{
"name": "Analogue 1 + 2 (2- Focusrite USB Audio)",
"index": 48,
"hostapi": 2,
"max_input_channels": 2,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
},
{
"name": "Voicemeeter Out B3 (VB-Audio Voicemeeter VAIO)",
"index": 49,
"hostapi": 2,
"max_input_channels": 2,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
},
{
"name": "Microphone (Voicemod Virtual Audio Device (WDM))",
"index": 50,
"hostapi": 2,
"max_input_channels": 2,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
},
{
"name": "Voicemeeter Out A2 (VB-Audio Voicemeeter VAIO)",
"index": 51,
"hostapi": 2,
"max_input_channels": 2,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
},
{
"name": "VM Mic mix (VB-Audio Voicemeeter VAIO)",
"index": 52,
"hostapi": 2,
"max_input_channels": 2,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
},
{
"name": "VM Rec mix (VB-Audio Voicemeeter VAIO)",
"index": 53,
"hostapi": 2,
"max_input_channels": 2,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
},
{
"name": "Voicemeeter Out A1 (VB-Audio Voicemeeter VAIO)",
"index": 54,
"hostapi": 2,
"max_input_channels": 2,
"max_output_channels": 0,
"default_low_input_latency": 0.003,
"default_low_output_latency": 0.0,
"default_high_input_latency": 0.01,
"default_high_output_latency": 0.0,
"default_samplerate": 48000.0
}
]

View File

@@ -1,8 +1,8 @@
import { ipcMain } from 'electron';
import { dialog, ipcMain } from 'electron';
import fs from 'fs';
import AudioChannels from './channels';
import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types';
import PythonSubprocessManager from '../../main/service';
import PythonSubprocessManager from '../main/service';
export default function registerAudioIpcHandlers() {
ipcMain.handle(

View File

@@ -1,8 +0,0 @@
const SettingsChannels = {
GET_DEFAULTS: 'settings:get-defaults',
GET_SETTINGS: 'settings:get-settings',
SET_SETTINGS: 'settings:set-settings',
GET_INPUT_DEVICES: 'settings:get-input-devices',
} as const;
export default SettingsChannels;

View File

@@ -10,12 +10,20 @@
*/
import fs from 'fs';
import path from 'path';
import { app, BrowserWindow, shell, ipcMain, Tray, Menu } from 'electron';
import {
app,
BrowserWindow,
shell,
ipcMain,
Tray,
Menu,
dialog,
} from 'electron';
import { autoUpdater } from 'electron-updater';
import log from 'electron-log';
import MenuBuilder from './menu';
import { resolveHtmlPath } from './util';
import registerFileIpcHandlers from '../ipc/audio/main';
import registerFileIpcHandlers from '../ipc/main';
import PythonSubprocessManager from './service';
const pythonManager = new PythonSubprocessManager('src/main.py');
@@ -110,6 +118,21 @@ const createWindow = async () => {
menuBuilder.buildMenu();
registerFileIpcHandlers();
ipcMain.handle('select-directory', async () => {
try {
const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], // Key property to select a folder
});
if (!result.canceled && result.filePaths.length > 0) {
// Send the selected directory path back to the renderer process
return result.filePaths[0];
}
return null;
} catch (err: any) {
return { error: err.message };
}
});
// Open urls in the user's browser
mainWindow.webContents.setWindowOpenHandler((edata) => {
shell.openExternal(edata.url);
@@ -164,7 +187,7 @@ app
.whenReady()
.then(() => {
// if (app.isPackaged) {
// pythonManager.start();
pythonManager.start();
// }
createWindow();
app.on('activate', () => {

View File

@@ -1,8 +1,8 @@
// Disable no-unused-vars, broken for spread args
/* eslint no-unused-vars: off */
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import { LoadAudioBufferArgs } from '../ipc/audio/types';
import AudioChannels from '../ipc/audio/channels';
import { LoadAudioBufferArgs } from '../ipc/types';
import AudioChannels from '../ipc/channels';
// import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API
export type Channels = 'ipc-example';

View File

@@ -1,10 +1,13 @@
import React, { useEffect, useState } from 'react';
// import { ipcRenderer } from 'electron';
import { useNavigate } from 'react-router-dom';
import './App.css';
import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import { IconButton } from '@mui/material';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import { apiFetch } from './api';
type AudioDevice = {
@@ -123,18 +126,22 @@ export default function SettingsPage() {
};
const handleFolderChange = async () => {
// Replace with actual folder picker
// Example: const folder = await window.api.selectFolder();
// const folder = window.prompt(
// 'Enter output folder path:',
// settings.outputFolder,
// );
// if (folder !== null) {
// setSettings((prev) => ({
// ...prev,
// outputFolder: folder,
// }));
// }
await window.electron.ipcRenderer
.invoke('select-directory')
.then((result) => {
if (result) {
setSettings((prev) => ({
...prev,
save_path: result,
}));
sendSettingsToBackend({
...settings,
save_path: result,
});
}
return null;
});
return null;
};
return (
@@ -259,13 +266,22 @@ export default function SettingsPage() {
value={settings.save_path}
className="ml-2 w-[300px]"
/>
<button
<IconButton
component="label"
size="small"
tabIndex={-1}
onClick={handleFolderChange}
>
<MoreHorizIcon />
</IconButton>
{/* <button
type="button"
onClick={handleFolderChange}
className="ml-2 px-3 py-1 rounded bg-plumDark text-offwhite hover:bg-plum"
>
<VisuallyHiddenInput type="file" />
...
</button>
</button> */}
</div>
</div>
</div>

View File

@@ -35,6 +35,7 @@ export default function DeleteClipDialog({
<button
type="button"
onClick={onDelete}
autoFocus
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
>
Delete

View File

@@ -4,6 +4,7 @@ import {
DialogTitle,
DialogContent,
DialogActions,
TextField,
} from '@mui/material';
export default function NameEditDialog({
@@ -35,17 +36,17 @@ export default function NameEditDialog({
>
<DialogTitle>Edit Clip Name</DialogTitle>
<DialogContent>
<textarea
<TextField
autoFocus
multiline
variant="standard"
className="font-bold text-lg bg-transparent outline-none border-b border-plum focus:border-plumDark text-white mb-1 w-full text-center resize-y"
value={input}
onChange={(e) => {
setInput(e.target.value);
}}
rows={3}
onFocus={(event) => event.target.select()}
aria-label="Edit clip name"
style={{ minHeight: '3em' }}
/>
</DialogContent>
<DialogActions>

View File

@@ -265,6 +265,7 @@ namespace ClipTrimDotNet.Client
public async void SaveClip()
{
if (socket is null) return;
CheckPort();
await socket.EmitAsync("record_clip", new List<object>() { });
}
@@ -272,7 +273,7 @@ namespace ClipTrimDotNet.Client
{
if (socket is null) return;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Checking port {socket}");
if (currentHostname != HostName)
if (currentHostname != HostName || !socket.Connected)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"port {socket}");
if (socket.Connected)

View File

@@ -6,8 +6,6 @@
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<PreBuildEvent>npm run stop</PreBuildEvent>
<PostBuildEvent>npm run start</PostBuildEvent>
<AssemblyTitle>ClipTrimDotNet</AssemblyTitle>
<Product>ClipTrimDotNet</Product>
<Copyright>Copyright © 2020</Copyright>
@@ -18,9 +16,10 @@
<OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath>
<OutputPath>bin\Release\com.michal-courson.cliptrim.sdPlugin\</OutputPath>
</PropertyGroup>
<ItemGroup>
<None Remove="ClipTrim.streamDeckProfile" />
<None Remove="Images\app_icon.png" />
<None Remove="Images\back.png" />
<None Remove="Images\category_icon.png" />
@@ -71,6 +70,9 @@
</ItemGroup>
<ItemGroup>
<Content Include="!!README!!.txt" />
<Content Include="ClipTrim.streamDeckProfile">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Images\app_icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>