mxivideo/src/components/AudioChartViewer.tsx

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