stem_separator / index.html
joserpagil's picture
Upload 4 files
5ecc86a verified
Raw
History Blame Contribute Delete
10.4 kB
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset = "UTF-8">
<meta name = "viewport" content = "width=device-width, initial-scale=1.0">
<title>demix & workers v250317</title>
</head>
<body style = "margin : 0;">
<input type = "file" id = "file_input" accept = "audio/mp3" disabled>
<br>
<button id = "stop_button" disabled>stop</button>
<br>
<div id = "audios_div" hidden>
vocals
<br>
<audio id = "vocals_audio" controls></audio>
<br>
instrumentals
<br>
<audio id = "instrumentals_audio" controls></audio>
<br>
</div>
<div id = "progress_div"></div>
<script>
const segment_size = 256
const fftSize = 7680
const hop = 1024
const chunk_size = hop * ( segment_size - 1 )
const trim = Math.floor( fftSize / 2 )
const generated_size = chunk_size - 2 * trim
const step = chunk_size - fftSize
let file
let started
let mix
let step_count
let ops = 0
let audio_context
let sample_rate
let converter_worker
let inference_workers
let inference_idle_workers
// inference_workers_count
// determines the number of threads
// larger values operates faster
// but the cpu can overheat
let inference_workers_count = 2
let model_array_buffer
let mix_specs
let model_specs
let audios
let vocals
let instrumentals
// called everytime progress is reported
const progress =( percent_completed )=>{
progress_div.innerHTML += "progress = " + Math.floor( percent_completed ) + "%<br>"
}
// called when all operations are complete
const complete = args =>{
file_input.disabled = false
stop_button.disabled = true
audios_div.hidden = false
progress_div.innerHTML += "audio separation completed in " + (( Date.now() - started )/ 1000 ).toFixed( 0 ) + " seconds.<br>"
}
const create_workers =() =>{
converter_worker = new Worker( "converter_worker.js" )
converter_worker.onmessage = function( event ){
switch( event.data.selector ){
case "init" : break
case "audio_2_spec":{
update_progress()
const index = event.data.index
mix_specs[ index ].value = event.data.spec
mix_specs[ index ].state = "complete"
progress_div.innerHTML += "audio_2_spec complete, " + index + " => " + (( Date.now() - started )/ 1000 ).toFixed( 1 ) + "<br>"
schedule()
} break
case "spec_2_audio":{
update_progress()
const index = event.data.index
audios[ 0 ][ index ].value = event.data.audio[ 0 ]
audios[ 0 ][ index ].state = "complete"
audios[ 1 ][ index ].value = event.data.audio[ 1 ]
audios[ 1 ][ index ].state = "complete"
progress_div.innerHTML += "spec_2_audio complete, " + index + " => " + (( Date.now() - started )/ 1000 ).toFixed( 1 ) + "<br>"
schedule()
} break
}
}
converter_worker.postMessage({ selector : "init" })
inference_workers =[]
inference_idle_workers =[]
for( let i = 0; i < inference_workers_count; ++i ){
const inference_worker = new Worker( "inference_worker.js" )
inference_worker.onmessage = function( event ){
switch( event.data.selector ){
case "init" : break
case "run_model":{
update_progress()
const index = event.data.index
model_specs[ index ].value = event.data.spec
model_specs[ index ].state = "complete"
inference_idle_workers.push( inference_workers[ event.data.id ])
progress_div.innerHTML += "model_run complete, " + index + " => " + (( Date.now() - started )/ 1000 ).toFixed( 1 ) + "<br>"
schedule()
} break
}
}
inference_worker.postMessage({ selector : "init", id : i, model_array_buffer })
inference_workers.push( inference_worker )
}
inference_idle_workers = inference_workers.slice()
}
const construct =() =>{
const vocals_size = mix[ 0 ].length
vocals = [ new Float32Array( vocals_size ), new Float32Array( vocals_size )]
instrumentals = [ new Float32Array( vocals_size ), new Float32Array( vocals_size )]
let offset = 0
for( const audio of audios[ 0 ] ){
for( const [ index, value ] of audio.value.entries()){
vocals[ 0 ][ index + offset ] += value * .095
instrumentals[ 0 ][ index + offset ] = mix[ 0 ][ index + offset ] - vocals[ 0 ][ index + offset ]
}
offset += audio.value.length - trim * 4
}
vocals_audio.src = URL.createObjectURL( array_2_wav( vocals[ 0 ], sample_rate ))
instrumentals_audio.src = URL.createObjectURL( array_2_wav( instrumentals[ 0 ], sample_rate ))
complete()
}
const schedule =() =>{
let completed = 0
for( let i = 0; i < audios[ 0 ].length; ++i )
if( audios[ 0 ][ i ].state == "complete" )
++completed
if( completed == step_count ){
construct()
return
}
// scan for pending inferences
for( let i = 0; i < model_specs.length; ++i )
if( model_specs[ i ].state == "pending" && mix_specs[ i ].state == "complete" && inference_idle_workers.length ){
const inference_worker = inference_idle_workers[ 0 ]
inference_idle_workers.shift()
model_specs[ i ].state = "wip"
inference_worker.postMessage({ selector : "run_model", index : i, spec : mix_specs[ i ].value })
progress_div.innerHTML += "run_model, index = " + i + "<br>"
break
}
// scan for pending mix_specs
for( let i = 0; i < mix_specs.length; ++i )
if( mix_specs[ i ].state == "pending" ){
mix_specs[ i ].state = "wip"
converter_worker.postMessage({ selector : "audio_2_spec", index : i, audio : get_mix_part( i * step )})
progress_div.innerHTML += "audio_2_spec, index = " + i + "<br>"
return
}
// scan for pending audios
for( let i = 0; i < audios[ 0 ].length; ++i )
if( audios[ 0 ][ i ].state == "pending" && model_specs[ i ].state == "complete" ){
audios[ 0 ][ i ].state = "wip"
converter_worker.postMessage({ selector : "spec_2_audio", index : i, spec : model_specs[ i ].value })
progress_div.innerHTML += "spec_2_audio, index = " + i + "<br>"
break
}
}
const get_mix_part = index =>{
const end = Math.min( index + chunk_size, mix[ 0 ].length )
mix_part = mix.map( channel => channel.slice( index, end ))
if( end !== index + chunk_size )
mix_part = mix_part.map( channel => new Float32Array([ ... channel, ... new Float32Array( index + chunk_size - end )]))
return mix_part
}
const update_progress =() =>{
const percent_completed = ++ops * 100 / ( step_count * 3 )
progress ( percent_completed )
}
const array_2_wav =( audioArray, sampleRate = 44100 ) =>{
function writeString(view, offset, string){
for( let i = 0; i < string.length; ++i )
view.setUint8( offset + i, string.charCodeAt( i ))
}
const buffer = new ArrayBuffer(44 + audioArray.length * 2);
const view = new DataView(buffer);
writeString(view, 0, 'RIFF');
view.setUint32(4, 36 + audioArray.length * 2, true);
writeString(view, 8, 'WAVE');
writeString(view, 12, 'fmt ');
view.setUint32(16, 16, true);
view.setUint16(20, 1, true);
view.setUint16(22, 1, true);
view.setUint32(24, sampleRate, true);
view.setUint32(28, sampleRate * 2, true);
view.setUint16(32, 2, true);
view.setUint16(34, 16, true);
writeString(view, 36, 'data');
view.setUint32(40, audioArray.length * 2, true);
for (let i = 0; i < audioArray.length; i++) {
const sample = Math.max(-1, Math.min(1, audioArray[i]));
view.setInt16(44 + i * 2, sample * 0x7FFF, true);
}
return new Blob([ buffer ], { type: 'audio/wav' })
}
const play =( array, sample_rate = 44100 ) =>{
const buffer = audio_context.createBuffer( 1, array.length, sample_rate )
buffer.getChannelData( 0 ).set( array )
const buffer_source = audio_context.createBufferSource()
buffer_source.buffer = buffer
buffer_source.connect( audio_context.destination )
buffer_source.start ()
}
const download =( filename, array, sample_rate = 44100 )=>{
const a = document.createElement( 'a' )
a.href = URL.createObjectURL( array_2_wav( array, sample_rate ))
a.download = filename
a.click ()
}
async function start(){
started = Date.now()
progress_div.innerHTML = "starting ..." + "<br>"
file_input.disabled = true
stop_button.disabled = false
audios_div.hidden = true
const array_buffer = await file.arrayBuffer()
const audio_buffer = await audio_context.decodeAudioData( array_buffer )
sample_rate = audio_buffer.sampleRate
mix =[]
for( let channel = 0; channel < audio_buffer.numberOfChannels; ++channel ){
const channel_data = audio_buffer.getChannelData( channel )
const pad = generated_size + trim - ( channel_data.length % generated_size )
mix.push ( new Float32Array([ ... new Float32Array( trim ), ... channel_data, ... new Float32Array( pad )]))
}
step_count = Math.floor( mix[ 0 ].length / step ) + 1
audios = [ Array.from({ length : step_count }, () =>({ state : "pending", value : null })),
Array.from({ length : step_count }, () =>({ state : "pending", value : null }))]
mix_specs = Array.from({ length : step_count }, () =>({ state : "pending", value : null }))
model_specs = Array.from({ length : step_count }, () =>({ state : "pending", value : null }))
schedule()
}
( async ()=>{
audio_context = new AudioContext()
const fetched = await fetch( "UVR_MDXNET_Main.onnx" )
model_array_buffer = await fetched.arrayBuffer()
create_workers ()
file_input.disabled = false
})()
document.getElementById( 'file_input' ).addEventListener( 'change', function(){
file = file_input.files[ 0 ]
start()
})
document.getElementById( 'stop_button' ).addEventListener( 'click', function(){
stop_button.disabled = true
converter_worker.terminate()
for( const worker of inference_workers )
worker.terminate()
create_workers()
})
</script>
</body>
</html>