192 lines
7.1 KiB
TypeScript
192 lines
7.1 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import { X, BarChart3, Download } from 'lucide-react'
|
|
import { invoke } from '@tauri-apps/api/core'
|
|
import { AudioFile } from '../services/audioService'
|
|
|
|
interface AudioChartViewerProps {
|
|
isOpen: boolean
|
|
audio: AudioFile | null
|
|
onClose: () => void
|
|
}
|
|
|
|
const AudioChartViewer: React.FC<AudioChartViewerProps> = ({
|
|
isOpen,
|
|
audio,
|
|
onClose
|
|
}) => {
|
|
const [chartImageSrc, setChartImageSrc] = useState<string>('')
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (isOpen && audio && audio.frequency_chart_path) {
|
|
loadChartImage()
|
|
}
|
|
}, [isOpen, audio])
|
|
|
|
const loadChartImage = async () => {
|
|
if (!audio?.frequency_chart_path) return
|
|
|
|
setLoading(true)
|
|
try {
|
|
// 使用Tauri的文件读取API将图片转换为data URL
|
|
const dataUrl = await invoke<string>('read_image_as_data_url', {
|
|
filePath: audio.frequency_chart_path
|
|
})
|
|
setChartImageSrc(dataUrl)
|
|
} catch (error) {
|
|
console.error('Failed to load chart image:', error)
|
|
setChartImageSrc('')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleDownload = () => {
|
|
if (!chartImageSrc || !audio) return
|
|
|
|
// 创建下载链接
|
|
const link = document.createElement('a')
|
|
link.href = chartImageSrc
|
|
link.download = `${audio.filename}_frequency_chart.png`
|
|
document.body.appendChild(link)
|
|
link.click()
|
|
document.body.removeChild(link)
|
|
}
|
|
|
|
if (!isOpen || !audio) return null
|
|
|
|
return (
|
|
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-lg shadow-xl w-full max-w-6xl mx-4 max-h-[90vh] overflow-hidden">
|
|
{/* 标题栏 */}
|
|
<div className="flex items-center justify-between p-4 border-b border-gray-200">
|
|
<div className="flex items-center space-x-3">
|
|
<BarChart3 className="text-blue-600" size={24} />
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-gray-900">音频频率分析</h2>
|
|
<p className="text-sm text-gray-600">{audio.filename}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center space-x-2">
|
|
{chartImageSrc && (
|
|
<button
|
|
onClick={handleDownload}
|
|
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
|
title="下载图表"
|
|
>
|
|
<Download size={20} />
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={onClose}
|
|
className="p-2 text-gray-400 hover:text-gray-600 transition-colors"
|
|
title="关闭"
|
|
>
|
|
<X size={24} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 内容区域 */}
|
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-80px)]">
|
|
{/* 音频信息摘要 */}
|
|
<div className="mb-6 p-4 bg-gray-50 rounded-lg">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-gray-600">时长:</span>
|
|
<span className="ml-2 font-medium">
|
|
{Math.floor(audio.duration / 60)}:{(audio.duration % 60).toFixed(0).padStart(2, '0')}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-gray-600">采样率:</span>
|
|
<span className="ml-2 font-medium">{audio.sample_rate} Hz</span>
|
|
</div>
|
|
{audio.tempo && (
|
|
<div>
|
|
<span className="text-gray-600">节拍:</span>
|
|
<span className="ml-2 font-medium">{audio.tempo.toFixed(1)} BPM</span>
|
|
</div>
|
|
)}
|
|
{audio.spectral_centroid && (
|
|
<div>
|
|
<span className="text-gray-600">频谱重心:</span>
|
|
<span className="ml-2 font-medium">{audio.spectral_centroid.toFixed(0)} Hz</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 频率图表 */}
|
|
<div className="text-center">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-96">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
|
<span className="ml-3 text-gray-600">加载频率图中...</span>
|
|
</div>
|
|
) : chartImageSrc ? (
|
|
<div className="bg-white rounded-lg border border-gray-200 p-4">
|
|
<img
|
|
src={chartImageSrc}
|
|
alt={`${audio.filename} 频率分析图`}
|
|
className="max-w-full h-auto mx-auto"
|
|
style={{ maxHeight: '600px' }}
|
|
/>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-center h-96 text-gray-500">
|
|
<div className="text-center">
|
|
<BarChart3 size={48} className="mx-auto mb-4 text-gray-300" />
|
|
<p>频率图不可用</p>
|
|
<p className="text-sm">可能需要重新处理音频文件</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 音频特征详情 */}
|
|
{(audio.spectral_rolloff || audio.zero_crossing_rate || audio.mfcc_features) && (
|
|
<div className="mt-6 p-4 bg-gray-50 rounded-lg">
|
|
<h3 className="text-md font-medium text-gray-900 mb-3">音频特征分析</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
|
{audio.spectral_rolloff && (
|
|
<div>
|
|
<span className="text-gray-600">频谱滚降:</span>
|
|
<span className="ml-2 font-medium">{audio.spectral_rolloff.toFixed(0)} Hz</span>
|
|
</div>
|
|
)}
|
|
{audio.zero_crossing_rate && (
|
|
<div>
|
|
<span className="text-gray-600">过零率:</span>
|
|
<span className="ml-2 font-medium">{(audio.zero_crossing_rate * 100).toFixed(2)}%</span>
|
|
</div>
|
|
)}
|
|
{audio.mfcc_features && (
|
|
<div>
|
|
<span className="text-gray-600">MFCC特征:</span>
|
|
<span className="ml-2 font-medium">{audio.mfcc_features.length} 维</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 节拍时间点 */}
|
|
{audio.beat_times && audio.beat_times.length > 0 && (
|
|
<div className="mt-3">
|
|
<span className="text-gray-600">检测到节拍点:</span>
|
|
<span className="ml-2 font-medium">{audio.beat_times.length} 个</span>
|
|
<div className="mt-2 text-xs text-gray-500">
|
|
前5个节拍时间: {audio.beat_times.slice(0, 5).map(t => t.toFixed(2)).join('s, ')}s
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AudioChartViewer
|