diff --git a/Cargo.lock b/Cargo.lock
index b62e6694d7..2ca2f4c146 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -966,6 +966,7 @@ dependencies = [
"bytes",
"cidre",
"cpal",
+ "dasp",
"data",
"ebur128",
"futures-channel",
diff --git a/apps/desktop/src/components/editor-area/note-header/listen-button.tsx b/apps/desktop/src/components/editor-area/note-header/listen-button.tsx
index 4c69f5fede..987a7194dd 100644
--- a/apps/desktop/src/components/editor-area/note-header/listen-button.tsx
+++ b/apps/desktop/src/components/editor-area/note-header/listen-button.tsx
@@ -1,6 +1,16 @@
import { Trans } from "@lingui/react/macro";
import { useMutation, useQuery } from "@tanstack/react-query";
-import { MicIcon, MicOffIcon, PauseIcon, PlayIcon, StopCircleIcon, Volume2Icon, VolumeOffIcon } from "lucide-react";
+import {
+ CheckIcon,
+ ChevronDownIcon,
+ MicIcon,
+ MicOffIcon,
+ PauseIcon,
+ PlayIcon,
+ StopCircleIcon,
+ Volume2Icon,
+ VolumeOffIcon,
+} from "lucide-react";
import { useEffect, useState } from "react";
import SoundIndicator from "@/components/sound-indicator";
@@ -319,16 +329,14 @@ function RecordingControls({
return (
<>
-
-
+ toggleMicMuted.mutate()}
- type="mic"
+ onToggleMuted={() => toggleMicMuted.mutate()}
/>
- toggleSpeakerMuted.mutate()}
- type="speaker"
/>
@@ -377,35 +385,147 @@ function RecordingControls({
);
}
-function AudioControlButton({
- type,
+function MicrophoneSelector({
+ isMuted,
+ onToggleMuted,
+ disabled,
+}: {
+ isMuted?: boolean;
+ onToggleMuted: () => void;
+ disabled?: boolean;
+}) {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const allDevicesQuery = useQuery({
+ queryKey: ["microphone", "devices"],
+ queryFn: () => listenerCommands.listMicrophoneDevices(),
+ });
+
+ const currentDeviceQuery = useQuery({
+ queryKey: ["microphone", "current-device"],
+ queryFn: () => listenerCommands.getCurrentMicrophoneDevice(),
+ });
+
+ const handleSelectDevice = (device: string) => {
+ listenerCommands.setMicrophoneDevice(device).then(() => {
+ currentDeviceQuery.refetch();
+ });
+ };
+
+ useEffect(() => {
+ console.log("currentDeviceQuery.data", currentDeviceQuery.data);
+ console.log("allDevicesQuery.data", allDevicesQuery.data);
+ }, [currentDeviceQuery.data, allDevicesQuery.data]);
+
+ const Icon = isMuted ? MicOffIcon : MicIcon;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Microphone
+
+
+ {allDevicesQuery.isLoading
+ ? (
+
+ )
+ : allDevicesQuery.data?.length === 0
+ ? (
+
+ )
+ : (
+
+ {allDevicesQuery.data?.map((device) => {
+ const isSelected = device === currentDeviceQuery.data;
+ return (
+
+ );
+ })}
+
+ )}
+
+
+
+
+ );
+}
+
+function SpeakerButton({
isMuted,
onClick,
disabled,
}: {
- type: "mic" | "speaker";
isMuted?: boolean;
onClick: () => void;
disabled?: boolean;
}) {
- const Icon = type === "mic"
- ? isMuted
- ? MicOffIcon
- : MicIcon
- : isMuted
- ? VolumeOffIcon
- : Volume2Icon;
+ const Icon = isMuted ? VolumeOffIcon : Volume2Icon;
return (
-
+
+
+
);
}
diff --git a/apps/desktop/src/locales/en/messages.po b/apps/desktop/src/locales/en/messages.po
index 8cf92a8a76..03b55fa809 100644
--- a/apps/desktop/src/locales/en/messages.po
+++ b/apps/desktop/src/locales/en/messages.po
@@ -256,8 +256,8 @@ msgstr "(Beta) Upcoming meeting notifications"
#. placeholder {0}: disabled ? "Wait..." : isHovered ? "Resume" : "Ended"
#: src/components/settings/views/templates.tsx:194
#: src/components/settings/components/wer-modal.tsx:116
-#: src/components/editor-area/note-header/listen-button.tsx:179
-#: src/components/editor-area/note-header/listen-button.tsx:218
+#: src/components/editor-area/note-header/listen-button.tsx:189
+#: src/components/editor-area/note-header/listen-button.tsx:228
msgid "{0}"
msgstr "{0}"
@@ -887,7 +887,7 @@ msgstr "No speech-to-text models available or failed to load."
#~ msgid "No Template"
#~ msgstr "No Template"
-#: src/components/editor-area/note-header/listen-button.tsx:342
+#: src/components/editor-area/note-header/listen-button.tsx:350
msgid "No Template (Default)"
msgstr "No Template (Default)"
@@ -955,7 +955,7 @@ msgstr "Optional for participant suggestions"
msgid "Owner"
msgstr "Owner"
-#: src/components/editor-area/note-header/listen-button.tsx:365
+#: src/components/editor-area/note-header/listen-button.tsx:373
msgid "Pause"
msgstr "Pause"
@@ -967,7 +967,7 @@ msgstr "people"
msgid "Performance difference between languages"
msgstr "Performance difference between languages"
-#: src/components/editor-area/note-header/listen-button.tsx:198
+#: src/components/editor-area/note-header/listen-button.tsx:208
msgid "Play video"
msgstr "Play video"
@@ -1011,7 +1011,7 @@ msgstr "Required to transcribe other people's voice during meetings"
msgid "Required to transcribe your voice during meetings"
msgstr "Required to transcribe your voice during meetings"
-#: src/components/editor-area/note-header/listen-button.tsx:107
+#: src/components/editor-area/note-header/listen-button.tsx:117
msgid "Resume"
msgstr "Resume"
@@ -1109,11 +1109,11 @@ msgstr "Start Annual Plan"
msgid "Start Monthly Plan"
msgstr "Start Monthly Plan"
-#: src/components/editor-area/note-header/listen-button.tsx:154
+#: src/components/editor-area/note-header/listen-button.tsx:164
msgid "Start recording"
msgstr "Start recording"
-#: src/components/editor-area/note-header/listen-button.tsx:373
+#: src/components/editor-area/note-header/listen-button.tsx:381
msgid "Stop"
msgstr "Stop"
diff --git a/apps/desktop/src/locales/ko/messages.po b/apps/desktop/src/locales/ko/messages.po
index 8f91e752cd..b6cfb26f95 100644
--- a/apps/desktop/src/locales/ko/messages.po
+++ b/apps/desktop/src/locales/ko/messages.po
@@ -256,8 +256,8 @@ msgstr ""
#. placeholder {0}: disabled ? "Wait..." : isHovered ? "Resume" : "Ended"
#: src/components/settings/views/templates.tsx:194
#: src/components/settings/components/wer-modal.tsx:116
-#: src/components/editor-area/note-header/listen-button.tsx:179
-#: src/components/editor-area/note-header/listen-button.tsx:218
+#: src/components/editor-area/note-header/listen-button.tsx:189
+#: src/components/editor-area/note-header/listen-button.tsx:228
msgid "{0}"
msgstr ""
@@ -887,7 +887,7 @@ msgstr ""
#~ msgid "No Template"
#~ msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:342
+#: src/components/editor-area/note-header/listen-button.tsx:350
msgid "No Template (Default)"
msgstr ""
@@ -955,7 +955,7 @@ msgstr ""
msgid "Owner"
msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:365
+#: src/components/editor-area/note-header/listen-button.tsx:373
msgid "Pause"
msgstr ""
@@ -967,7 +967,7 @@ msgstr ""
msgid "Performance difference between languages"
msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:198
+#: src/components/editor-area/note-header/listen-button.tsx:208
msgid "Play video"
msgstr ""
@@ -1011,7 +1011,7 @@ msgstr ""
msgid "Required to transcribe your voice during meetings"
msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:107
+#: src/components/editor-area/note-header/listen-button.tsx:117
msgid "Resume"
msgstr ""
@@ -1109,11 +1109,11 @@ msgstr ""
msgid "Start Monthly Plan"
msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:154
+#: src/components/editor-area/note-header/listen-button.tsx:164
msgid "Start recording"
msgstr ""
-#: src/components/editor-area/note-header/listen-button.tsx:373
+#: src/components/editor-area/note-header/listen-button.tsx:381
msgid "Stop"
msgstr ""
diff --git a/apps/desktop/src/utils/broadcast.ts b/apps/desktop/src/utils/broadcast.ts
index 7a7a48eff2..794f2f6d96 100644
--- a/apps/desktop/src/utils/broadcast.ts
+++ b/apps/desktop/src/utils/broadcast.ts
@@ -41,12 +41,6 @@ export function broadcastQueryClient(queryClient: QueryClient) {
const keys = event.payload.queryKey as string[];
- if (keys.some((key) => key?.includes("extension"))) {
- queryClient.invalidateQueries({
- predicate: (query) => query.queryKey.some((key) => typeof key === "string" && key.includes("extension")),
- });
- }
-
if (keys.some((key) => key?.includes("flags"))) {
queryClient.invalidateQueries({
predicate: (query) => query.queryKey.some((key) => typeof key === "string" && key.includes("flags")),
diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml
index bfe6a8e05f..abde9554ef 100644
--- a/crates/audio/Cargo.toml
+++ b/crates/audio/Cargo.toml
@@ -17,6 +17,7 @@ futures-util = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros"] }
cpal = { workspace = true }
+dasp = { workspace = true }
rodio = { workspace = true }
ebur128 = "0.1.10"
diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs
index 5f7aeec1f3..0acdb80192 100644
--- a/crates/audio/src/lib.rs
+++ b/crates/audio/src/lib.rs
@@ -11,6 +11,7 @@ pub use speaker::*;
pub use stream::*;
pub use cpal;
+use cpal::traits::{DeviceTrait, HostTrait};
use futures_util::Stream;
pub use kalosm_sound::AsyncSource;
@@ -70,6 +71,23 @@ pub struct AudioInput {
}
impl AudioInput {
+ pub fn get_default_mic_device_name() -> String {
+ let host = cpal::default_host();
+ let device = host.default_input_device().unwrap();
+ device.name().unwrap().to_string()
+ }
+
+ pub fn list_mic_devices() -> Vec {
+ let host = cpal::default_host();
+
+ let devices = host.input_devices().unwrap();
+
+ devices
+ .filter_map(|d| d.name().ok())
+ .filter(|d| d != "hypr-audio-tap")
+ .collect()
+ }
+
pub fn from_mic() -> Self {
Self {
source: AudioSource::RealtimeMic,
@@ -79,6 +97,15 @@ impl AudioInput {
}
}
+ pub fn from_mic_with_device_name(device_name: String) -> Self {
+ Self {
+ source: AudioSource::RealtimeMic,
+ mic: Some(MicInput::from_device(device_name)),
+ speaker: None,
+ data: None,
+ }
+ }
+
pub fn from_speaker(sample_rate_override: Option) -> Self {
Self {
source: AudioSource::RealtimeSpeaker,
@@ -97,6 +124,14 @@ impl AudioInput {
}
}
+ pub fn device_name(&self) -> String {
+ match &self.source {
+ AudioSource::RealtimeMic => self.mic.as_ref().unwrap().device_name(),
+ AudioSource::RealtimeSpeaker => "TODO".to_string(),
+ AudioSource::Recorded => "TODO".to_string(),
+ }
+ }
+
pub fn stream(&mut self) -> AudioStream {
match &self.source {
AudioSource::RealtimeMic => AudioStream::RealtimeMic {
diff --git a/crates/audio/src/mic.rs b/crates/audio/src/mic.rs
index f32164b1ac..9d478e4bb9 100644
--- a/crates/audio/src/mic.rs
+++ b/crates/audio/src/mic.rs
@@ -1,4 +1,186 @@
-pub use kalosm_sound::{MicInput, MicStream};
+use cpal::{
+ traits::{DeviceTrait, HostTrait, StreamTrait},
+ SizedSample,
+};
+use dasp::sample::ToSample;
+use futures_channel::mpsc;
+use futures_util::{Stream, StreamExt};
+use std::pin::Pin;
+
+use crate::AsyncSource;
+
+pub struct MicInput {
+ #[allow(dead_code)]
+ host: cpal::Host,
+ device: cpal::Device,
+ config: cpal::SupportedStreamConfig,
+}
+
+impl Default for MicInput {
+ fn default() -> Self {
+ let host = cpal::default_host();
+ let device = host
+ .default_input_device()
+ .expect("Failed to get default input device");
+ let config = device
+ .default_input_config()
+ .expect("Failed to get default input config");
+
+ Self {
+ host,
+ device,
+ config,
+ }
+ }
+}
+
+impl MicInput {
+ pub fn device_name(&self) -> String {
+ self.device.name().expect("Failed to get input device name")
+ }
+
+ pub fn from_device(device_name: impl AsRef) -> Self {
+ let host = cpal::default_host();
+ let device = host
+ .input_devices()
+ .expect("Failed to get input devices")
+ .find(|d| d.name().expect("Failed to get input device name") == device_name.as_ref())
+ .expect("Failed to get input device");
+ let config = device
+ .default_input_config()
+ .expect("Failed to get default input config");
+
+ Self {
+ host,
+ device,
+ config,
+ }
+ }
+}
+
+impl MicInput {
+ pub fn stream(&self) -> MicStream {
+ let (tx, rx) = mpsc::unbounded::>();
+
+ let config = self.config.clone();
+ let device = self.device.clone();
+ let (drop_tx, drop_rx) = std::sync::mpsc::channel();
+
+ std::thread::spawn(move || {
+ fn build_stream + SizedSample>(
+ device: &cpal::Device,
+ config: &cpal::SupportedStreamConfig,
+ mut tx: mpsc::UnboundedSender>,
+ ) -> Result {
+ let channels = config.channels() as usize;
+ device.build_input_stream::(
+ &config.config(),
+ move |data: &[S], _input_callback_info: &_| {
+ let _ = tx.start_send(
+ data.iter()
+ .step_by(channels)
+ .map(|&x| x.to_sample())
+ .collect(),
+ );
+ },
+ |err| {
+ tracing::error!("an error occurred on stream: {}", err);
+ },
+ None,
+ )
+ }
+
+ let start_stream = || {
+ let stream = match config.sample_format() {
+ cpal::SampleFormat::I8 => build_stream::(&device, &config, tx),
+ cpal::SampleFormat::I16 => build_stream::(&device, &config, tx),
+ cpal::SampleFormat::I32 => build_stream::(&device, &config, tx),
+ cpal::SampleFormat::F32 => build_stream::(&device, &config, tx),
+ sample_format => {
+ tracing::error!("Unsupported sample format '{sample_format}'");
+ return None;
+ }
+ };
+
+ let stream = match stream {
+ Ok(stream) => stream,
+ Err(err) => {
+ tracing::error!("Error starting stream: {}", err);
+ return None;
+ }
+ };
+
+ if let Err(err) = stream.play() {
+ tracing::error!("Error playing stream: {}", err);
+ }
+
+ Some(stream)
+ };
+
+ let stream = match start_stream() {
+ Some(stream) => stream,
+ None => {
+ return;
+ }
+ };
+
+ // Wait for the stream to be dropped
+ drop_rx.recv().unwrap();
+
+ // Then drop the stream
+ drop(stream);
+ });
+
+ let receiver = rx.map(futures_util::stream::iter).flatten();
+ MicStream {
+ drop_tx,
+ config: self.config.clone(),
+ receiver: Box::pin(receiver),
+ read_data: Vec::new(),
+ }
+ }
+}
+
+pub struct MicStream {
+ drop_tx: std::sync::mpsc::Sender<()>,
+ config: cpal::SupportedStreamConfig,
+ read_data: Vec,
+ receiver: Pin + Send + Sync>>,
+}
+
+impl Drop for MicStream {
+ fn drop(&mut self) {
+ self.drop_tx.send(()).unwrap();
+ }
+}
+
+impl Stream for MicStream {
+ type Item = f32;
+
+ fn poll_next(
+ mut self: std::pin::Pin<&mut Self>,
+ cx: &mut std::task::Context<'_>,
+ ) -> std::task::Poll