mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-16 13:20:39 -07:00
Refactor AudioPlayer for improved native playback handling and debugging
- Introduced a stop flag mechanism to manage audio playback more effectively. - Enhanced native playback logic to ensure proper stopping of existing streams before starting new playback. - Updated error handling and logging for better visibility during playback operations. - Refactored audio output handling in Rust to support stopping playback and outputting silence when required. - Improved the integration of native playback with WaveSurfer for seamless audio visualization.
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { Pause, Play, Repeat, Volume2, VolumeX } from 'lucide-react';
|
||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import WaveSurfer from 'wavesurfer.js';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Slider } from '@/components/ui/slider';
|
||||
import { formatAudioDuration } from '@/lib/utils/audio';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { apiClient } from '@/lib/api/client';
|
||||
import { isTauri } from '@/lib/tauri';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { formatAudioDuration } from '@/lib/utils/audio';
|
||||
import { usePlayerStore } from '@/stores/playerStore';
|
||||
|
||||
export function AudioPlayer() {
|
||||
const {
|
||||
@@ -54,23 +54,21 @@ export function AudioPlayer() {
|
||||
profileChannels,
|
||||
channels,
|
||||
});
|
||||
|
||||
|
||||
if (!isTauri() || !profileChannels || !channels) {
|
||||
console.log('useNativePlayback: false - missing requirements');
|
||||
return false;
|
||||
}
|
||||
|
||||
const assignedChannels = channels.filter((ch) =>
|
||||
profileChannels.channel_ids.includes(ch.id),
|
||||
);
|
||||
|
||||
|
||||
const assignedChannels = channels.filter((ch) => profileChannels.channel_ids.includes(ch.id));
|
||||
|
||||
console.log('Assigned channels:', assignedChannels);
|
||||
|
||||
|
||||
// Use native playback if any assigned channel has non-default devices
|
||||
const shouldUseNative = assignedChannels.some(
|
||||
(ch) => ch.device_ids.length > 0 && !ch.is_default,
|
||||
);
|
||||
|
||||
|
||||
console.log('useNativePlayback result:', shouldUseNative);
|
||||
return shouldUseNative;
|
||||
}, [profileChannels, channels, profileId]);
|
||||
@@ -80,6 +78,7 @@ export function AudioPlayer() {
|
||||
const loadingRef = useRef(false);
|
||||
const previousAudioIdRef = useRef<string | null>(null);
|
||||
const hasInitializedRef = useRef(false);
|
||||
const isUsingNativePlaybackRef = useRef(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -186,8 +185,9 @@ export function AudioPlayer() {
|
||||
wavesurfer.setVolume(currentVolume);
|
||||
|
||||
// Get the underlying audio element and ensure it's not muted
|
||||
// (unless we're using native playback, which will be set later)
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
if (mediaElement && !isUsingNativePlaybackRef.current) {
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log('Audio element volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||
@@ -197,18 +197,18 @@ export function AudioPlayer() {
|
||||
// Get current values from the store and queries at runtime (not captured closure values)
|
||||
const currentAudioUrl = usePlayerStore.getState().audioUrl;
|
||||
const currentProfileId = usePlayerStore.getState().profileId;
|
||||
|
||||
|
||||
console.log('Auto-play check - capturing runtime values...');
|
||||
|
||||
|
||||
// Fetch profile channels at runtime (not using captured value)
|
||||
let runtimeProfileChannels = null;
|
||||
let runtimeChannels = null;
|
||||
|
||||
|
||||
if (isTauri() && currentProfileId) {
|
||||
try {
|
||||
runtimeProfileChannels = await apiClient.getProfileChannels(currentProfileId);
|
||||
console.log('Runtime profileChannels:', runtimeProfileChannels);
|
||||
|
||||
|
||||
if (runtimeProfileChannels && runtimeProfileChannels.channel_ids.length > 0) {
|
||||
runtimeChannels = await apiClient.listChannels();
|
||||
console.log('Runtime channels:', runtimeChannels);
|
||||
@@ -217,7 +217,7 @@ export function AudioPlayer() {
|
||||
console.error('Failed to fetch runtime channel data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log('Auto-play check:', {
|
||||
isTauri: isTauri(),
|
||||
currentAudioUrl,
|
||||
@@ -225,60 +225,125 @@ export function AudioPlayer() {
|
||||
hasProfileChannels: !!runtimeProfileChannels,
|
||||
hasChannels: !!runtimeChannels,
|
||||
});
|
||||
|
||||
if (isTauri() && currentAudioUrl && currentProfileId && runtimeProfileChannels && runtimeChannels) {
|
||||
|
||||
if (
|
||||
isTauri() &&
|
||||
currentAudioUrl &&
|
||||
currentProfileId &&
|
||||
runtimeProfileChannels &&
|
||||
runtimeChannels
|
||||
) {
|
||||
console.log('Attempting native audio playback...');
|
||||
|
||||
// Stop any existing native playback first
|
||||
if (isUsingNativePlaybackRef.current) {
|
||||
try {
|
||||
await invoke('stop_audio_playback');
|
||||
console.log('Stopped existing native playback before starting new one');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop existing playback:', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Collect all device IDs from assigned channels
|
||||
const assignedChannels = runtimeChannels.filter((ch: any) =>
|
||||
runtimeProfileChannels.channel_ids.includes(ch.id),
|
||||
);
|
||||
console.log('Assigned channels for playback:', assignedChannels);
|
||||
|
||||
|
||||
// Check if any assigned channel has non-default devices
|
||||
const shouldUseNative = assignedChannels.some(
|
||||
(ch: any) => ch.device_ids.length > 0 && !ch.is_default,
|
||||
);
|
||||
console.log('Should use native playback:', shouldUseNative);
|
||||
|
||||
|
||||
if (!shouldUseNative) {
|
||||
console.log('No custom devices assigned, falling back to WaveSurfer');
|
||||
// Reset native playback flag and unmute WaveSurfer
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log('WaveSurfer unmuted for normal playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||
}
|
||||
} else {
|
||||
const deviceIds = assignedChannels.flatMap((ch: any) => ch.device_ids);
|
||||
console.log('Device IDs to play to:', deviceIds);
|
||||
|
||||
if (deviceIds.length > 0) {
|
||||
console.log('Fetching audio data from:', currentAudioUrl);
|
||||
// Fetch audio data
|
||||
const response = await fetch(currentAudioUrl);
|
||||
const audioData = new Uint8Array(await response.arrayBuffer());
|
||||
console.log('Audio data size:', audioData.length);
|
||||
console.log('Fetching audio data from:', currentAudioUrl);
|
||||
// Fetch audio data
|
||||
const response = await fetch(currentAudioUrl);
|
||||
const audioData = new Uint8Array(await response.arrayBuffer());
|
||||
console.log('Audio data size:', audioData.length);
|
||||
|
||||
// Play via native audio
|
||||
console.log('Invoking play_audio_to_devices...');
|
||||
try {
|
||||
const result = await invoke('play_audio_to_devices', {
|
||||
audioData: Array.from(audioData),
|
||||
deviceIds: deviceIds,
|
||||
});
|
||||
console.log('play_audio_to_devices completed successfully, result:', result);
|
||||
setIsPlaying(true);
|
||||
console.log('Auto-playing via native audio routing - SUCCESS');
|
||||
return;
|
||||
} catch (invokeError) {
|
||||
console.error('play_audio_to_devices invoke failed:', invokeError);
|
||||
throw invokeError;
|
||||
}
|
||||
// Play via native audio
|
||||
console.log('Invoking play_audio_to_devices...');
|
||||
try {
|
||||
const result = await invoke('play_audio_to_devices', {
|
||||
audioData: Array.from(audioData),
|
||||
deviceIds: deviceIds,
|
||||
});
|
||||
console.log('play_audio_to_devices completed successfully, result:', result);
|
||||
|
||||
// Mark that we're using native playback
|
||||
isUsingNativePlaybackRef.current = true;
|
||||
|
||||
// Mute WaveSurfer's audio element to prevent UI audio output
|
||||
// Keep WaveSurfer running for visualization
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
mediaElement.volume = 0;
|
||||
mediaElement.muted = true;
|
||||
console.log('WaveSurfer muted for native playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||
}
|
||||
|
||||
// Start WaveSurfer playback for visualization (muted)
|
||||
wavesurfer.play().catch((error) => {
|
||||
console.error('Failed to start WaveSurfer visualization:', error);
|
||||
});
|
||||
|
||||
setIsPlaying(true);
|
||||
console.log('Auto-playing via native audio routing - SUCCESS');
|
||||
return;
|
||||
} catch (invokeError) {
|
||||
console.error('play_audio_to_devices invoke failed:', invokeError);
|
||||
throw invokeError;
|
||||
}
|
||||
} else {
|
||||
console.log('No device IDs found, falling back to WaveSurfer');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Native playback failed during auto-play, falling back to WaveSurfer:', error);
|
||||
console.error(
|
||||
'Native playback failed during auto-play, falling back to WaveSurfer:',
|
||||
error,
|
||||
);
|
||||
// Reset native playback flag and unmute WaveSurfer
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log('WaveSurfer unmuted after native playback failure - volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||
}
|
||||
// Fall through to WaveSurfer playback
|
||||
}
|
||||
} else {
|
||||
console.log('Not using native playback, using WaveSurfer');
|
||||
// Reset native playback flag and unmute WaveSurfer
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log('WaveSurfer unmuted for normal playback - volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||
}
|
||||
}
|
||||
|
||||
// Standard WaveSurfer auto-play
|
||||
@@ -294,13 +359,22 @@ export function AudioPlayer() {
|
||||
// Handle play/pause
|
||||
wavesurfer.on('play', () => {
|
||||
setIsPlaying(true);
|
||||
// Ensure audio element is not muted when playing
|
||||
// Ensure audio element volume is set correctly
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
mediaElement.muted = false;
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
console.log('Playing - volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||
// Double-check: if using native playback, keep WaveSurfer muted
|
||||
// Otherwise, ensure it's unmuted
|
||||
if (isUsingNativePlaybackRef.current) {
|
||||
mediaElement.volume = 0;
|
||||
mediaElement.muted = true;
|
||||
console.log('Playing (native mode) - WaveSurfer muted for visualization only');
|
||||
} else {
|
||||
// Ensure WaveSurfer is unmuted for normal playback
|
||||
const currentVolume = usePlayerStore.getState().volume;
|
||||
mediaElement.volume = currentVolume;
|
||||
mediaElement.muted = false;
|
||||
console.log('Playing (normal mode) - volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||
}
|
||||
}
|
||||
});
|
||||
wavesurfer.on('pause', () => setIsPlaying(false));
|
||||
@@ -406,10 +480,35 @@ export function AudioPlayer() {
|
||||
setDuration(0);
|
||||
setCurrentTime(0);
|
||||
setError(null);
|
||||
// Reset native playback flag
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop native playback if it was active
|
||||
if (isUsingNativePlaybackRef.current && isTauri()) {
|
||||
(async () => {
|
||||
try {
|
||||
await invoke('stop_audio_playback');
|
||||
console.log('Stopped native audio playback');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop native playback:', error);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
// Reset native playback flag when loading new audio
|
||||
// Also unmute WaveSurfer if it was muted
|
||||
if (isUsingNativePlaybackRef.current) {
|
||||
const mediaElement = wavesurfer.getMediaElement();
|
||||
if (mediaElement) {
|
||||
mediaElement.muted = false;
|
||||
mediaElement.volume = usePlayerStore.getState().volume;
|
||||
}
|
||||
}
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
|
||||
// CRITICAL: Force stop any current playback and cancel any pending loads
|
||||
// This must happen BEFORE any early returns
|
||||
console.log('Audio URL changed to:', audioUrl);
|
||||
@@ -490,9 +589,16 @@ export function AudioPlayer() {
|
||||
// Also ensure the underlying audio element volume is set
|
||||
const mediaElement = wavesurferRef.current.getMediaElement();
|
||||
if (mediaElement) {
|
||||
mediaElement.volume = volume;
|
||||
mediaElement.muted = volume === 0;
|
||||
console.log('Volume synced:', volume, 'muted:', mediaElement.muted);
|
||||
// If using native playback, keep WaveSurfer muted regardless of volume setting
|
||||
if (isUsingNativePlaybackRef.current) {
|
||||
mediaElement.volume = 0;
|
||||
mediaElement.muted = true;
|
||||
console.log('Volume sync: Using native playback, keeping WaveSurfer muted');
|
||||
} else {
|
||||
mediaElement.volume = volume;
|
||||
mediaElement.muted = volume === 0;
|
||||
console.log('Volume synced:', volume, 'muted:', mediaElement.muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [volume]);
|
||||
@@ -526,7 +632,7 @@ export function AudioPlayer() {
|
||||
setIsPlaying(false);
|
||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
});
|
||||
|
||||
|
||||
// Clear the restart flag
|
||||
clearRestartFlag();
|
||||
}, [shouldRestart, duration, setIsPlaying, clearRestartFlag]);
|
||||
@@ -534,16 +640,44 @@ export function AudioPlayer() {
|
||||
// Handle loop - WaveSurfer handles this via the 'finish' event
|
||||
|
||||
const handlePlayPause = async () => {
|
||||
// If using native playback, handle differently
|
||||
// Standard WaveSurfer playback (works for both normal and native playback modes)
|
||||
// When using native playback, WaveSurfer is muted but still controls visualization
|
||||
if (!wavesurferRef.current) {
|
||||
console.error('WaveSurfer not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if audio is loaded
|
||||
if (duration === 0 && !isLoading) {
|
||||
console.error('Audio not loaded yet');
|
||||
setError('Audio not loaded. Please wait...');
|
||||
return;
|
||||
}
|
||||
|
||||
// If using native playback
|
||||
if (useNativePlayback && audioUrl && profileChannels && channels) {
|
||||
if (isPlaying) {
|
||||
// For native playback, we'd need to track and stop streams
|
||||
// For now, just toggle the state
|
||||
setIsPlaying(false);
|
||||
// Pause: stop native playback and pause WaveSurfer visualization
|
||||
try {
|
||||
await invoke('stop_audio_playback');
|
||||
console.log('Stopped native audio playback');
|
||||
} catch (error) {
|
||||
console.error('Failed to stop native playback:', error);
|
||||
}
|
||||
wavesurferRef.current.pause();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Play: trigger native playback
|
||||
try {
|
||||
// Stop any existing native playback first
|
||||
try {
|
||||
await invoke('stop_audio_playback');
|
||||
} catch (error) {
|
||||
// Ignore errors when stopping (might not be playing)
|
||||
console.log('No existing playback to stop');
|
||||
}
|
||||
|
||||
// Collect all device IDs from assigned channels
|
||||
const assignedChannels = channels.filter((ch) =>
|
||||
profileChannels.channel_ids.includes(ch.id),
|
||||
@@ -561,31 +695,45 @@ export function AudioPlayer() {
|
||||
deviceIds: deviceIds,
|
||||
});
|
||||
|
||||
setIsPlaying(true);
|
||||
// Mark that we're using native playback
|
||||
isUsingNativePlaybackRef.current = true;
|
||||
|
||||
// Mute WaveSurfer and start it for visualization
|
||||
const mediaElement = wavesurferRef.current.getMediaElement();
|
||||
if (mediaElement) {
|
||||
mediaElement.volume = 0;
|
||||
mediaElement.muted = true;
|
||||
}
|
||||
|
||||
// Start WaveSurfer for visualization (muted)
|
||||
wavesurferRef.current.play().catch((error) => {
|
||||
console.error('Failed to start WaveSurfer visualization:', error);
|
||||
setIsPlaying(false);
|
||||
setError(`Playback error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Native playback failed, falling back to WaveSurfer:', error);
|
||||
// Fall through to WaveSurfer playback
|
||||
isUsingNativePlaybackRef.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Standard WaveSurfer playback
|
||||
if (!wavesurferRef.current) {
|
||||
console.error('WaveSurfer not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if audio is loaded
|
||||
if (duration === 0 && !isLoading) {
|
||||
console.error('Audio not loaded yet');
|
||||
setError('Audio not loaded. Please wait...');
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard WaveSurfer playback (or fallback from native playback failure)
|
||||
if (wavesurferRef.current.isPlaying()) {
|
||||
wavesurferRef.current.pause();
|
||||
} else {
|
||||
// Ensure WaveSurfer is not muted if not using native playback
|
||||
if (!isUsingNativePlaybackRef.current) {
|
||||
const mediaElement = wavesurferRef.current.getMediaElement();
|
||||
if (mediaElement) {
|
||||
mediaElement.muted = false;
|
||||
mediaElement.volume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
wavesurferRef.current.play().catch((error) => {
|
||||
console.error('Failed to play:', error);
|
||||
setIsPlaying(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use cpal::{Device, Host, SampleFormat, StreamConfig};
|
||||
use cpal::{Device, Host, SampleFormat, Stream, StreamConfig};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct AudioOutputDevice {
|
||||
@@ -12,15 +12,24 @@ pub struct AudioOutputDevice {
|
||||
|
||||
pub struct AudioOutputState {
|
||||
host: Host,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl AudioOutputState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
host: cpal::default_host(),
|
||||
stop_flag: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_all_playback(&self) -> Result<(), String> {
|
||||
eprintln!("stop_all_playback: Setting stop flag");
|
||||
self.stop_flag.store(true, Ordering::Relaxed);
|
||||
eprintln!("stop_all_playback: Stop flag set - active streams will output silence");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_output_devices(&self) -> Result<Vec<AudioOutputDevice>, String> {
|
||||
let devices = self
|
||||
.host
|
||||
@@ -91,11 +100,18 @@ impl AudioOutputState {
|
||||
}
|
||||
|
||||
eprintln!("Playing to {} device(s)", devices.len());
|
||||
|
||||
// Stop any existing playback first
|
||||
self.stop_all_playback().ok();
|
||||
|
||||
// Reset stop flag for new playback
|
||||
self.stop_flag.store(false, Ordering::Relaxed);
|
||||
|
||||
// Play to each device
|
||||
for (i, device) in devices.iter().enumerate() {
|
||||
let device_name = device.name().unwrap_or_else(|_| "unknown".to_string());
|
||||
eprintln!("Playing to device {}/{}: {}", i + 1, devices.len(), device_name);
|
||||
self.play_to_device(device, samples.clone(), sample_rate, channels)
|
||||
self.play_to_device(device, samples.clone(), sample_rate, channels, self.stop_flag.clone())
|
||||
.map_err(|e| format!("Failed to play to device {}: {}", device_name, e))?;
|
||||
eprintln!("Successfully started playback on device: {}", device_name);
|
||||
}
|
||||
@@ -231,6 +247,7 @@ impl AudioOutputState {
|
||||
samples: Vec<f32>,
|
||||
sample_rate: u32,
|
||||
channels: u16,
|
||||
stop_flag: Arc<AtomicBool>,
|
||||
) -> Result<(), String> {
|
||||
let device_name = device.name().unwrap_or_else(|_| "unknown".to_string());
|
||||
eprintln!("play_to_device: Starting playback to device: {}", device_name);
|
||||
@@ -281,6 +298,7 @@ impl AudioOutputState {
|
||||
buffer_size: cpal::BufferSize::Default,
|
||||
};
|
||||
|
||||
let stop_flag_clone = stop_flag.clone();
|
||||
let stream = match config.sample_format() {
|
||||
SampleFormat::F32 => {
|
||||
let buffer = buffer_clone.clone();
|
||||
@@ -289,6 +307,14 @@ impl AudioOutputState {
|
||||
.build_output_stream(
|
||||
&stream_config,
|
||||
move |data: &mut [f32], _: &cpal::OutputCallbackInfo| {
|
||||
// Check stop flag - if set, output silence
|
||||
if stop_flag_clone.load(Ordering::Relaxed) {
|
||||
for sample in data.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut idx = pos.load(Ordering::Relaxed);
|
||||
let buf = buffer.lock().unwrap();
|
||||
for sample in data.iter_mut() {
|
||||
@@ -313,6 +339,14 @@ impl AudioOutputState {
|
||||
.build_output_stream(
|
||||
&stream_config,
|
||||
move |data: &mut [i16], _: &cpal::OutputCallbackInfo| {
|
||||
// Check stop flag - if set, output silence
|
||||
if stop_flag_clone.load(Ordering::Relaxed) {
|
||||
for sample in data.iter_mut() {
|
||||
*sample = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut idx = pos.load(Ordering::Relaxed);
|
||||
let buf = buffer.lock().unwrap();
|
||||
for sample in data.iter_mut() {
|
||||
@@ -337,6 +371,14 @@ impl AudioOutputState {
|
||||
.build_output_stream(
|
||||
&stream_config,
|
||||
move |data: &mut [u16], _: &cpal::OutputCallbackInfo| {
|
||||
// Check stop flag - if set, output silence
|
||||
if stop_flag_clone.load(Ordering::Relaxed) {
|
||||
for sample in data.iter_mut() {
|
||||
*sample = 32768;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let mut idx = pos.load(Ordering::Relaxed);
|
||||
let buf = buffer.lock().unwrap();
|
||||
for sample in data.iter_mut() {
|
||||
@@ -365,15 +407,6 @@ impl AudioOutputState {
|
||||
|
||||
eprintln!("play_to_device: Stream started successfully");
|
||||
|
||||
// Keep stream alive until playback completes
|
||||
// In a real implementation, we'd track this and clean up when done
|
||||
eprintln!("play_to_device: Keeping stream alive for ~{} seconds", duration_secs.min(30));
|
||||
std::thread::spawn(move || {
|
||||
let sleep_duration = std::time::Duration::from_secs(duration_secs.min(30));
|
||||
std::thread::sleep(sleep_duration);
|
||||
eprintln!("play_to_device: Stream sleep completed, stream will be dropped");
|
||||
});
|
||||
|
||||
eprintln!("play_to_device: Function completed successfully");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -385,6 +385,13 @@ async fn play_audio_to_devices(
|
||||
state.play_audio_to_devices(audio_data, device_ids).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
fn stop_audio_playback(
|
||||
state: State<'_, audio_output::AudioOutputState>,
|
||||
) -> Result<(), String> {
|
||||
state.stop_all_playback()
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
@@ -443,7 +450,8 @@ pub fn run() {
|
||||
stop_system_audio_capture,
|
||||
is_system_audio_supported,
|
||||
list_audio_output_devices,
|
||||
play_audio_to_devices
|
||||
play_audio_to_devices,
|
||||
stop_audio_playback
|
||||
])
|
||||
.on_window_event(|window, event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
|
||||
Reference in New Issue
Block a user