Skip to content

Commit edc3586

Browse files
committed
feat: Add pre-heat microphone option with UI toggle and persistent configuration.
1 parent bee9ee4 commit edc3586

File tree

4 files changed

+169
-41
lines changed

4 files changed

+169
-41
lines changed

application/src/index.html

Lines changed: 67 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -266,37 +266,74 @@ <h2 class="modal-title">⚠️ Permissões Necessárias</h2>
266266
<span>Powered by <strong>Ciro Cesar Maciel</strong></span>
267267
</a>
268268

269-
<!-- Auto-Launch Toggle (compact with icon) -->
270-
<label class="auto-launch-switch" title="Iniciar com o sistema">
271-
<input type="checkbox" id="auto-launch-toggle" />
272-
<span class="switch-track">
273-
<span class="switch-thumb">
274-
<svg
275-
class="icon-off"
276-
viewBox="0 0 24 24"
277-
width="10"
278-
height="10"
279-
fill="none"
280-
stroke="currentColor"
281-
stroke-width="2.5"
282-
>
283-
<path d="M18.36 6.64a9 9 0 1 1-12.73 0" />
284-
<line x1="12" y1="2" x2="12" y2="12" />
285-
</svg>
286-
<svg
287-
class="icon-on"
288-
viewBox="0 0 24 24"
289-
width="10"
290-
height="10"
291-
fill="none"
292-
stroke="currentColor"
293-
stroke-width="2.5"
294-
>
295-
<polyline points="20 6 9 17 4 12" />
296-
</svg>
269+
<div style="display: flex; gap: 16px; align-items: center;">
270+
<!-- Pre-heat Toggle -->
271+
<label class="auto-launch-switch" title="Microfone sempre ouvindo (mais rápido)">
272+
<input type="checkbox" id="pre-heat-toggle" />
273+
<span class="switch-track">
274+
<span class="switch-thumb">
275+
<svg
276+
class="icon-off"
277+
viewBox="0 0 24 24"
278+
width="10"
279+
height="10"
280+
fill="none"
281+
stroke="currentColor"
282+
stroke-width="2.5"
283+
>
284+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path>
285+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
286+
<line x1="12" y1="19" x2="12" y2="22"></line>
287+
</svg>
288+
<svg
289+
class="icon-on"
290+
viewBox="0 0 24 24"
291+
width="10"
292+
height="10"
293+
fill="none"
294+
stroke="currentColor"
295+
stroke-width="2.5"
296+
>
297+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"></path>
298+
<path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
299+
<line x1="12" y1="19" x2="12" y2="22"></line>
300+
</svg>
301+
</span>
302+
</span>
303+
</label>
304+
305+
<!-- Auto-Launch Toggle (compact with icon) -->
306+
<label class="auto-launch-switch" title="Iniciar com o sistema">
307+
<input type="checkbox" id="auto-launch-toggle" />
308+
<span class="switch-track">
309+
<span class="switch-thumb">
310+
<svg
311+
class="icon-off"
312+
viewBox="0 0 24 24"
313+
width="10"
314+
height="10"
315+
fill="none"
316+
stroke="currentColor"
317+
stroke-width="2.5"
318+
>
319+
<path d="M18.36 6.64a9 9 0 1 1-12.73 0" />
320+
<line x1="12" y1="2" x2="12" y2="12" />
321+
</svg>
322+
<svg
323+
class="icon-on"
324+
viewBox="0 0 24 24"
325+
width="10"
326+
height="10"
327+
fill="none"
328+
stroke="currentColor"
329+
stroke-width="2.5"
330+
>
331+
<polyline points="20 6 9 17 4 12" />
332+
</svg>
333+
</span>
297334
</span>
298-
</span>
299-
</label>
335+
</label>
336+
</div>
300337
</div>
301338
</div>
302339

