188 lines
7.2 KiB
TypeScript
188 lines
7.2 KiB
TypeScript
// MasteringDropdown.tsx — Mastering config for the global param bar
|
|
//
|
|
// The on/off toggle is in the bar header (ToggleSwitch).
|
|
// This dropdown shows reference track selection and options when mastering is enabled.
|
|
|
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Upload, Trash2, Music2 } from 'lucide-react';
|
|
import { useGlobalParams } from '../../context/GlobalParamsContext';
|
|
import { masteringApi } from '../../services/api';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
import { ToggleSwitch } from './BarSection';
|
|
import { formatReferenceName } from './modelLabels';
|
|
|
|
interface ReferenceTrack {
|
|
name: string;
|
|
size: number;
|
|
url: string;
|
|
}
|
|
|
|
const formatFileSize = (bytes: number): string => {
|
|
if (bytes < 1024) return `${bytes} B`;
|
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
};
|
|
|
|
export const MasteringDropdown: React.FC = () => {
|
|
const gp = useGlobalParams();
|
|
const { t } = useTranslation();
|
|
const { token } = useAuth();
|
|
const [references, setReferences] = useState<ReferenceTrack[]>([]);
|
|
const [uploading, setUploading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
masteringApi.listReferences()
|
|
.then(data => setReferences(data.references))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
const handleUpload = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file || !token) return;
|
|
try {
|
|
setUploading(true);
|
|
const result = await masteringApi.uploadReference(file, token);
|
|
gp.setMasteringReference(result.name);
|
|
const data = await masteringApi.listReferences();
|
|
setReferences(data.references);
|
|
} catch (err) {
|
|
console.error('[Mastering] Upload failed:', err);
|
|
} finally {
|
|
setUploading(false);
|
|
e.target.value = '';
|
|
}
|
|
}, [token, gp]);
|
|
|
|
const handleDelete = useCallback(async (name: string) => {
|
|
if (!token) return;
|
|
try {
|
|
await masteringApi.deleteReference(name, token);
|
|
if (gp.masteringReference === name) gp.setMasteringReference('');
|
|
const data = await masteringApi.listReferences();
|
|
setReferences(data.references);
|
|
} catch (err) {
|
|
console.error('[Mastering] Delete failed:', err);
|
|
}
|
|
}, [token, gp]);
|
|
|
|
if (!gp.masteringEnabled) {
|
|
return (
|
|
<div className="text-xs text-zinc-500 italic text-center py-2">
|
|
{t('mastering.disabled')}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{/* Reference selector */}
|
|
<div>
|
|
<label className="block text-xs font-medium text-zinc-500 uppercase tracking-wider mb-1.5">
|
|
{t('mastering.referenceTrack')}
|
|
</label>
|
|
{references.length > 0 ? (
|
|
<select
|
|
className="w-full px-3 py-2 rounded-xl bg-zinc-100 dark:bg-zinc-800 border border-zinc-300 dark:border-white/10 text-sm text-zinc-800 dark:text-zinc-200 focus:border-amber-500/50 focus:ring-1 focus:ring-amber-500/20 outline-none transition-colors cursor-pointer"
|
|
value={gp.masteringReference}
|
|
onChange={e => gp.setMasteringReference(e.target.value)}
|
|
>
|
|
<option value="">{t('mastering.selectReference')}</option>
|
|
{references.map(r => (
|
|
<option key={r.name} value={r.name}>
|
|
{r.name} ({formatFileSize(r.size)})
|
|
</option>
|
|
))}
|
|
</select>
|
|
) : (
|
|
<div className="text-xs text-zinc-500 italic px-1">
|
|
{t('mastering.noReferencesYet')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Selected reference info + delete */}
|
|
{gp.masteringReference && (
|
|
<div className="flex items-center gap-2 px-2 py-1.5 rounded-lg bg-amber-500/5 border border-amber-500/10">
|
|
<Music2 size={14} className="text-amber-400 flex-shrink-0" />
|
|
<span className="text-xs text-amber-300 truncate flex-1">{gp.masteringReference}</span>
|
|
<button
|
|
onClick={() => handleDelete(gp.masteringReference)}
|
|
className="p-1 rounded hover:bg-red-500/10 text-zinc-500 hover:text-red-400 transition-colors flex-shrink-0"
|
|
title={t('mastering.deleteReference')}
|
|
>
|
|
<Trash2 size={12} />
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Upload button */}
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="file"
|
|
accept="audio/*"
|
|
id="mastering-ref-upload-bar"
|
|
className="hidden"
|
|
onChange={handleUpload}
|
|
/>
|
|
<label
|
|
htmlFor="mastering-ref-upload-bar"
|
|
className={`flex items-center gap-2 px-3 py-2 text-xs font-semibold rounded-xl border cursor-pointer transition-all ${
|
|
uploading
|
|
? 'bg-zinc-100 dark:bg-zinc-800 text-zinc-500 border-zinc-200 dark:border-white/5 cursor-wait'
|
|
: 'bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 border-zinc-300 dark:border-white/10 hover:border-amber-500/30 hover:text-amber-400'
|
|
}`}
|
|
>
|
|
{uploading ? (
|
|
<><span className="w-3 h-3 border-2 border-zinc-500 border-t-transparent rounded-full animate-spin" /> Uploading...</>
|
|
) : (
|
|
<><Upload size={14} /> {t('mastering.uploadReference')}</>
|
|
)}
|
|
</label>
|
|
</div>
|
|
|
|
{/* Timbre reference toggle */}
|
|
{gp.masteringReference && (
|
|
gp.timbreAudioPath ? (
|
|
<div className="flex items-center gap-1.5 mt-1 px-2 py-1.5 rounded-lg bg-teal-500/5 border border-teal-500/10">
|
|
<Music2 size={14} className="text-teal-400" />
|
|
<span className="text-[10px] text-teal-400">Timbre: using dedicated reference ({gp.timbreAudioPath.split(/[\\/]/).pop()})</span>
|
|
</div>
|
|
) : (
|
|
<div className="flex items-center justify-between mt-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<Music2 size={14} className="text-teal-400" />
|
|
<span className="text-sm text-zinc-600 dark:text-zinc-400">{t('mastering.alsoTimbreRef')}</span>
|
|
</div>
|
|
<ToggleSwitch checked={gp.timbreReference} onChange={gp.setTimbreReference} accentColor="amber" />
|
|
</div>
|
|
)
|
|
)}
|
|
{gp.timbreReference && gp.masteringReference && !gp.timbreAudioPath && (
|
|
<p className="text-[10px] text-zinc-600 leading-relaxed">
|
|
The reference track will be VAE-encoded and fed into the timbre conditioning pipeline,
|
|
guiding the generation's tone and texture to match the reference.
|
|
</p>
|
|
)}
|
|
|
|
{/* Info */}
|
|
<p className="text-[10px] text-zinc-600 leading-relaxed">
|
|
The generated audio will be mastered to match the RMS level, frequency spectrum,
|
|
and dynamic characteristics of the reference track.
|
|
</p>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
/** Summary badge for the Mastering section */
|
|
export const MasteringBadge: React.FC = () => {
|
|
const { masteringEnabled, masteringReference } = useGlobalParams();
|
|
if (!masteringEnabled) return null;
|
|
const refName = formatReferenceName(masteringReference);
|
|
return (
|
|
<span className="text-[10px] text-amber-400/60 font-mono truncate">
|
|
{refName || 'No ref'}
|
|
</span>
|
|
);
|
|
};
|