$ mkdir js
$ curl -LO https://github.com/mmig/libflac.js/archive/refs/tags/5.4.0.tar.gz
$ tar xvzf 5.4.0.tar.gz
$ cp libflac.js-5.4.0/dist/libflac.wasm.* ./js
Japanese blog: https://qiita.com/mbotsu/items/1e8cdf67363aed0c09df
$ mkdir js
$ curl -LO https://github.com/mmig/libflac.js/archive/refs/tags/5.4.0.tar.gz
$ tar xvzf 5.4.0.tar.gz
$ cp libflac.js-5.4.0/dist/libflac.wasm.* ./js
Japanese blog: https://qiita.com/mbotsu/items/1e8cdf67363aed0c09df
| from subprocess import CalledProcessError, run | |
| import numpy as np | |
| import soundfile as sf | |
| SAMPLE_RATE = 16000 | |
| def load_audio(file: str, sr: int = SAMPLE_RATE): | |
| cmd = [ | |
| "ffmpeg", | |
| "-nostdin", | |
| "-threads", "0", | |
| "-i", file, | |
| "-f", "s16le", | |
| "-ac", "1", | |
| "-acodec", "pcm_s16le", | |
| "-ar", str(sr), | |
| "-" | |
| ] | |
| try: | |
| out = run(cmd, capture_output=True, check=True).stdout | |
| except CalledProcessError as e: | |
| raise RuntimeError(f"Failed to load audio: {e.stderr.decode()}") from e | |
| return np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0 | |
| res = load_audio("output.flac") | |
| print(res.shape) | |
| print(res.dtype) | |
| filepath = "output.wav" | |
| sf.write(filepath, res, SAMPLE_RATE) |
| <html lang="ja"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>flac convert test</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <script src="js/libflac.wasm.js" type="text/javascript"></script> | |
| </head> | |
| <body> | |
| <script> | |
| let audioCtx; | |
| let message; | |
| async function main() { | |
| message = document.getElementById("message"); | |
| if (!audioCtx) { | |
| audioCtx = new AudioContext({ sampleRate: 16000 }); | |
| await loadAudio(); | |
| } | |
| } | |
| main(); | |
| const reader = new FileReader(); | |
| reader.addEventListener("progress", readerEvent); | |
| function readerEvent(event) { | |
| message.innerText = "fileReading: " + event.loaded; | |
| } | |
| // Debug | |
| async function loadAudio(){ | |
| try { | |
| const name = "test.mp4"; | |
| // const name = "test_long.mp4"; | |
| const response = await fetch(name); | |
| decodeAudioData(await response.arrayBuffer()); | |
| } catch (err) { | |
| decodeError(err); | |
| } | |
| } | |
| function decodeAudioData(arrayBuffer){ | |
| audioCtx.decodeAudioData(arrayBuffer, transcode, decodeError); | |
| } | |
| function decodeError(error){ | |
| console.error(`Unable to fetch the audio file. Error: ${error.message}`); | |
| } | |
| async function transcode(audioBuffer){ | |
| const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext; | |
| const offlineAudioContext = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate); | |
| const merger = offlineAudioContext.createChannelMerger(audioBuffer.numberOfChannels); | |
| const source = offlineAudioContext.createBufferSource(); | |
| source.buffer = audioBuffer; | |
| for (let i = 0; i < audioBuffer.numberOfChannels; i++) { | |
| source.connect(merger, 0, i); | |
| } | |
| merger.connect(offlineAudioContext.destination); | |
| source.start(); | |
| const mixedBuffer = await offlineAudioContext.startRendering(); | |
| const float32PcmData = mixedBuffer.getChannelData(0); | |
| merger.disconnect(); | |
| source.disconnect(); | |
| // audioCtx.close(); | |
| const result = flac_encode(float32PcmData); | |
| if (result['error']){ | |
| message = document.getElementById("message"); | |
| message.innerText = result['error']; | |
| } | |
| } | |
| function flac_encode(buffer){ | |
| let flac_encoder, | |
| CHANNELS = 1, | |
| SAMPLERATE = 16000, | |
| COMPRESSION = 5, | |
| BPS = 16, | |
| VERIFY = false, | |
| BLOCK_SIZE = 0, | |
| flac_ok = 1, | |
| USE_OGG = false; | |
| const buf_length = buffer.length; | |
| let buffer_i32 = new Int32Array(buf_length); | |
| let view = new DataView(buffer_i32.buffer); | |
| const volume = 1; | |
| let index = 0; | |
| for (var i = 0; i < buf_length; i++){ | |
| view.setInt32(index, (buffer[i] * (0x7FFF * volume)), true); | |
| index += 4; | |
| } | |
| let recBuffers = []; | |
| let recLength = 0; | |
| let meta_data; | |
| function write_callback_fn(buffer, bytes, samples, current_frame){ | |
| recBuffers.push(buffer); | |
| recLength += bytes; | |
| } | |
| function metadata_callback_fn(data){ | |
| console.info('meta data: ', data); | |
| meta_data = data; | |
| } | |
| flac_encoder = Flac.create_libflac_encoder(SAMPLERATE, CHANNELS, BPS, COMPRESSION, 0, VERIFY, BLOCK_SIZE); | |
| if (flac_encoder == 0){ | |
| Flac.FLAC__stream_encoder_delete(flac_encoder); | |
| const msg = 'Error initializing the decoder.'; | |
| console.error(msg); | |
| return {error: msg, status: 1}; | |
| } | |
| try { | |
| const init_status = Flac.init_encoder_stream(flac_encoder, write_callback_fn, metadata_callback_fn); | |
| flac_ok &= init_status == 0; | |
| if (flac_ok != true){ | |
| throw new Error('Error initializing the encoder.'); | |
| } | |
| flac_return = Flac.FLAC__stream_encoder_process_interleaved(flac_encoder, buffer_i32, buffer_i32.length / 1); | |
| if (flac_return != true){ | |
| throw new Error("Error: FLAC__stream_encoder_process_interleaved returned false. " + flac_return); | |
| } | |
| flac_ok &= Flac.FLAC__stream_encoder_finish(flac_encoder); | |
| if (flac_ok != true){ | |
| throw new Error('Error Finish the encoder.'); | |
| } | |
| Flac.FLAC__stream_encoder_delete(flac_encoder); | |
| } catch(e){ | |
| console.error(e); | |
| flac_ok = Flac.FLAC__stream_encoder_get_state(flac_encoder); | |
| Flac.FLAC__stream_encoder_delete(flac_encoder); | |
| return {error: e, status: flac_ok}; | |
| } | |
| let samples = new Uint8Array(recLength); | |
| let offset = 0; | |
| recBuffers.forEach(item => { | |
| samples.set(item, offset); | |
| offset += item.length; | |
| }); | |
| let audioBlob = URL.createObjectURL(new Blob([samples], { type: 'audio/flac' })); | |
| const downloadLink = document.getElementById('download'); | |
| downloadLink.href = audioBlob; | |
| downloadLink.download = 'output.flac'; | |
| downloadLink.style.display = 'inline'; | |
| const audio = document.getElementById('output-audio'); | |
| audio.src = audioBlob; | |
| audio.style.display = 'inline'; | |
| return {error: "", status: 0} | |
| } | |
| </script> | |
| <audio id="output-audio" controls style="display: none;"></audio> | |
| <a id="download" class="btn btn-primary btn-lg" role="button" style="display: none;">Download</a> | |
| <div id="message"></div> | |
| </body> | |
| </html> |