Skip to content

Commit 6ef2c97

Browse files
committed
feat: Add microphone selection UI and persist user choice for audio input.
1 parent 8f8648f commit 6ef2c97

File tree

6 files changed

+146
-16
lines changed

6 files changed

+146
-16
lines changed

application/src/index.html

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,34 @@ <h2 class="modal-title">⚠️ Permissões Necessárias</h2>
167167
</div>
168168
</div>
169169

170-
<!-- New Model Selection Section -->
170+
<!-- Audio Settings Section (Model + Microphone) -->
171171
<div class="controls-suite" style="margin-top: 12px;">
172-
<div class="control-group full-width">
173-
<label class="control-label">Modelo de Transcrição</label>
174-
172+
<!-- Microphone Selection -->
173+
<div class="control-group" style="flex: 1;">
174+
<label class="control-label">Microfone</label>
175+
<div class="model-selector-container">
176+
<select id="microphone-select" class="model-select">
177+
<option value="" disabled selected>Carregando...</option>
178+
</select>
179+
</div>
180+
</div>
181+
182+
<div class="divider-vertical"></div>
183+
184+
<!-- Model Selection -->
185+
<div class="control-group" style="flex: 1;">
186+
<label class="control-label">Modelo</label>
175187
<div class="model-selector-container">
176188
<select id="model-select" class="model-select">
177-
<option value="" disabled selected>Carregando modelos...</option>
189+
<option value="" disabled selected>Carregando...</option>
178190
</select>
179191
</div>
192+
</div>
193+
</div>
180194

181-
<!-- Model Info & Action -->
195+
<!-- Model Info Panel (separate row) -->
196+
<div class="controls-suite" style="margin-top: 8px;">
197+
<div class="control-group full-width">
182198
<div id="model-info-panel" class="model-info-panel hidden">
183199
<p id="model-description" class="model-description">Descrição do modelo...</p>
184200

application/src/main.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,22 @@ ipcMain.handle("set-pre-heat-microphone", async (event, enabled) => {
167167
}
168168
});
169169

