mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-09-29 15:15:27 -07:00
Enhance AudioPlayer component for native playback and debugging
- Improved the useNativePlayback logic to include detailed console logging for better debugging. - Updated auto-play functionality to fetch runtime profile channels and channels, ensuring accurate playback decisions. - Refactored audio playback handling to support native audio routing with enhanced error handling and logging. - Introduced a new MultiSelect component for improved channel selection in VoicesTab. - Updated FloatingGenerateBox to include selectedProfileId in audio setting. - Added new dependencies for audio processing in Cargo.toml and Cargo.lock.
This commit is contained in:
@@ -48,17 +48,32 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// Determine if we should use native playback
|
// Determine if we should use native playback
|
||||||
const useNativePlayback = useMemo(() => {
|
const useNativePlayback = useMemo(() => {
|
||||||
if (!isTauri() || !profileChannels || !channels) return false;
|
console.log('useNativePlayback memo:', {
|
||||||
|
isTauri: isTauri(),
|
||||||
|
profileId,
|
||||||
|
profileChannels,
|
||||||
|
channels,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isTauri() || !profileChannels || !channels) {
|
||||||
|
console.log('useNativePlayback: false - missing requirements');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const assignedChannels = channels.filter((ch) =>
|
const assignedChannels = channels.filter((ch) =>
|
||||||
profileChannels.channel_ids.includes(ch.id),
|
profileChannels.channel_ids.includes(ch.id),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
console.log('Assigned channels:', assignedChannels);
|
||||||
|
|
||||||
// Use native playback if any assigned channel has non-default devices
|
// Use native playback if any assigned channel has non-default devices
|
||||||
return assignedChannels.some(
|
const shouldUseNative = assignedChannels.some(
|
||||||
(ch) => ch.device_ids.length > 0 && !ch.is_default,
|
(ch) => ch.device_ids.length > 0 && !ch.is_default,
|
||||||
);
|
);
|
||||||
}, [profileChannels, channels, isTauri()]);
|
|
||||||
|
console.log('useNativePlayback result:', shouldUseNative);
|
||||||
|
return shouldUseNative;
|
||||||
|
}, [profileChannels, channels, profileId]);
|
||||||
|
|
||||||
const waveformRef = useRef<HTMLDivElement>(null);
|
const waveformRef = useRef<HTMLDivElement>(null);
|
||||||
const wavesurferRef = useRef<WaveSurfer | null>(null);
|
const wavesurferRef = useRef<WaveSurfer | null>(null);
|
||||||
@@ -157,7 +172,7 @@ export function AudioPlayer() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update store when duration is loaded
|
// Update store when duration is loaded
|
||||||
wavesurfer.on('ready', () => {
|
wavesurfer.on('ready', async () => {
|
||||||
const dur = wavesurfer.getDuration();
|
const dur = wavesurfer.getDuration();
|
||||||
setDuration(dur);
|
setDuration(dur);
|
||||||
loadingRef.current = false;
|
loadingRef.current = false;
|
||||||
@@ -178,7 +193,95 @@ export function AudioPlayer() {
|
|||||||
console.log('Audio element volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
console.log('Audio element volume:', mediaElement.volume, 'muted:', mediaElement.muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto-play when ready
|
// Auto-play when ready - check if we should use native playback
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch runtime channel data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Auto-play check:', {
|
||||||
|
isTauri: isTauri(),
|
||||||
|
currentAudioUrl,
|
||||||
|
currentProfileId,
|
||||||
|
hasProfileChannels: !!runtimeProfileChannels,
|
||||||
|
hasChannels: !!runtimeChannels,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isTauri() && currentAudioUrl && currentProfileId && runtimeProfileChannels && runtimeChannels) {
|
||||||
|
console.log('Attempting native audio playback...');
|
||||||
|
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');
|
||||||
|
} 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);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
} 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);
|
||||||
|
// Fall through to WaveSurfer playback
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('Not using native playback, using WaveSurfer');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard WaveSurfer auto-play
|
||||||
// Use a small delay to ensure audio element is fully ready
|
// Use a small delay to ensure audio element is fully ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
wavesurfer.play().catch((error) => {
|
wavesurfer.play().catch((error) => {
|
||||||
@@ -454,8 +557,8 @@ export function AudioPlayer() {
|
|||||||
|
|
||||||
// Play via native audio
|
// Play via native audio
|
||||||
await invoke('play_audio_to_devices', {
|
await invoke('play_audio_to_devices', {
|
||||||
audio_data: Array.from(audioData),
|
audioData: Array.from(audioData),
|
||||||
device_ids: deviceIds,
|
deviceIds: deviceIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsPlaying(true);
|
setIsPlaying(true);
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export function FloatingGenerateBox({ isPlayerOpen }: FloatingGenerateBoxProps)
|
|||||||
});
|
});
|
||||||
|
|
||||||
const audioUrl = apiClient.getAudioUrl(result.id);
|
const audioUrl = apiClient.getAudioUrl(result.id);
|
||||||
setAudio(audioUrl, result.id, data.text.substring(0, 50));
|
setAudio(audioUrl, result.id, selectedProfileId, data.text.substring(0, 50));
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
setIsExpanded(false);
|
setIsExpanded(false);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { MultiSelect } from '@/components/ui/multi-select';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { apiClient } from '@/lib/api/client';
|
||||||
|
import type { VoiceProfileResponse } from '@/lib/api/types';
|
||||||
import { useHistory } from '@/lib/hooks/useHistory';
|
import { useHistory } from '@/lib/hooks/useHistory';
|
||||||
import { useDeleteProfile, useProfileSamples, useProfiles } from '@/lib/hooks/useProfiles';
|
import { useDeleteProfile, useProfileSamples, useProfiles } from '@/lib/hooks/useProfiles';
|
||||||
import { useUIStore } from '@/stores/uiStore';
|
import { useUIStore } from '@/stores/uiStore';
|
||||||
@@ -136,12 +138,7 @@ export function VoicesTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface VoiceRowProps {
|
interface VoiceRowProps {
|
||||||
profile: {
|
profile: VoiceProfileResponse;
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
description: string | null;
|
|
||||||
language: string;
|
|
||||||
};
|
|
||||||
generationCount: number;
|
generationCount: number;
|
||||||
channelIds: string[];
|
channelIds: string[];
|
||||||
channels: Array<{ id: string; name: string; is_default: boolean }>;
|
channels: Array<{ id: string; name: string; is_default: boolean }>;
|
||||||
@@ -175,22 +172,16 @@ function VoiceRow({
|
|||||||
<TableCell>{generationCount}</TableCell>
|
<TableCell>{generationCount}</TableCell>
|
||||||
<TableCell>{samples?.length || 0}</TableCell>
|
<TableCell>{samples?.length || 0}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<select
|
<MultiSelect
|
||||||
multiple
|
options={channels.map((ch) => ({
|
||||||
|
value: ch.id,
|
||||||
|
label: `${ch.name}${ch.is_default ? ' (Default)' : ''}`,
|
||||||
|
}))}
|
||||||
value={channelIds}
|
value={channelIds}
|
||||||
onChange={(e) => {
|
onChange={onChannelChange}
|
||||||
const selected = Array.from(e.target.selectedOptions, (opt) => opt.value);
|
placeholder="Select channels..."
|
||||||
onChannelChange(selected);
|
className="min-w-[200px]"
|
||||||
}}
|
/>
|
||||||
className="w-full min-w-[200px] border rounded px-2 py-1 text-sm"
|
|
||||||
size={Math.min(channels.length + 1, 5)}
|
|
||||||
>
|
|
||||||
{channels.map((ch) => (
|
|
||||||
<option key={ch.id} value={ch.id}>
|
|
||||||
{ch.name} {ch.is_default && '(Default)'}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { ChevronDown, Check } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils/cn';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||||
|
|
||||||
|
export interface MultiSelectOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultiSelectProps {
|
||||||
|
options: MultiSelectOption[];
|
||||||
|
value: string[];
|
||||||
|
onChange: (value: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MultiSelectCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
MultiSelectCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
export function MultiSelect({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Select...',
|
||||||
|
className,
|
||||||
|
}: MultiSelectProps) {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
|
||||||
|
const handleSelect = (optionValue: string) => {
|
||||||
|
const newValue = value.includes(optionValue)
|
||||||
|
? value.filter((v) => v !== optionValue)
|
||||||
|
: [...value, optionValue];
|
||||||
|
onChange(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayText =
|
||||||
|
value.length === 0
|
||||||
|
? placeholder
|
||||||
|
: value.length === 1
|
||||||
|
? options.find((opt) => opt.value === value[0])?.label || placeholder
|
||||||
|
: `${value.length} selected`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu open={open} onOpenChange={setOpen}>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
'flex h-8 w-full items-center justify-between rounded-full border border-border bg-card px-3 py-2 text-xs ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 hover:bg-background/50 transition-all',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span className="line-clamp-1">{displayText}</span>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent
|
||||||
|
className="max-h-96 overflow-auto"
|
||||||
|
align="start"
|
||||||
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<MultiSelectCheckboxItem
|
||||||
|
key={option.value}
|
||||||
|
checked={value.includes(option.value)}
|
||||||
|
onSelect={() => handleSelect(option.value)}
|
||||||
|
onCheckedChange={() => handleSelect(option.value)}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</MultiSelectCheckboxItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
Generated
+383
-5
@@ -32,6 +32,28 @@ dependencies = [
|
|||||||
"alloc-no-stdlib",
|
"alloc-no-stdlib",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alsa"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43"
|
||||||
|
dependencies = [
|
||||||
|
"alsa-sys",
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "alsa-sys"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -56,6 +78,12 @@ dependencies = [
|
|||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atk"
|
name = "atk"
|
||||||
version = "0.18.2"
|
version = "0.18.2"
|
||||||
@@ -276,6 +304,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
|
checksum = "6354c81bbfd62d9cfa9cb3c773c2b7b2a3a482d569de977fd0e961f6e7c00583"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -416,6 +446,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coreaudio-rs"
|
||||||
|
version = "0.11.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"coreaudio-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coreaudio-sys"
|
name = "coreaudio-sys"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -425,6 +466,29 @@ dependencies = [
|
|||||||
"bindgen",
|
"bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cpal"
|
||||||
|
version = "0.15.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||||
|
dependencies = [
|
||||||
|
"alsa",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"coreaudio-rs",
|
||||||
|
"dasp_sample",
|
||||||
|
"jni",
|
||||||
|
"js-sys",
|
||||||
|
"libc",
|
||||||
|
"mach2",
|
||||||
|
"ndk 0.8.0",
|
||||||
|
"ndk-context",
|
||||||
|
"oboe",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"windows 0.54.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -540,6 +604,12 @@ dependencies = [
|
|||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dasp_sample"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -755,6 +825,12 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "extended"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -1653,6 +1729,16 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.85"
|
version = "0.3.85"
|
||||||
@@ -1814,6 +1900,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mach2"
|
||||||
|
version = "0.4.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "malloc_buf"
|
name = "malloc_buf"
|
||||||
version = "0.0.6"
|
version = "0.0.6"
|
||||||
@@ -1929,6 +2024,20 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndk"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.10.0",
|
||||||
|
"jni-sys",
|
||||||
|
"log",
|
||||||
|
"ndk-sys 0.5.0+25.2.9519653",
|
||||||
|
"num_enum",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -1938,7 +2047,7 @@ dependencies = [
|
|||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"jni-sys",
|
"jni-sys",
|
||||||
"log",
|
"log",
|
||||||
"ndk-sys",
|
"ndk-sys 0.6.0+11769913",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
@@ -1950,6 +2059,15 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndk-sys"
|
||||||
|
version = "0.5.0+25.2.9519653"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||||
|
dependencies = [
|
||||||
|
"jni-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk-sys"
|
name = "ndk-sys"
|
||||||
version = "0.6.0+11769913"
|
version = "0.6.0+11769913"
|
||||||
@@ -1987,6 +2105,17 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-derive"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-integer"
|
name = "num-integer"
|
||||||
version = "0.1.46"
|
version = "0.1.46"
|
||||||
@@ -2260,6 +2389,29 @@ dependencies = [
|
|||||||
"objc2-security",
|
"objc2-security",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oboe"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||||
|
dependencies = [
|
||||||
|
"jni",
|
||||||
|
"ndk 0.8.0",
|
||||||
|
"ndk-context",
|
||||||
|
"num-derive",
|
||||||
|
"num-traits",
|
||||||
|
"oboe-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oboe-sys"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@@ -3448,7 +3600,7 @@ checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"bytemuck",
|
"bytemuck",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"ndk",
|
"ndk 0.9.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-core-graphics",
|
"objc2-core-graphics",
|
||||||
@@ -3542,6 +3694,201 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"symphonia-bundle-flac",
|
||||||
|
"symphonia-bundle-mp3",
|
||||||
|
"symphonia-codec-aac",
|
||||||
|
"symphonia-codec-adpcm",
|
||||||
|
"symphonia-codec-alac",
|
||||||
|
"symphonia-codec-pcm",
|
||||||
|
"symphonia-codec-vorbis",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-format-caf",
|
||||||
|
"symphonia-format-isomp4",
|
||||||
|
"symphonia-format-mkv",
|
||||||
|
"symphonia-format-ogg",
|
||||||
|
"symphonia-format-riff",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-bundle-flac"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-bundle-mp3"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-aac"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-adpcm"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2dddc50e2bbea4cfe027441eece77c46b9f319748605ab8f3443350129ddd07f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-alac"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8413fa754942ac16a73634c9dfd1500ed5c61430956b33728567f667fdd393ab"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-pcm"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-codec-vorbis"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-core"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"bytemuck",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-caf"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8faf379316b6b6e6bbc274d00e7a592e0d63ff1a7e182ce8ba25e24edd3d096"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-isomp4"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-mkv"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "122d786d2c43a49beb6f397551b4a050d8229eaa54c7ddf9ee4b98899b8742d0"
|
||||||
|
dependencies = [
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-ogg"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
"symphonia-utils-xiph",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-format-riff"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f"
|
||||||
|
dependencies = [
|
||||||
|
"extended",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-metadata"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16"
|
||||||
|
dependencies = [
|
||||||
|
"encoding_rs",
|
||||||
|
"lazy_static",
|
||||||
|
"log",
|
||||||
|
"symphonia-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symphonia-utils-xiph"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16"
|
||||||
|
dependencies = [
|
||||||
|
"symphonia-core",
|
||||||
|
"symphonia-metadata",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
@@ -3618,9 +3965,9 @@ dependencies = [
|
|||||||
"lazy_static",
|
"lazy_static",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"ndk",
|
"ndk 0.9.0",
|
||||||
"ndk-context",
|
"ndk-context",
|
||||||
"ndk-sys",
|
"ndk-sys 0.6.0+11769913",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-foundation",
|
"objc2-foundation",
|
||||||
@@ -4498,12 +4845,14 @@ dependencies = [
|
|||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"coreaudio-sys",
|
"coreaudio-sys",
|
||||||
|
"cpal",
|
||||||
"hound",
|
"hound",
|
||||||
"objc",
|
"objc",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"screencapturekit",
|
"screencapturekit",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"symphonia",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
@@ -4816,6 +5165,16 @@ dependencies = [
|
|||||||
"windows-version",
|
"windows-version",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core 0.54.0",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.61.3"
|
version = "0.61.3"
|
||||||
@@ -4859,6 +5218,16 @@ dependencies = [
|
|||||||
"windows-core 0.62.2",
|
"windows-core 0.62.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.54.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65"
|
||||||
|
dependencies = [
|
||||||
|
"windows-result 0.1.2",
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.2"
|
version = "0.61.2"
|
||||||
@@ -4961,6 +5330,15 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -5316,7 +5694,7 @@ dependencies = [
|
|||||||
"jni",
|
"jni",
|
||||||
"kuchikiki",
|
"kuchikiki",
|
||||||
"libc",
|
"libc",
|
||||||
"ndk",
|
"ndk 0.9.0",
|
||||||
"objc2",
|
"objc2",
|
||||||
"objc2-app-kit",
|
"objc2-app-kit",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ tokio = { version = "1", features = ["full"] }
|
|||||||
hound = "3.5"
|
hound = "3.5"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
cpal = "0.15"
|
cpal = "0.15"
|
||||||
symphonia = { version = "0.5", features = ["wav", "pcm"] }
|
symphonia = { version = "0.5", features = ["all"] }
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
|||||||
Binary file not shown.
@@ -58,10 +58,16 @@ impl AudioOutputState {
|
|||||||
audio_data: Vec<u8>,
|
audio_data: Vec<u8>,
|
||||||
device_ids: Vec<String>,
|
device_ids: Vec<String>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
eprintln!("play_audio_to_devices called with {} bytes, {} device IDs", audio_data.len(), device_ids.len());
|
||||||
|
eprintln!("Requested device IDs: {:?}", device_ids);
|
||||||
|
|
||||||
// Decode audio file (assuming WAV format)
|
// Decode audio file (assuming WAV format)
|
||||||
|
eprintln!("Decoding audio data...");
|
||||||
let (samples, sample_rate, channels) = self.decode_wav(&audio_data)?;
|
let (samples, sample_rate, channels) = self.decode_wav(&audio_data)?;
|
||||||
|
eprintln!("Audio decoded: {} samples, {}Hz, {} channels", samples.len(), sample_rate, channels);
|
||||||
|
|
||||||
// Find devices by ID
|
// Find devices by ID
|
||||||
|
eprintln!("Enumerating output devices...");
|
||||||
let devices: Vec<Device> = self
|
let devices: Vec<Device> = self
|
||||||
.host
|
.host
|
||||||
.output_devices()
|
.output_devices()
|
||||||
@@ -69,7 +75,9 @@ impl AudioOutputState {
|
|||||||
.filter_map(|device| {
|
.filter_map(|device| {
|
||||||
let name = device.name().ok()?;
|
let name = device.name().ok()?;
|
||||||
let id = format!("device_{}", name.replace(' ', "_").to_lowercase());
|
let id = format!("device_{}", name.replace(' ', "_").to_lowercase());
|
||||||
|
eprintln!("Found device: {} (id: {})", name, id);
|
||||||
if device_ids.contains(&id) {
|
if device_ids.contains(&id) {
|
||||||
|
eprintln!(" -> Matched! Will play to this device");
|
||||||
Some(device)
|
Some(device)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
@@ -78,15 +86,21 @@ impl AudioOutputState {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if devices.is_empty() {
|
if devices.is_empty() {
|
||||||
|
eprintln!("ERROR: No matching devices found");
|
||||||
return Err("No matching devices found".to_string());
|
return Err("No matching devices found".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eprintln!("Playing to {} device(s)", devices.len());
|
||||||
// Play to each device
|
// Play to each device
|
||||||
for device in devices {
|
for (i, device) in devices.iter().enumerate() {
|
||||||
self.play_to_device(&device, samples.clone(), sample_rate, channels)
|
let device_name = device.name().unwrap_or_else(|_| "unknown".to_string());
|
||||||
.map_err(|e| format!("Failed to play to device: {}", e))?;
|
eprintln!("Playing to device {}/{}: {}", i + 1, devices.len(), device_name);
|
||||||
|
self.play_to_device(device, samples.clone(), sample_rate, channels)
|
||||||
|
.map_err(|e| format!("Failed to play to device {}: {}", device_name, e))?;
|
||||||
|
eprintln!("Successfully started playback on device: {}", device_name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eprintln!("play_audio_to_devices completed successfully");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,83 +108,120 @@ impl AudioOutputState {
|
|||||||
use symphonia::core::formats::FormatOptions;
|
use symphonia::core::formats::FormatOptions;
|
||||||
use symphonia::core::io::MediaSourceStream;
|
use symphonia::core::io::MediaSourceStream;
|
||||||
use symphonia::core::meta::MetadataOptions;
|
use symphonia::core::meta::MetadataOptions;
|
||||||
use symphonia::core::probe::Probe;
|
|
||||||
|
|
||||||
|
eprintln!("decode_wav: Creating MediaSourceStream from {} bytes", data.len());
|
||||||
let mss = MediaSourceStream::new(
|
let mss = MediaSourceStream::new(
|
||||||
Box::new(std::io::Cursor::new(data)),
|
Box::new(std::io::Cursor::new(data.to_vec())),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut probe = Probe::default();
|
eprintln!("decode_wav: Probing audio format...");
|
||||||
let mut format = probe
|
let mut format = symphonia::default::get_probe()
|
||||||
.format(
|
.format(
|
||||||
&Default::default(),
|
&Default::default(),
|
||||||
mss,
|
mss,
|
||||||
&FormatOptions::default(),
|
&FormatOptions::default(),
|
||||||
&MetadataOptions::default(),
|
&MetadataOptions::default(),
|
||||||
)
|
)
|
||||||
.map_err(|e| format!("Failed to probe audio: {}", e))?
|
.map_err(|e| {
|
||||||
|
eprintln!("decode_wav: Failed to probe audio: {}", e);
|
||||||
|
format!("Failed to probe audio: {}", e)
|
||||||
|
})?
|
||||||
.format;
|
.format;
|
||||||
|
|
||||||
|
eprintln!("decode_wav: Audio format probed successfully");
|
||||||
|
|
||||||
|
eprintln!("decode_wav: Finding audio track...");
|
||||||
let track = format
|
let track = format
|
||||||
.tracks()
|
.tracks()
|
||||||
.iter()
|
.iter()
|
||||||
.find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
|
.find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
|
||||||
.ok_or("No audio track found")?;
|
.ok_or_else(|| {
|
||||||
|
eprintln!("decode_wav: No audio track found");
|
||||||
|
"No audio track found".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
let sample_rate = track
|
let sample_rate = track
|
||||||
.codec_params
|
.codec_params
|
||||||
.sample_rate
|
.sample_rate
|
||||||
.ok_or("No sample rate found")?;
|
.ok_or_else(|| {
|
||||||
|
eprintln!("decode_wav: No sample rate found in track");
|
||||||
|
"No sample rate found".to_string()
|
||||||
|
})?;
|
||||||
|
|
||||||
let channels = track
|
let channels = track
|
||||||
.codec_params
|
.codec_params
|
||||||
.channels
|
.channels
|
||||||
.ok_or("No channels found")?
|
.ok_or_else(|| {
|
||||||
|
eprintln!("decode_wav: No channels found in track");
|
||||||
|
"No channels found".to_string()
|
||||||
|
})?
|
||||||
.count() as u16;
|
.count() as u16;
|
||||||
|
|
||||||
|
eprintln!("decode_wav: Track info - sample_rate: {}, channels: {}", sample_rate, channels);
|
||||||
|
|
||||||
|
eprintln!("decode_wav: Creating decoder...");
|
||||||
let mut decoder = symphonia::default::get_codecs()
|
let mut decoder = symphonia::default::get_codecs()
|
||||||
.make(&track.codec_params, &Default::default())
|
.make(&track.codec_params, &Default::default())
|
||||||
.map_err(|e| format!("Failed to create decoder: {}", e))?;
|
.map_err(|e| {
|
||||||
|
eprintln!("decode_wav: Failed to create decoder: {}", e);
|
||||||
|
format!("Failed to create decoder: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
eprintln!("decode_wav: Decoder created successfully");
|
||||||
|
|
||||||
let mut samples = Vec::new();
|
let mut samples = Vec::new();
|
||||||
|
let mut packet_count = 0;
|
||||||
|
eprintln!("decode_wav: Starting packet decoding loop...");
|
||||||
loop {
|
loop {
|
||||||
let packet = match format.next_packet() {
|
let packet = match format.next_packet() {
|
||||||
Ok(packet) => packet,
|
Ok(packet) => packet,
|
||||||
Err(_) => break, // End of stream
|
Err(e) => {
|
||||||
|
eprintln!("decode_wav: End of stream or error: {:?}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
packet_count += 1;
|
||||||
let decoded = decoder
|
let decoded = decoder
|
||||||
.decode(&packet)
|
.decode(&packet)
|
||||||
.map_err(|e| format!("Decode error: {}", e))?;
|
.map_err(|e| {
|
||||||
|
eprintln!("decode_wav: Decode error on packet {}: {}", packet_count, e);
|
||||||
|
format!("Decode error: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Convert to f32 samples by matching on the buffer type
|
||||||
|
use symphonia::core::audio::{AudioBufferRef, Signal};
|
||||||
|
use symphonia::core::conv::FromSample;
|
||||||
|
|
||||||
// Convert to f32 samples
|
|
||||||
let spec = *decoded.spec();
|
let spec = *decoded.spec();
|
||||||
let duration = decoded.capacity() as u64;
|
let num_channels = spec.channels.count();
|
||||||
|
let num_frames = decoded.frames();
|
||||||
|
|
||||||
// Handle multi-channel audio
|
eprintln!("decode_wav: Packet {} - {} frames, {} channels", packet_count, num_frames, num_channels);
|
||||||
if spec.channels.count() == 1 {
|
|
||||||
// Mono
|
// Interleave samples from all channels
|
||||||
let plane = decoded.plane(0);
|
for frame_idx in 0..num_frames {
|
||||||
for i in 0..duration {
|
for ch in 0..num_channels {
|
||||||
if let Some(&sample) = plane.get(i as usize) {
|
let sample_f32 = match &decoded {
|
||||||
samples.push(sample as f32 / 32768.0);
|
AudioBufferRef::U8(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
}
|
AudioBufferRef::U16(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
}
|
AudioBufferRef::U24(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
} else {
|
AudioBufferRef::U32(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
// Multi-channel - interleave
|
AudioBufferRef::S8(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
for i in 0..duration {
|
AudioBufferRef::S16(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
for ch in 0..spec.channels.count() {
|
AudioBufferRef::S24(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
if let Some(plane) = decoded.plane(ch) {
|
AudioBufferRef::S32(buf) => f32::from_sample(buf.chan(ch)[frame_idx]),
|
||||||
if let Some(&sample) = plane.get(i as usize) {
|
AudioBufferRef::F32(buf) => buf.chan(ch)[frame_idx],
|
||||||
samples.push(sample as f32 / 32768.0);
|
AudioBufferRef::F64(buf) => buf.chan(ch)[frame_idx] as f32,
|
||||||
}
|
};
|
||||||
}
|
samples.push(sample_f32);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eprintln!("decode_wav: Decoded {} packets, total {} samples", packet_count, samples.len());
|
||||||
|
eprintln!("decode_wav: Returning sample_rate={}, channels={}", sample_rate, channels);
|
||||||
Ok((samples, sample_rate, channels))
|
Ok((samples, sample_rate, channels))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +232,10 @@ impl AudioOutputState {
|
|||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
channels: u16,
|
channels: u16,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
|
let device_name = device.name().unwrap_or_else(|_| "unknown".to_string());
|
||||||
|
eprintln!("play_to_device: Starting playback to device: {}", device_name);
|
||||||
|
eprintln!("play_to_device: Input - {} samples, {}Hz, {} channels", samples.len(), sample_rate, channels);
|
||||||
|
|
||||||
let config = device
|
let config = device
|
||||||
.default_output_config()
|
.default_output_config()
|
||||||
.map_err(|e| format!("Failed to get default config: {}", e))?;
|
.map_err(|e| format!("Failed to get default config: {}", e))?;
|
||||||
@@ -188,16 +243,29 @@ impl AudioOutputState {
|
|||||||
// Prepare samples for the device's format
|
// Prepare samples for the device's format
|
||||||
let device_sample_rate = config.sample_rate().0;
|
let device_sample_rate = config.sample_rate().0;
|
||||||
let device_channels = config.channels();
|
let device_channels = config.channels();
|
||||||
|
let device_sample_format = config.sample_format();
|
||||||
|
|
||||||
|
eprintln!("play_to_device: Device config - {}Hz, {} channels, format: {:?}",
|
||||||
|
device_sample_rate, device_channels, device_sample_format);
|
||||||
|
|
||||||
// Resample if needed (simple linear interpolation for now)
|
// Resample if needed (simple linear interpolation for now)
|
||||||
let resampled = if device_sample_rate != sample_rate {
|
let resampled = if device_sample_rate != sample_rate {
|
||||||
self.resample(&samples, sample_rate, device_sample_rate)
|
eprintln!("play_to_device: Resampling from {}Hz to {}Hz", sample_rate, device_sample_rate);
|
||||||
|
let result = self.resample(&samples, sample_rate, device_sample_rate);
|
||||||
|
eprintln!("play_to_device: Resampled {} samples to {} samples", samples.len(), result.len());
|
||||||
|
result
|
||||||
} else {
|
} else {
|
||||||
|
eprintln!("play_to_device: No resampling needed");
|
||||||
samples
|
samples
|
||||||
};
|
};
|
||||||
|
|
||||||
// Interleave/convert channels if needed
|
// Interleave/convert channels if needed
|
||||||
|
eprintln!("play_to_device: Interleaving channels from {} to {} channels", channels, device_channels);
|
||||||
let interleaved = self.interleave_channels(&resampled, channels, device_channels);
|
let interleaved = self.interleave_channels(&resampled, channels, device_channels);
|
||||||
|
eprintln!("play_to_device: Interleaved to {} samples", interleaved.len());
|
||||||
|
|
||||||
|
// Calculate duration before moving interleaved
|
||||||
|
let duration_secs = (interleaved.len() as f64 / (device_sample_rate as f64 * device_channels as f64)).ceil() as u64 + 1;
|
||||||
|
|
||||||
// Create shared buffer for playback
|
// Create shared buffer for playback
|
||||||
let buffer: Arc<Mutex<Vec<f32>>> = Arc::new(Mutex::new(interleaved));
|
let buffer: Arc<Mutex<Vec<f32>>> = Arc::new(Mutex::new(interleaved));
|
||||||
@@ -289,14 +357,24 @@ impl AudioOutputState {
|
|||||||
_ => return Err("Unsupported sample format".to_string()),
|
_ => return Err("Unsupported sample format".to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
stream.play().map_err(|e| format!("Failed to play stream: {}", e))?;
|
eprintln!("play_to_device: Starting stream playback...");
|
||||||
|
stream.play().map_err(|e| {
|
||||||
|
eprintln!("play_to_device: Failed to play stream: {}", e);
|
||||||
|
format!("Failed to play stream: {}", e)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
eprintln!("play_to_device: Stream started successfully");
|
||||||
|
|
||||||
// Keep stream alive until playback completes
|
// Keep stream alive until playback completes
|
||||||
// In a real implementation, we'd track this and clean up when done
|
// 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 || {
|
std::thread::spawn(move || {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(30)); // Max 30s
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user