-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathindex.html
1 lines (1 loc) · 15.4 KB
/
index.html
1
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <style>*{ margin:0;padding:0;}body{ background-color:rgb(250,250,250);background-repeat:no-repeat;background-position:center;background-size:90%;font-family:"Ubuntu", "Calibri", "Sitka", sans-serif;}p, input{ margin-bottom:3px;margin-right:3px;margin-left:3px;}input[type=button]{ padding:5px 20px 5px 20px;margin-top:10px;}.page-wrapper{ display:flex;flex-direction:column;align-items:center;justify-content:center;margin-bottom:20px;margin-top:40px;}.page-wrapper > h1{ color:#333;font-size:24px;margin-bottom:10px;}.page-wrapper > h2{ color:#333;font-size:15px;margin-bottom:10px;}.page-wrapper > footer{ display:flex;flex-direction:column;align-items:center;margin-top:30px;}.page-wrapper > footer > p{ font-size:0.8rem;color:#666;}.fx-notice{ font-size:0.7rem;color:#A00;display:none;}.drag-and-drop-area{ display:flex;flex-direction:column;align-items:center;justify-content:center;width:300px;height:100px;background-color:#0003;border-radius:10px;border:dashed 4px gray;margin-bottom:20px;margin-top:20px;}.drag-and-drop-area-highlight{ background-color:#F006;}.wav-settings{ display:flex;flex-direction:row;align-items:center;margin-bottom:20px;}.wav-settings > label{ margin-right:5px;}.wav-settings > select{ margin-right:10px;}.file-table{ min-width:400px;max-width:700px;}.file-table > tbody > tr > td{ padding-bottom:2px;padding-right:5px;} .file-table > tbody > tr :nth-child(2){ white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:left;max-width:400px;} .file-table > tbody > tr :nth-child(5){ position:absolute;}.file-table > tbody > tr :nth-child(5)::before{ content:"\2716";font-size:1rem;} .file-table > tbody > tr :nth-child(6){ position:absolute;margin-top:-7px;margin-left:18px;font-size:1.5rem;cursor:default;user-select:none;}.custom-file-input::before{ content:'open';display:inline-block;background:-webkit-linear-gradient(top, #f9f9f9, #e3e3e3);border:1px solid #999;border-radius:2px;padding:1px 8px;outline:none;white-space:nowrap;-webkit-user-select:none;cursor:default;text-shadow:1px 1px #fff;margin-top:5px;}.custom-file-input:hover::before{ border-color:black;}.custom-file-input:active::before{ background:-webkit-linear-gradient(top, #e3e3e3, #f9f9f9);}.overlay-wait{ visibility:collapse;display:flex;flex-direction:column;justify-content:center;background-color:#0008;position:fixed;width:100%;height:100%;z-index:1000;top:0px;left:0px;text-align:center;}.overlay-wait::before{ font-weight:bold;color:rgb(250,250,250);font-size:25px;content:"Processing your files. Please wait...";}</style> </head> <title>MP3 to WAV converter</title> <body> <div class="overlay-wait"></div> <div class="page-wrapper"> <h1>MP3 to WAV converter</h1> <h2>Works offline*. Processes files in bulk. Accepts most audio formats.</h2> <p class="fx-notice">Consider using this converter in <a href="https://www.mozilla.org/en-US/firefox/new/" target="_blank">Firefox</a>.</p> <p class="fx-notice">Firefox provides much better resampling quality, which especially noticeable on low sample rates (e.g. 8000).</p> <div class="drag-and-drop-area"> Darg & drop audio files here or <label class="custom-file-input" for="file-input"></label> <input id="file-input" type="file" multiple="multiple" accept="audio/*" style="display:none"> </div> <div class="wav-settings"> <label for="wav-bit-depth">Bit-depth</label> <select id="wav-bit-depth"> <option value="16" selected>16</option> <option value="8">8</option> </select> <label for="wav-channels">Channels</label> <select id="wav-channels"> <option value="both" selected>2</option> <option value="mix">1 - mix</option> <option value="left">1 - left</option> <option value="right">1 - right</option> </select> <label for="wav-sample-rate">Sample rate</label> <select id="wav-sample-rate"> <option value="48000" selected>48000</option> <option value="44100">44100</option> <option value="32000">32000</option> <option value="28000">28000</option> <option value="24000">24000</option> <option value="20000">20000</option> <option value="16000">16000</option> <option value="12000">12000</option> <option value="10000">10000</option> <option value="8000">8000</option> </select> </div> <table class="file-table"> <tbody id="file-table-body"> </tbody> </table> <div> <input id="clear-button" type="button" value="Clear" disabled> <input id="save-button" type="button" value="Convert and save" disabled> </div> <footer> <p>* just Ctrl+S this html page and use it offline!</p> <p>© 2020 <a href="https://github.com/AlexIII/web-wav-converter" target="_blank">github.com/AlexIII</a></p> </footer> </div> <script>"use strict";var __awaiter=this&&this.__awaiter||function(thisArg,_arguments,P,generator){return new(P||(P=Promise))((function(resolve,reject){function fulfilled(value){try{step(generator.next(value))}catch(e){reject(e)}}function rejected(value){try{step(generator.throw(value))}catch(e){reject(e)}}function step(result){var value;result.done?resolve(result.value):(value=result.value,value instanceof P?value:new P((function(resolve){resolve(value)}))).then(fulfilled,rejected)}step((generator=generator.apply(thisArg,_arguments||[])).next())}))};const dragAndDropArea=document.querySelector(".drag-and-drop-area"),fileInputElem=document.querySelector("#file-input"),fileTableBodyElem=document.querySelector("#file-table-body"),saveButton=document.querySelector("#save-button"),clearButton=document.querySelector("#clear-button"),sampleRateInput=document.querySelector("#wav-sample-rate"),bitDepthInput=document.querySelector("#wav-bit-depth"),channelsInput=document.querySelector("#wav-channels"),waitOverlayElem=document.querySelector(".overlay-wait"),isFirefox=navigator.userAgent.toLowerCase().indexOf("firefox")>-1;if(document.querySelectorAll(".fx-notice").forEach(el=>el.style.display=isFirefox?"none":"block"),window.location.hash){const initVals=window.location.hash.substr(1).split("&"),bitDepth=~~initVals[0],channels=initVals[1],sampleRate=~~initVals[2];8!==bitDepth&&16!==bitDepth||(bitDepthInput.value=String(bitDepth)),"both"!==channels&&"left"!==channels&&"right"!==channels&&"mix"!==channels||(channelsInput.value=channels),(sampleRate>=8e3||sampleRate<=64e3)&&(sampleRateInput.value=String(sampleRate))}const waitOverlay=isOn=>{waitOverlayElem.style.visibility=isOn?"visible":"collapse"},initDragAndDropArea=(elem,highlightClassName,ondrop)=>{elem.ondrop=ev=>{var _a,_b,_c;if(elem.classList.remove(highlightClassName),ev.preventDefault(),null===(_a=ev.dataTransfer)||void 0===_a?void 0:_a.items){const files=Array.from(ev.dataTransfer.items).filter(it=>"file"===it.kind).map(it=>it.getAsFile());files.length&&ondrop(files)}else if(null===(_b=ev.dataTransfer)||void 0===_b?void 0:_b.files){const files=Array.from(null===(_c=ev.dataTransfer)||void 0===_c?void 0:_c.files);files.length&&ondrop(files)}},elem.ondragover=ev=>{ev.preventDefault()},elem.ondragenter=()=>elem.classList.add(highlightClassName),elem.ondragleave=()=>elem.classList.remove(highlightClassName)},initOpenFiles=onopen=>{var elem,highlightClassName,ondrop;highlightClassName="drag-and-drop-area-highlight",ondrop=onopen,(elem=dragAndDropArea).ondrop=ev=>{var _a,_b,_c;if(elem.classList.remove(highlightClassName),ev.preventDefault(),null===(_a=ev.dataTransfer)||void 0===_a?void 0:_a.items){const files=Array.from(ev.dataTransfer.items).filter(it=>"file"===it.kind).map(it=>it.getAsFile());files.length&&ondrop(files)}else if(null===(_b=ev.dataTransfer)||void 0===_b?void 0:_b.files){const files=Array.from(null===(_c=ev.dataTransfer)||void 0===_c?void 0:_c.files);files.length&&ondrop(files)}},elem.ondragover=ev=>{ev.preventDefault()},elem.ondragenter=()=>elem.classList.add(highlightClassName),elem.ondragleave=()=>elem.classList.remove(highlightClassName),fileInputElem.onchange=()=>{fileInputElem.files&&(onopen(Array.from(fileInputElem.files)),fileInputElem.value="")}},makeFileTableRows=(files,playing,stats)=>files.map((f,idx)=>`<tr> \n <td>${idx+1}</td> \n <td>${f.name}</td> \n <td>${Math.round(stats[idx].duration/60)}:${Math.round(stats[idx].duration%60)}</td> \n <td>${(stats[idx].inSize/1024/1024).toFixed(1)}Mbyte -> ${(stats[idx].outSize/1024/1024).toFixed(1)}Mbyte</td> \n <td onclick="removeFileButtonHandler(${idx});"></td> \n <td onclick="playPauseButtonHandler(${idx});">${idx!==playing?"⏵":"⏸"}</td> \n </tr>`).join("\n");class AudioFilesProcessor{constructor(){this.files=[],this.playing=null,this.stopPlaying=null,globalThis.removeFileButtonHandler=this.remove.bind(this),globalThis.playPauseButtonHandler=this.playPause.bind(this),saveButton.onclick=()=>__awaiter(this,void 0,void 0,(function*(){this.files.length&&(waitOverlay(!0),yield Promise.all(this.files.map(f=>this.convertAndSaveAudioBuffer(f.audioBuffer,f.file.name.replace(/\.[0-9a-z]+$/i,".wav")))),waitOverlay(!1))})),clearButton.onclick=()=>{this.files=[],null!==this.playing&&this.playPause(this.playing,!1),this.updateUI()},sampleRateInput.onchange=bitDepthInput.onchange=channelsInput.onchange=()=>{null!==this.playing&&this.playPause(this.playing,!1),this.updateUI()}}add(files){return __awaiter(this,void 0,void 0,(function*(){const tId=setTimeout(()=>waitOverlay(!0),300),filesToAdd=files.filter(f=>!this.files.map(({file:file})=>file.name).includes(f.name));this.files.push(...yield Promise.all(filesToAdd.map(file=>__awaiter(this,void 0,void 0,(function*(){return{file:file,audioBuffer:yield(new AudioContext).decodeAudioData(yield file.arrayBuffer())}}))))),this.files.sort((a,b)=>a.file.name.localeCompare(b.file.name)),clearTimeout(tId),waitOverlay(!1),this.updateUI()}))}remove(index){null!==this.playing&&this.playPause(this.playing,!1),this.files.splice(index,1),this.updateUI()}playPause(index,updateUI=!0){this.playing===index?this.stopPlaying&&(this.stopPlaying(),this.stopPlaying=null,this.playing=null):(null!==this.playing&&this.playPause(this.playing,!1),this.playing=index,this.playAudioBuffer(this.files[index].audioBuffer)),updateUI&&this.updateUI()}playAudioBuffer(audioBufferIn){return __awaiter(this,void 0,void 0,(function*(){const targetOptions=this.getTargetOptions(),audioCtx=new AudioContext,audioBuffer=yield processAudioFile(audioBufferIn,targetOptions.channelOpt,targetOptions.sampleRate),song=audioCtx.createBufferSource();song.buffer=audioBuffer,song.connect(audioCtx.destination),song.start(),this.stopPlaying=()=>song.stop(),song.onended=()=>null!==this.playing&&this.playPause(this.playing)}))}convertAndSaveAudioBuffer(audioBufferIn,saveFileName){return __awaiter(this,void 0,void 0,(function*(){const targetOptions=this.getTargetOptions(),audioBuffer=yield processAudioFile(audioBufferIn,targetOptions.channelOpt,targetOptions.sampleRate),rawData=audioToRawWave("both"===targetOptions.channelOpt?[audioBuffer.getChannelData(0),audioBuffer.getChannelData(1)]:[audioBuffer.getChannelData(0)],targetOptions.bytesPerSample),blob=makeWav(rawData,"both"===targetOptions.channelOpt?2:1,targetOptions.sampleRate,targetOptions.bytesPerSample);saveAs(blob,saveFileName)}))}updateUI(){return __awaiter(this,void 0,void 0,(function*(){const targetOptions=this.getTargetOptions();var files,playing,stats;fileTableBodyElem.innerHTML=(files=this.files.map(({file:file})=>file),playing=this.playing,stats=this.files.map(({audioBuffer:audioBuffer,file:file})=>({duration:audioBuffer.duration,inSize:file.size,outSize:audioBuffer.length/audioBuffer.sampleRate*targetOptions.sampleRate*targetOptions.bytesPerSample*("both"===targetOptions.channelOpt?2:1)})),files.map((f,idx)=>`<tr> \n <td>${idx+1}</td> \n <td>${f.name}</td> \n <td>${Math.round(stats[idx].duration/60)}:${Math.round(stats[idx].duration%60)}</td> \n <td>${(stats[idx].inSize/1024/1024).toFixed(1)}Mbyte -> ${(stats[idx].outSize/1024/1024).toFixed(1)}Mbyte</td> \n <td onclick="removeFileButtonHandler(${idx});"></td> \n <td onclick="playPauseButtonHandler(${idx});">${idx!==playing?"⏵":"⏸"}</td> \n </tr>`).join("\n")),clearButton.disabled=saveButton.disabled=!this.files.length}))}getTargetOptions(){return{sampleRate:Math.round(Number(sampleRateInput.value)),bytesPerSample:8===Math.round(Number(bitDepthInput.value))?1:2,channelOpt:channelsInput.value}}}const audioFilesProcessor=new AudioFilesProcessor;initOpenFiles(files=>{const audioFiles=files.filter(f=>f.type.startsWith("audio/"));audioFiles.length&&audioFilesProcessor.add(audioFiles)});const saveAs=(blob,fileName)=>{var a=document.createElement("a");document.body.appendChild(a),a.style.display="none";const url=window.URL.createObjectURL(blob);a.href=url,a.download=fileName,a.click(),setTimeout(()=>window.URL.revokeObjectURL(url),1e3)},audioResample=(buffer,sampleRate)=>{const offlineCtx=new OfflineAudioContext(2,buffer.length/buffer.sampleRate*sampleRate,sampleRate),source=offlineCtx.createBufferSource();return source.buffer=buffer,source.connect(offlineCtx.destination),source.start(),offlineCtx.startRendering()},audioReduceChannels=(buffer,targetChannelOpt)=>{if("both"===targetChannelOpt||buffer.numberOfChannels<2)return buffer;const outBuffer=new AudioBuffer({sampleRate:buffer.sampleRate,length:buffer.length,numberOfChannels:1}),data=[buffer.getChannelData(0),buffer.getChannelData(1)],newData=new Float32Array(buffer.length);for(let i=0;i<buffer.length;++i)newData[i]="left"===targetChannelOpt?data[0][i]:"right"===targetChannelOpt?data[1][i]:(data[0][i]+data[1][i])/2;return outBuffer.copyToChannel(newData,0),outBuffer},audioNormalize=buffer=>{const data=Array.from(Array(buffer.numberOfChannels)).map((_,idx)=>buffer.getChannelData(idx)),maxAmplitude=Math.max(...data.map(chan=>chan.reduce((acc,cur)=>Math.max(acc,Math.abs(cur)),0)));if(maxAmplitude>=1)return buffer;const coeff=1/maxAmplitude;return data.forEach(chan=>{chan.forEach((v,idx)=>chan[idx]=v*coeff),buffer.copyToChannel(chan,0)}),buffer},processAudioFile=(audioBufferIn,targetChannelOpt,targetSampleRate)=>__awaiter(void 0,void 0,void 0,(function*(){const resampled=yield audioResample(audioBufferIn,targetSampleRate),reduced=audioReduceChannels(resampled,targetChannelOpt);return audioNormalize(reduced)})),audioToRawWave=(audioChannels,bytesPerSample,mixChannels=!1)=>{const bufferLength=audioChannels[0].length,numberOfChannels=1===audioChannels.length?1:2,reducedData=new Uint8Array(bufferLength*numberOfChannels*bytesPerSample);for(let i=0;i<bufferLength;++i)for(let channel=0;channel<(mixChannels?1:numberOfChannels);++channel){const outputIndex=(i*numberOfChannels+channel)*bytesPerSample;let sample;switch(sample=mixChannels?audioChannels.reduce((prv,cur)=>prv+cur[i],0)/numberOfChannels:audioChannels[channel][i],sample=sample>1?1:sample<-1?-1:sample,bytesPerSample){case 2:sample*=32767,reducedData[outputIndex]=sample,reducedData[outputIndex+1]=sample>>8;break;case 1:reducedData[outputIndex]=127*(sample+1);break;default:throw"Only 8, 16 bits per sample are supported"}}return reducedData},makeWav=(data,channels,sampleRate,bytesPerSample)=>{var wav=new Uint8Array(44+data.length),view=new DataView(wav.buffer);return view.setUint32(0,1380533830,!1),view.setUint32(4,36+data.length,!0),view.setUint32(8,1463899717,!1),view.setUint32(12,1718449184,!1),view.setUint32(16,16,!0),view.setUint16(20,1,!0),view.setUint16(22,channels,!0),view.setUint32(24,sampleRate,!0),view.setUint32(28,sampleRate*bytesPerSample*channels,!0),view.setUint16(32,bytesPerSample*channels,!0),view.setUint16(34,8*bytesPerSample,!0),view.setUint32(36,1684108385,!1),view.setUint32(40,data.length,!0),wav.set(data,44),new Blob([wav.buffer],{type:"audio/wav"})};</script> </body> </html>