170+
ipcMain.handle("get-microphone-config", () => {
171+
return store.get("selectedMicrophone", null);
172+
});
173+
174+
ipcMain.handle("set-microphone", async (event, deviceId) => {
175+
try {
176+
CONFIG.selectedMicrophone = deviceId;
177+
store.set("selectedMicrophone", deviceId);
178+
log.info(`Storage: Microphone set to: ${deviceId}`);
179+
return true;
180+
} catch (e) {
181+
log.error("Failed to set microphone:", e);
182+
return false;
183+
}
184+
});
185+
170186
ipcMain.on("audio-level", (event, level) => {
171187
const overlayWindow = getOverlayWindow();
172188
if (overlayWindow && !overlayWindow.isDestroyed()) {

application/src/main/core.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export function loadConfig() {
7272
model: store.get("model", "tiny"),
7373
audioDevice: store.get("audioDevice", "default"),
7474
preHeatMicrophone: store.get("preHeatMicrophone"),
75+
selectedMicrophone: store.get("selectedMicrophone", null),
7576
audioFile: path.join(app.getPath("temp"), "recording.wav"),
7677
audio: {
7778
rate: 16000,

application/src/preload.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ contextBridge.exposeInMainWorld("api", {
2424
setPreHeatMicrophone: (enabled) =>
2525
ipcRenderer.invoke("set-pre-heat-microphone", enabled),
2626

27+
// Microphone selection
28+
getMicrophoneConfig: () => ipcRenderer.invoke("get-microphone-config"),
29+
setMicrophone: (deviceId) => ipcRenderer.invoke("set-microphone", deviceId),
30+
2731
// Permission management
2832
checkPermissions: () => ipcRenderer.invoke("check-permissions"),
2933
openSettings: (pane) => ipcRenderer.invoke("open-settings", pane),

application/src/renderer.js

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
startAudioRecording,
1010
stopAudioRecording,
1111
setLogCallback,
12+
getAvailableMicrophones,
1213
} from "./renderer/audio.js";
1314

1415
import {
@@ -54,6 +55,7 @@ const stepMic = document.getElementById("step-mic");
5455
const stepAccessibility = document.getElementById("step-accessibility");
5556
const autoLaunchToggle = document.getElementById("auto-launch-toggle");
5657
const preHeatToggle = document.getElementById("pre-heat-toggle");
58+
const microphoneSelect = document.getElementById("microphone-select");
5759
const modelSelect = document.getElementById("model-select");
5860
const modelInfoPanel = document.getElementById("model-info-panel");
5961
const modelDescription = document.getElementById("model-description");
@@ -102,9 +104,12 @@ async function init() {
102104
);
103105

104106
if (permissions.microphone && config.preHeatMicrophone !== false) {
105-
await warmUpMicrophone();
107+
await warmUpMicrophone(config.selectedMicrophone);
106108
}
107109

110+
// Load microphones list
111+
await loadMicrophones(config.selectedMicrophone);
112+
108113
currentHotkey = config.hotkey;
109114
updateHotkeyDisplay(
110115
currentHotkey,
@@ -150,7 +155,8 @@ async function init() {
150155
if (success) {
151156
if (enabled) {
152157
log("🔥 Pré-aquecimento ativado. Ligando microfone...");
153-
await warmUpMicrophone();
158+
const savedMic = await window.api.getMicrophoneConfig();
159+
await warmUpMicrophone(savedMic);
154160
} else {
155161
log("❄️ Pré-aquecimento desativado. Microfone desligado.");
156162
await stopMicrophone();
@@ -162,6 +168,27 @@ async function init() {
162168
});
163169
}
164170

171+
// Microphone selection handler
172+
if (microphoneSelect) {
173+
microphoneSelect.addEventListener("change", async (e) => {
174+
const deviceId = e.target.value;
175+
const success = await window.api.setMicrophone(deviceId);
176+
177+
if (success) {
178+
const selectedLabel = e.target.selectedOptions[0]?.text || deviceId;
179+
log(`🎤 Microfone alterado: ${selectedLabel}`);
180+
181+
// Restart microphone if pre-heat is enabled
182+
if (preHeatToggle && preHeatToggle.checked) {
183+
await stopMicrophone();
184+
await warmUpMicrophone(deviceId);
185+
}
186+
} else {
187+
log("❌ Falha ao salvar microfone", "error");
188+
}
189+
});
190+
}
191+
165192
// Models
166193
await loadModels(config.model);
167194

@@ -200,6 +227,50 @@ async function init() {
200227
}
201228
}
202229

230+
// ============================================================================
231+
// MICROPHONES
232+
// ============================================================================
233+
234+
async function loadMicrophones(savedDeviceId = null) {
235+
if (!microphoneSelect) return;
236+
237+
try {
238+
const mics = await getAvailableMicrophones();
239+
240+
microphoneSelect.innerHTML = "";
241+
242+
if (mics.length === 0) {
243+
const option = document.createElement("option");
244+
option.value = "";
245+
option.textContent = "Nenhum microfone encontrado";
246+
option.disabled = true;
247+
microphoneSelect.appendChild(option);
248+
return;
249+
}
250+
251+
mics.forEach((mic) => {
252+
const option = document.createElement("option");
253+
option.value = mic.deviceId;
254+
option.textContent = mic.label;
255+
256+
if (savedDeviceId && mic.deviceId === savedDeviceId) {
257+
option.selected = true;
258+
}
259+
260+
microphoneSelect.appendChild(option);
261+
});
262+
263+
// If no saved preference, select first available
264+
if (!savedDeviceId && mics.length > 0) {
265+
microphoneSelect.value = mics[0].deviceId;
266+
}
267+
268+
log(`🎤 ${mics.length} microfone(s) encontrado(s)`);
269+
} catch (err) {
270+
log("❌ Erro ao carregar microfones: " + err.message, "error");
271+
}
272+
}
273+
203274
// ============================================================================
204275
// MODELS
205276
// ============================================================================

application/src/renderer/audio.js

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,20 @@ function log(msg, type = null) {
3535
// MICROPHONE WARMUP (Pre-heat for instant recording)
3636
// ============================================================================
3737

38-
export async function warmUpMicrophone() {
38+
/**
39+
* Get list of available audio input devices
40+
*/
41+
export async function getAvailableMicrophones() {
42+
const devices = await navigator.mediaDevices.enumerateDevices();
43+
return devices
44+
.filter((d) => d.kind === "audioinput")
45+
.map((d) => ({
46+
deviceId: d.deviceId,
47+
label: d.label || `Microfone ${d.deviceId.slice(0, 8)}`,
48+
}));
49+
}
50+
51+
export async function warmUpMicrophone(preferredDeviceId = null) {
3952
if (
4053
audioContext &&
4154
audioContext.state === "running" &&
@@ -49,14 +62,23 @@ export async function warmUpMicrophone() {
4962
const devices = await navigator.mediaDevices.enumerateDevices();
5063
const audioInputs = devices.filter((d) => d.kind === "audioinput");
5164

52-
let selectedDeviceId = "default";
53-
const specificMic = audioInputs.find(
54-
(d) => d.deviceId !== "default" && d.deviceId !== "communications"
55-
);
65+
let selectedDeviceId = preferredDeviceId || "default";
5666

57-
if (specificMic) {
58-
selectedDeviceId = specificMic.deviceId;
59-
log(`🎯 Microfone selecionado: ${specificMic.label}`);
67+
// If no preference, find a specific mic (not default/communications)
68+
if (!preferredDeviceId) {
69+
const specificMic = audioInputs.find(
70+
(d) => d.deviceId !== "default" && d.deviceId !== "communications"
71+
);
72+
if (specificMic) {
73+
selectedDeviceId = specificMic.deviceId;
74+
}
75+
}
76+
77+
const selectedMic = audioInputs.find(
78+
(d) => d.deviceId === selectedDeviceId
79+
);
80+
if (selectedMic) {
81+
log(`🎯 Microfone selecionado: ${selectedMic.label}`);
6082
}
6183

6284
warmStream = await navigator.mediaDevices.getUserMedia({

0 commit comments

Comments
 (0)