murmuraba
Version:
Real-time audio noise reduction with advanced chunked processing for web applications
238 lines (237 loc) • 14.4 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useCallback, useMemo } from 'react';
import { WaveformAnalyzer } from '../waveform-analyzer/waveform-analyzer';
import './synced-waveforms.css';
export const SyncedWaveforms = ({ originalAudioUrl, processedAudioUrl,
// isPlaying = false, // Reserved for future use
onPlayingChange, className = '', 'aria-label': ariaLabel, disabled = false, showVolumeControls = true, showPlaybackControls = true, originalLabel = 'Original Audio', processedLabel = 'Processed Audio (Noise Reduced)', originalColor = '#ef4444', processedColor = '#10b981' }) => {
const [originalVolume, setOriginalVolume] = useState(0.5);
const [processedVolume, setProcessedVolume] = useState(0.8);
const [localIsPlaying, setLocalIsPlaying] = useState(false);
const [currentAudioType, setCurrentAudioType] = useState('processed');
const handlePlayingChange = useCallback((playing) => {
if (!disabled) {
setLocalIsPlaying(playing);
onPlayingChange?.(playing);
}
}, [disabled, onPlayingChange]);
// Volume change handlers - reserved for future volume control implementation
// const handleOriginalVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
// const value = Math.max(0, Math.min(1, parseFloat(e.target.value)));
// setOriginalVolume(value);
// }, []);
// const handleProcessedVolumeChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
// const value = Math.max(0, Math.min(1, parseFloat(e.target.value)));
// setProcessedVolume(value);
// }, []);
const handleKeyDown = useCallback((event) => {
if (disabled)
return;
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handlePlayingChange(!localIsPlaying);
}
}, [disabled, localIsPlaying, handlePlayingChange]);
// Memoized styles for performance
const containerStyle = useMemo(() => ({
opacity: disabled ? 0.6 : 1,
pointerEvents: disabled ? 'none' : 'auto',
}), [disabled]);
// Styles reserved for future volume controls implementation
// const volumeControlsStyle = useMemo(() => ({
// display: 'flex',
// gap: '2rem',
// justifyContent: 'center' as const,
// alignItems: 'center' as const,
// marginBottom: '1rem',
// flexWrap: 'wrap' as const,
// }), []);
// const volumeControlStyle = useMemo(() => ({
// display: 'flex',
// alignItems: 'center',
// gap: '0.5rem',
// minWidth: '200px',
// }), []);
const buttonStyle = useMemo(() => ({
padding: '8px 24px',
borderRadius: '24px',
border: 'none',
fontWeight: '500',
fontSize: '14px',
transition: 'all 0.2s ease',
cursor: disabled ? 'not-allowed' : 'pointer',
backgroundColor: disabled ? '#666' : (localIsPlaying ? '#dc2626' : '#4f46e5'),
color: 'white',
opacity: disabled ? 0.6 : 1,
':hover': {
backgroundColor: disabled ? '#666' : (localIsPlaying ? '#b91c1c' : '#3730a3'),
},
}), [disabled, localIsPlaying]);
const toggleAudioType = useCallback(() => {
setCurrentAudioType(prev => prev === 'original' ? 'processed' : 'original');
}, []);
const waveformColumns = [
{
audioUrl: originalAudioUrl,
label: originalLabel,
color: originalColor,
volume: originalVolume,
onVolumeChange: setOriginalVolume,
emoji: '🔴'
},
{
audioUrl: processedAudioUrl,
label: processedLabel,
color: processedColor,
volume: processedVolume,
onVolumeChange: setProcessedVolume,
emoji: '🟢'
}
];
return (_jsxs("div", { className: `synced-waveforms ${className}`, style: {
...containerStyle,
display: 'flex',
flexDirection: 'column',
gap: '1.5rem',
padding: '1.5rem',
borderRadius: '16px',
boxShadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'
}, role: "region", "aria-label": ariaLabel || 'Synchronized audio waveform comparison', children: [_jsx("div", { className: "waveforms-grid", style: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '1.5rem',
alignItems: 'stretch'
}, children: waveformColumns.map((column, index) => (_jsxs("div", { style: {
display: 'flex',
flexDirection: 'column',
gap: '1rem',
padding: '1.25rem',
borderRadius: '12px',
boxShadow: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
transition: 'transform 0.2s ease, box-shadow 0.2s ease',
cursor: disabled ? 'default' : 'pointer'
}, onMouseEnter: (e) => {
if (!disabled) {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
}
}, onMouseLeave: (e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)';
}, children: [_jsx("div", { style: { minHeight: '120px' }, children: _jsx(WaveformAnalyzer, { audioUrl: column.audioUrl, label: column.label, color: column.color, hideControls: true, isPaused: !localIsPlaying, isMuted: index === 0 ? currentAudioType !== 'original' : currentAudioType !== 'processed', volume: column.volume, onPlayStateChange: handlePlayingChange, disabled: disabled, disablePlayback: index === 0 ? currentAudioType !== 'original' : currentAudioType !== 'processed', className: "synced-waveform-analyzer", "aria-label": `${column.label} waveform`, width: 300, height: 120 }) }), showVolumeControls && (_jsxs("div", { style: {
display: 'flex',
flexDirection: 'column',
gap: '0.75rem',
padding: '0.75rem',
background: 'rgba(0, 0, 0, 0.02)',
borderRadius: '8px'
}, children: [_jsxs("div", { style: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: '14px',
fontWeight: '600',
color: column.color
}, children: [_jsxs("span", { children: [column.emoji, " ", column.label] }), _jsxs("span", { style: { fontSize: '16px', fontWeight: '700' }, children: [Math.round(column.volume * 100), "%"] })] }), _jsxs("div", { style: { position: 'relative' }, children: [_jsx("input", { type: "range", min: "0", max: "1", step: "0.01", value: column.volume, onChange: (e) => column.onVolumeChange(parseFloat(e.target.value)), disabled: disabled, style: {
width: '100%',
height: '6px',
borderRadius: '3px',
background: `linear-gradient(to right, ${column.color} 0%, ${column.color} ${column.volume * 100}%, #e5e7eb ${column.volume * 100}%, #e5e7eb 100%)`,
outline: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
WebkitAppearance: 'none',
appearance: 'none'
}, "aria-label": `${column.label} volume` }), _jsx("style", { dangerouslySetInnerHTML: { __html: `
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 20px;
height: 20px;
background: white;
border: 3px solid ${column.color};
border-radius: 50%;
cursor: ${disabled ? 'not-allowed' : 'pointer'};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
input[type="range"]::-webkit-slider-thumb:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: scale(1.1);
}
input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: white;
border: 3px solid ${column.color};
border-radius: 50%;
cursor: ${disabled ? 'not-allowed' : 'pointer'};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
input[type="range"]::-moz-range-thumb:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: scale(1.1);
}
` } })] })] }))] }, index))) }), showPlaybackControls && (_jsxs("div", { style: {
display: 'flex',
justifyContent: 'center',
gap: '1rem',
flexWrap: 'wrap'
}, children: [_jsxs("button", { onClick: () => handlePlayingChange(!localIsPlaying), onKeyDown: handleKeyDown, disabled: disabled, style: {
...buttonStyle,
padding: '12px 32px',
fontSize: '16px',
fontWeight: '600',
background: localIsPlaying
? 'linear-gradient(135deg, #dc2626 0%, #b91c1c 100%)'
: 'linear-gradient(135deg, #4f46e5 0%, #3730a3 100%)',
border: 'none',
borderRadius: '12px',
color: 'white',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
boxShadow: localIsPlaying
? '0 4px 14px 0 rgba(220, 38, 38, 0.35)'
: '0 4px 14px 0 rgba(79, 70, 229, 0.35)',
transition: 'all 0.3s ease',
transform: 'translateY(0)'
}, onMouseEnter: (e) => {
if (!disabled) {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = localIsPlaying
? '0 6px 20px 0 rgba(220, 38, 38, 0.4)'
: '0 6px 20px 0 rgba(79, 70, 229, 0.4)';
}
}, onMouseLeave: (e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = localIsPlaying
? '0 4px 14px 0 rgba(220, 38, 38, 0.35)'
: '0 4px 14px 0 rgba(79, 70, 229, 0.35)';
}, "aria-label": localIsPlaying ? 'Pause synchronized playback' : 'Play synchronized playback', children: [_jsx("span", { style: { fontSize: '20px' }, children: localIsPlaying ? '⏸' : '▶' }), _jsx("span", { children: localIsPlaying ? 'Pause' : 'Play Both' })] }), _jsxs("button", { onClick: toggleAudioType, disabled: disabled || !localIsPlaying, style: {
padding: '12px 24px',
fontSize: '14px',
fontWeight: '600',
background: currentAudioType === 'original'
? 'linear-gradient(135deg, #ef4444 0%, #dc2626 100%)'
: 'linear-gradient(135deg, #10b981 0%, #059669 100%)',
border: 'none',
borderRadius: '12px',
color: 'white',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
boxShadow: currentAudioType === 'original'
? '0 4px 14px 0 rgba(239, 68, 68, 0.35)'
: '0 4px 14px 0 rgba(16, 185, 129, 0.35)',
transition: 'all 0.3s ease',
opacity: disabled || !localIsPlaying ? 0.5 : 1,
cursor: disabled || !localIsPlaying ? 'not-allowed' : 'pointer'
}, "aria-label": `Switch to ${currentAudioType === 'original' ? 'processed' : 'original'} audio`, children: [_jsx("span", { children: currentAudioType === 'original' ? '🔴' : '🟢' }), _jsxs("span", { children: ["Playing: ", currentAudioType === 'original' ? 'Original' : 'Processed'] })] })] })), !originalAudioUrl && !processedAudioUrl && (_jsxs("div", { style: {
textAlign: 'center',
color: '#6b7280',
padding: '3rem',
background: 'rgba(0, 0, 0, 0.02)',
borderRadius: '12px',
fontSize: '14px'
}, role: "status", children: [_jsx("span", { style: { fontSize: '24px', marginBottom: '0.5rem', display: 'block' }, children: "\uD83C\uDFB5" }), "No audio files provided for comparison"] }))] }));
};