application/src/main.js

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ function loadConfig() {
7171
if (userConfig.model) store.set("model", userConfig.model);
7272
if (userConfig.audioDevice)
7373
store.set("audioDevice", userConfig.audioDevice);
74+
if (userConfig.preHeatMicrophone !== undefined)
75+
store.set("preHeatMicrophone", userConfig.preHeatMicrophone);
7476

7577
store.set("migrated_from_json", true);
7678
log.info("Migration from config.json complete");
@@ -80,9 +82,14 @@ function loadConfig() {
8082
}
8183
}
8284

83-
return {
85+
// Ensure defaults exist in store
86+
if (!store.has("preHeatMicrophone")) {
87+
store.set("preHeatMicrophone", true);
88+
}
89+
90+
const config = {
8491
hotkey: store.get("hotkey", "CommandOrControl+Shift+Space"),
85-
triggerMode: store.get("triggerMode", "hybrid"), // hybrid, toggle, hold
92+
triggerMode: store.get("triggerMode", "hybrid"),
8693
language: store.get("language", "pt"),
8794
prompt: store.get(
8895
"prompt",
@@ -91,13 +98,19 @@ function loadConfig() {
9198
autoPaste: store.get("autoPaste", true),
9299
model: store.get("model", "tiny"),
93100
audioDevice: store.get("audioDevice", "default"),
101+
preHeatMicrophone: store.get("preHeatMicrophone"), // Verified to exist above
94102
audioFile: path.join(app.getPath("temp"), "recording.wav"),
95103
audio: {
96104
rate: 16000,
97105
channels: 1,
98106
bits: 16,
99107
},
100108
};
109+
110+
console.log("MAIN: Store Path:", store.path);
111+
console.log("MAIN: Loaded Config from Store:", config);
112+
log.info("Loaded Config:", config);
113+
return config;
101114
}
102115

103116
// Will be initialized after app is ready
@@ -961,12 +974,7 @@ function registerHotkey() {
961974
// ============================================================================
962975

963976
ipcMain.handle("get-config", () => {
964-
return {
965-
hotkey: CONFIG.hotkey,
966-
triggerMode: CONFIG.triggerMode,
967-
autoPaste: CONFIG.autoPaste,
968-
language: CONFIG.language,
969-
};
977+
return CONFIG;
970978
});
971979

972980
// Handle audio data from renderer and transcribe
@@ -1058,6 +1066,19 @@ ipcMain.handle("set-trigger-mode", async (event, mode) => {
10581066
}
10591067
});
10601068

1069+
// Set pre-heat microphone
1070+
ipcMain.handle("set-pre-heat-microphone", async (event, enabled) => {
1071+
try {
1072+
CONFIG.preHeatMicrophone = enabled;
1073+
store.set("preHeatMicrophone", enabled);
1074+
log.info(`Storage: Pre-heat Microphone set to: ${enabled}`);
1075+
return true;
1076+
} catch (e) {
1077+
log.error("Failed to set pre-heat microphone:", e);
1078+
return false;
1079+
}
1080+
});
1081+
10611082
// Audio level forwarding to overlay
10621083
ipcMain.on("audio-level", (event, level) => {
10631084
if (overlayWindow && !overlayWindow.isDestroyed()) {

application/src/preload.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ contextBridge.exposeInMainWorld("api", {
2121
setRecordingHotkey: (isRecording) =>
2222
ipcRenderer.invoke("set-recording-hotkey", isRecording),
2323
setTriggerMode: (mode) => ipcRenderer.invoke("set-trigger-mode", mode),
24+
setPreHeatMicrophone: (enabled) =>
25+
ipcRenderer.invoke("set-pre-heat-microphone", enabled),
2426

2527
// Permission management
2628
checkPermissions: () => ipcRenderer.invoke("check-permissions"),

application/src/renderer.js

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const btnCheckPermissions = document.getElementById("btn-check-permissions");
2828
const stepMic = document.getElementById("step-mic");
2929
const stepAccessibility = document.getElementById("step-accessibility");
3030
const autoLaunchToggle = document.getElementById("auto-launch-toggle");
31+
const preHeatToggle = document.getElementById("pre-heat-toggle");
3132
const modelSelect = document.getElementById("model-select");
3233
const modelInfoPanel = document.getElementById("model-info-panel");
3334
const modelDescription = document.getElementById("model-description");
@@ -73,14 +74,17 @@ function getModelDescription(name) {
7374
async function init() {
7475
const loadingScreen = document.getElementById("loading-screen");
7576

77+
const config = await window.api.getConfig();
78+
console.log("Renderer: Received config:", config);
79+
console.log("Renderer: preHeatMicrophone =", config.preHeatMicrophone);
80+
7681
const permissions = await checkAndShowPermissions();
7782

78-
// Warm up microphone for instant recording (if permissions granted)
79-
if (permissions.microphone) {
83+
// Warm up microphone for instant recording (if permissions granted AND enabled)
84+
if (permissions.microphone && config.preHeatMicrophone !== false) {
8085
await warmUpMicrophone();
8186
}
8287

83-
const config = await window.api.getConfig();
8488
currentHotkey = config.hotkey;
8589
updateHotkeyDisplay(config.hotkey);
8690

@@ -116,6 +120,32 @@ async function init() {
116120
});
117121
}
118122

123+
// Initialize Pre-Heat Toggle
124+
if (preHeatToggle) {
125+
// Default to true if undefined, matching main process default
126+
const preHeatEnabled = config.preHeatMicrophone !== false;
127+
preHeatToggle.checked = preHeatEnabled;
128+
129+
preHeatToggle.addEventListener("change", async (e) => {
130+
const enabled = e.target.checked;
131+
const success = await window.api.setPreHeatMicrophone(enabled);
132+
133+
if (success) {
134+
if (enabled) {
135+
log("🔥 Pré-aquecimento ativado. Ligando microfone...");
136+
await warmUpMicrophone();
137+
} else {
138+
log("❄️ Pré-aquecimento desativado. Microfone desligado.");
139+
await stopMicrophone();
140+
}
141+
} else {
142+
// Revert on failure
143+
preHeatToggle.checked = !enabled;
144+
log("❌ Falha ao salvar configuração", "error");
145+
}
146+
});
147+
}
148+
119149
// Initialize Models
120150
await loadModels(config.model);
121151

@@ -491,6 +521,15 @@ let gainNode = null;
491521

492522
// Initialize microphone once at startup (warm it up)
493523
async function warmUpMicrophone() {
524+
if (
525+
audioContext &&
526+
audioContext.state === "running" &&
527+
warmStream &&
528+
warmStream.active
529+
) {
530+
return true; // Already warm
531+
}
532+
494533
try {
495534
// List devices first to pick a specific one (avoiding 'default' which can be buggy)
496535
const devices = await navigator.mediaDevices.enumerateDevices();
@@ -599,6 +638,28 @@ async function warmUpMicrophone() {
599638
}
600639
}
601640

641+
async function stopMicrophone() {
642+
try {
643+
if (warmStream) {
644+
warmStream.getTracks().forEach((track) => track.stop());
645+
warmStream = null;
646+
}
647+
648+
if (audioContext && audioContext.state !== "closed") {
649+
await audioContext.close();
650+
}
651+
// Re-create context on next warmup
652+
audioContext = null;
653+
mediaStreamSource = null;
654+
scriptProcessor = null;
655+
656+
return true;
657+
} catch (err) {
658+
console.error("Failed to stop microphone:", err);
659+
return false;
660+
}
661+
}
662+
602663
// Start recording - now instant because mic is already warm
603664
async function startAudioRecording() {
604665
// If not warmed up yet, do it now (fallback)
@@ -667,6 +728,13 @@ async function stopAudioRecording() {
667728
// Keep context warm for next recording (don't close it)
668729
audioBuffers = [];
669730

731+
// CP: Check config to see if we should cool down
732+
// We need to fetch fresh config or check toggle state
733+
// Since we are in renderer, we can check toggle directly
734+
if (preHeatToggle && !preHeatToggle.checked) {
735+
await stopMicrophone();
736+
}
737+
670738
resolve(wavBuffer);
671739
});
672740
}

0 commit comments

Comments
 (0)