ruuda / hound

A wav encoding and decoding library in Rust
https://codeberg.org/ruuda/hound
Apache License 2.0
489 stars 65 forks source link

Add WASM Support. #56

Closed noahdahlman closed 2 years ago

noahdahlman commented 2 years ago

My understanding is that we need to strip out certain libraries that are unavailable with wasm bindings such as fs. Any idea what else we could remove for this? I want to be able to quickly encode/decode/process wav files in the browser. Does anyone else think wasm support would be a worthwhile contribution?

ruuda commented 2 years ago

I’m not familiar with wasm compilation, but there is nothing in Hound that inherently needs access to std::fs, the functions work with generic io::{Read, Write}. As a convenience, there are WavReader::open and WavWriter::{create, append} and those use st::fs.

Does the entire std::fs module not exist when compiling for wasm? Or does it cause an error at link time if you try to use one of its functions? If it’s the latter, no changes should be needed. If it’s the former, I suppose you can just delete the imports of std::fs and everything that breaks because of it.

noahdahlman commented 2 years ago

Seems like the crate type needs to be cdylib , looking at how to do this or a work around. It has to be a dynamic lib since it's compiling to another language but setting the crate type to cdylib breaks tests.

As for the std::fs module, I should be able to exclude those with a macro / avoid them with the functions that get exported to wasm.

austintheriot commented 2 years ago

I was able to get this library compiling and working for wasm as-is in a Rust project I'm currently working on (only tested encoding and then downloading the encoded audio). Instead of using WavWriter::create and trying to access the file system, I wrote the data into a buffer like so:

let mut bytes = Vec::new();
let mut bytes_cursor = Cursor::new(&mut bytes);
let mut wav_writer = WavWriter::new(&mut bytes_cursor, spec).unwrap();
// ... [omitted. Here I write sample data into the `bytes` buffer using the `wav_writer` just like the example]
wav_writer.finalize().unwrap();

and then downloaded the raw binary as a file in JavaScript by converting the Vec to a blob:

use wasm_bindgen::JsCast;
use web_sys::{Blob, HtmlAnchorElement, Url};

/// Downloads raw slice of bytes as a file from the browser
pub fn download_bytes(bytes: impl AsRef<[u8]>, file_name: &str) {
    let bytes = bytes.as_ref();
    // make all wasm memory allocations at the beginning of the function
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    let body = document.body().unwrap();
    let a: HtmlAnchorElement = document.create_element("a").unwrap().dyn_into().unwrap();
    a.style().set_css_text("display: none;");
    a.set_download(file_name);
    body.append_child(&a).unwrap();

    // data must be passed to blob constructor inside of a javascript array
    let blob_parts = js_sys::Array::new_with_length(1);

    // it is unsafe to get a raw view into WebAssembly memory, but because this memory gets imemdiately
    // used, downloaded, and then view is discarded, it is safe so long as no new allocations are 
    // made in between acquiring the view and using it
    let u8_view = unsafe { js_sys::Uint8Array::view(bytes) };
    blob_parts.set(0, u8_view.dyn_into().unwrap());

    // create blob from raw view into wasm linear memory
    let blob =
        Blob::new_with_buffer_source_sequence(&blob_parts.as_ref()).unwrap();

    // make blob downloadable by creating a global document url for the blob resource
    let url = Url::create_object_url_with_blob(&blob).unwrap();

    a.set_href(&url);
    a.click();

    // release url from window memory when done to prevent memory leak
    // (this does not get released automatically, unlike most of web memory)
    Url::revoke_object_url(&url).unwrap();
}

If it would be helpful to others, I'd be happy to share more details on how to get this up and running.

Thanks for the nice library :)

ruuda commented 2 years ago

I think there is nothing to be done here then, thanks for sharing the example @austintheriot.