@@ -12,6 +12,11 @@ import {
1212 type ModelSource ,
1313} from "./lib/audio/models" ;
1414import type { SeparateRequest } from "./lib/audio/separate" ;
15+ import {
16+ createStemArchive ,
17+ downloadBlob ,
18+ toStemArchiveFilename ,
19+ } from "./lib/audio/stem-archive" ;
1520import { encodeWavF32 } from "./lib/audio/wav" ;
1621import { separateInWorker } from "./lib/audio/worker-client" ;
1722import { loadPreferences , savePreferences } from "./lib/preferences" ;
@@ -104,14 +109,9 @@ export function App() {
104109 : "" ;
105110
106111 const handleAudioFileMutation = useMutation ( {
107- mutationFn : async ( file : File | undefined ) => {
108- if ( file ) {
109- return decodeAudioFile ( file ) ;
110- }
111- return null ;
112- } ,
112+ mutationFn : decodeAudioFile ,
113113 } ) ;
114- const decodedAudio = handleAudioFileMutation . data ?? null ;
114+ const decodedAudio = handleAudioFileMutation . data ;
115115
116116 const [ runProgress , setRunProgress ] = useState < RunProgress | null > ( null ) ;
117117
@@ -156,12 +156,22 @@ export function App() {
156156 [ output . left , output . right ] ,
157157 AUDIO_SAMPLE_RATE ,
158158 ) ;
159- return { ...output , url : URL . createObjectURL ( blob ) } ;
159+ return { ...output , blob , url : URL . createObjectURL ( blob ) } ;
160160 } ) ;
161161 outputCleanupRef . current = nextOutputs . map (
162162 ( output ) => ( ) => URL . revokeObjectURL ( output . url ) ,
163163 ) ;
164- return { outputs : nextOutputs , durationMs : performance . now ( ) - started } ;
164+ const durationMs = performance . now ( ) - started ;
165+ const archiveBlob = await createStemArchive ( nextOutputs ) ;
166+ const archive = {
167+ name : toStemArchiveFilename ( decodedAudio . name ) ,
168+ url : URL . createObjectURL ( archiveBlob ) ,
169+ } ;
170+ outputCleanupRef . current . push ( ( ) => URL . revokeObjectURL ( archive . url ) ) ;
171+ return { outputs : nextOutputs , archive, durationMs } ;
172+ } ,
173+ onSuccess : ( { archive } ) => {
174+ downloadBlob ( archive . url , archive . name ) ;
165175 } ,
166176 onSettled : ( _data , error ) => {
167177 if ( error ) {
@@ -218,9 +228,12 @@ export function App() {
218228 type = "file"
219229 id = "file"
220230 accept = "audio/*"
221- onChange = { ( event ) =>
222- handleAudioFileMutation . mutate ( event . target . files ?. [ 0 ] )
223- }
231+ onChange = { ( event ) => {
232+ const file = event . target . files ?. [ 0 ] ;
233+ if ( file ) {
234+ handleAudioFileMutation . mutate ( file ) ;
235+ }
236+ } }
224237 />
225238 { audioFileStatusText && (
226239 < p className = "text-muted mt-3.5 text-sm" id = "audio-status" >
@@ -304,10 +317,10 @@ export function App() {
304317 }
305318 >
306319 < option value = "" > off</ option >
320+ < option > vocals</ option >
307321 < option > drums</ option >
308322 < option > bass</ option >
309323 < option > other</ option >
310- < option > vocals</ option >
311324 </ select >
312325 </ div >
313326 < div className = "grid gap-2" >
@@ -441,16 +454,25 @@ export function App() {
441454 className = "bg-surface shadow-card min-w-0 rounded-lg border px-5 py-6 sm:p-9"
442455 aria-labelledby = "results-title"
443456 >
444- < div className = "mb-7" >
445- < p className = "text-primary-strong mb-2.5 text-xs font-extrabold tracking-[0.14em] uppercase" >
446- Separation complete
447- </ p >
448- < h2
449- className = "text-3xl font-semibold tracking-[-0.025em]"
450- id = "results-title"
457+ < div className = "mb-7 flex items-end justify-between gap-4" >
458+ < div >
459+ < p className = "text-primary-strong mb-2.5 text-xs font-extrabold tracking-[0.14em] uppercase" >
460+ Separation complete
461+ </ p >
462+ < h2
463+ className = "text-3xl font-semibold tracking-[-0.025em]"
464+ id = "results-title"
465+ >
466+ Your stems
467+ </ h2 >
468+ </ div >
469+ < a
470+ className = "text-primary hover:text-accent shrink-0 text-sm font-semibold underline underline-offset-3"
471+ href = { handleRunMutation . data ?. archive . url }
472+ download = { handleRunMutation . data ?. archive . name }
451473 >
452- Your stems
453- </ h2 >
474+ Download ZIP
475+ </ a >
454476 </ div >
455477 < div className = "grid gap-3.5" id = "stems" >
456478 { outputs . map ( ( output ) => (
0 commit comments