Skip to content

Setting up a new CPAL WASM project

Marc-Andre Lureau edited this page Apr 11, 2024 · 6 revisions

This guide is for anyone looking to quickly get started with web assembly and audio using the Rust audio crate cpal. The guide does not go into specific details, rather, it provides the steps you would need to take to setup a new project from scratch. If you would like to know more about how to work with web assembly and rust, please refer to this book. After completing this guide you will be able to start and stop cpal from within a browser. This tutorial was developed and adapted from existing work by ishitatsuyuki.

  1. Install npm if you don't already have it installed on your system.

  2. Open up Terminal and navigate to a folder where you would like to create your project. Enter the following command cargo new hello_wasm --lib

  3. Open the newly created hello_wasm folder in your editor of choice. Lets setup our html and javascript code first and then finish with preparing our rust code.

  4. In the top level of your project, create a new file called package.json. This file will contain the standard npm metadata and is where you put your JavaScript dependencies. Open up the newly created file in your editor and paste the following. You must change this file with your details (author, name, version).

{
  "author": "You <you@example.com>",
  "name": "rust-webpack-template",
  "version": "0.1.0",
  "scripts": {
    "build": "rimraf dist pkg && webpack",
    "start": "rimraf dist pkg && webpack-dev-server --open",
    "test": "cargo test && wasm-pack test --headless"
  },
  "devDependencies": {
    "@wasm-tool/wasm-pack-plugin": "^1.6.0",
    "copy-webpack-plugin": "^11.0.0",
    "webpack": "^5.73.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.9.3",
    "rimraf": "^3.0.2"
  },
  "dependencies": {
    "html-webpack-plugin": "^5.5.0"
  }
}
  1. Next, create a new file called webpack.config.js. This file contains the Webpack configuration. You shouldn't need to change this, unless you have very special needs. Open up the newly created file in your editor and paste the following.
const path = require("path");
const CopyPlugin = require("copy-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const dist = path.resolve(__dirname, "dist");

module.exports = {
  mode: "production",
  entry: {
    index: "./index.js"
  },
  output: {
    path: dist,
    filename: "[name].js"
  },
  experiments: {
    syncWebAssembly: true
  },
  devServer: {
    static: {
      directory: dist
    },
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: 'index.html'
    }),
    new WasmPackPlugin({
      crateDirectory: __dirname,
      outName: "index"
    }),
  ]
};
  1. Create a new file called index.html. We will create 2 simple buttons in order to start and stop cpal from playing in the browser. Copy the following and save.
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>cpal beep example</title>
  </head>
  <body>
    <input id="play" type="button" value="beep"/>
    <input id="stop" type="button" value="stop"/>
  </body>
</html>
  1. Create a new file called index.js. This file acts as the glue between our html button elements and calling functions in rust. In this instance, when the play button is clicked, our rust function called beep() is called. Copy the following and save.
import("./pkg").catch(console.error).then(rust_module=>{
    let handle = null;
    const play_button = document.getElementById("play");
    play_button.addEventListener("click", event => {
        handle = rust_module.beep();
    });
    const stop_button = document.getElementById("stop");
    stop_button.addEventListener("click", event => {
        if (handle != null) {
            handle.free();
	        handle = null;
        }
    });
});
  1. Its now time to setup our rust project. Navigate to the generated Cargo.toml file. This file contains the standard Rust metadata and is where the Rust dependencies for your project are set. Add the following and save.
...

[lib]
crate-type = ["cdylib"]

[profile.release]
# This makes the compiled code faster and smaller, but it makes compiling slower,
# so it's only enabled in release mode.
lto = true

[features]
# If you uncomment this line, it will enable `wee_alloc`:
#default = ["wee_alloc"]

[dependencies]
cpal = { version = "0.15", features = ["wasm-bindgen"] }
# The `wasm-bindgen` crate provides the bare minimum functionality needed
# to interact with JavaScript.
wasm-bindgen = "0.2.45"

# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
# compared to the default allocator's ~10K. However, it is slower than the default
# allocator, so it's not enabled by default.
wee_alloc = { version = "0.4.2", optional = true }

# The `web-sys` crate allows you to interact with the various browser APIs,
# like the DOM.
[dependencies.web-sys]
version = "0.3.22"
features = ["console"]

# The `console_error_panic_hook` crate provides better debugging of panics by
# logging them with `console.error`. This is great for development, but requires
# all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled
# in debug mode.
[target."cfg(debug_assertions)".dependencies]
console_error_panic_hook = "0.1.5"
  1. Next, navigate to the /src folder and open lib.rs file. Copy the code below in and save.
use wasm_bindgen::prelude::*;
use web_sys::console;
use cpal::{SizedSample, FromSample};

// When the `wee_alloc` feature is enabled, this uses `wee_alloc` as the global
// allocator.
//
// If you don't want to use `wee_alloc`, you can safely delete this.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// This is like the `main` function, except for JavaScript.
#[wasm_bindgen(start)]
pub fn main_js() -> Result<(), JsValue> {
    // This provides better error messages in debug mode.
    // It's disabled in release mode so it doesn't bloat up the file size.
    #[cfg(debug_assertions)]
    console_error_panic_hook::set_once();

    Ok(())
}

use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::Stream;

#[wasm_bindgen]
pub struct Handle(Stream);

#[wasm_bindgen]
pub fn beep() -> Handle {
    let host = cpal::default_host();
    let device = host
        .default_output_device()
        .expect("failed to find a default output device");
    let config = device.default_output_config().unwrap();

    Handle(match config.sample_format() {
        cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()),
        cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()),
        cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()),
        // not all supported sample formats are included in this example
        _ => panic!("Unsupported sample format!"),
    })
}

fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Stream
where
    T: SizedSample + FromSample<f32>,
{
    let sample_rate = config.sample_rate.0 as f32;
    let channels = config.channels as usize;

    // Produce a sinusoid of maximum amplitude.
    let mut sample_clock = 0f32;
    let mut next_value = move || {
        sample_clock = (sample_clock + 1.0) % sample_rate;
        (sample_clock * 440.0 * 2.0 * 3.141592 / sample_rate).sin()
    };

    let err_fn = |err| console::error_1(&format!("an error occurred on stream: {}", err).into());

    let stream = device
        .build_output_stream(
            config,
            move |data: &mut [T], _| write_data(data, channels, &mut next_value),
            err_fn,
            None,
        )
        .unwrap();
    stream.play().unwrap();
    stream
}

fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> f32)
where
    T: SizedSample + FromSample<f32>,
{
    for frame in output.chunks_mut(channels) {
        let value: T = T::from_sample(next_sample());
        for sample in frame.iter_mut() {
            *sample = value;
        }
    }
}
  1. We now have all of the necessary files and code in place ready to go! However, before we can run our example we need to install our javascript dependencies. To do so, open a terminal, cd into your project and type the following command npm install.

  2. Finally, lets build and run our example! debug and release options are below.

How to run in debug mode

# Builds the project and opens it in a new browser tab. Auto-reloads when the project changes.
npm start

How to run in release mode

# Builds the project and places it into the `dist` folder.
npm run build
Clone this wiki locally