WebAssembly简介--第二部分

386 阅读5分钟

WebAssembly简介--第二部分

在本文的第二部分,我们将继续探索WebAssembly,在React应用程序中使用它。基本上,我们将把现有的用Rust编写的代码,编译成WebAssembly,然后把产生的.wasm模块添加到用create-react-app生成的React应用中。

正如我在之前的文章中提到的,WebAssembly非常适用于资源密集型的任务,如处理大块的数据,如数字信号处理。因此,读和写音频正是我们的例子应用的内容。

当我为这篇文章想出一个应用程序的想法时,我试图远离一些过于复杂的东西,因为WebAssembly仍然处于活跃的开发阶段,没有那么多的工具将其与前端框架(如Angular、Vue.js和React)整合在一起。因此,在写这篇文章的过程中,我进行了大量的研究、试验和错误,只是为了找到一种适合初学者的方法,在React应用程序中导入Wasm模块。幸运的是,我找到了这篇文章,它介绍了一个小型的Web应用程序,使用Web Audio APIAudioBuffer放大简单的声波。我们的示例应用程序基本上将嵌入相同的功能,并进行一些小的调整,在一个消耗WebAssembly模块的反应应用程序中。

在我们开始之前,让我们熟悉一下数字音频的世界,因为它将帮助我们理解我们的例子应用程序的逻辑。我将在文章末尾添加链接,指向一些其他资源,以便进一步探索。

传入的数字音频使用AudioBuffer 接口存储在浏览器中,作为一系列32位浮点数,范围在-11 之间,其中每个数字代表原始音源的一个样本。一旦被放入AudioBuffer,音频就会被传递到AudioBufferSourceNode 接口,由浏览器播放。就我们的例子应用程序而言,我们将从JavaScript导入生成的音频样本,由编译成WebAssembly的Rust程序放大。

一些先决条件

本文将假设以下情况。

创建Rust模块

首先,让我们通过运行这个命令来生成我们的Rust模块。

wasm-pack new audio-wasm-library

这个命令将为我们生成一个新的RustWasm项目,处理所有必要的配置,如添加Cargo.toml manifest,指定wasm-bindgen 版本,生成一个src/lib.rs 文件,我们的Rust模块将被写入其中,以及其他许多设置功能。

打开src/lib.rs ,添加以下代码。

mod utils;

use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

// Define our number of samples we handle at once
const NUMBER_OF_SAMPLES: usize = 1024;

// Create a static mutable byte buffers.
// We will use these for passing audio samples from
// javascript to wasm, and from wasm to javascript
// NOTE: global `static mut` means we will have "unsafe" code
// but for passing memory between js and wasm should be fine.
static mut INPUT_BUFFER: [u8 ; NUMBER_OF_SAMPLES] = [0; NUMBER_OF_SAMPLES];
static mut OUTPUT_BUFFER: [u8 ; NUMBER_OF_SAMPLES] = [0; NUMBER_OF_SAMPLES];

// Function to return a pointer to our
// output buffer in wasm memory
#[wasm_bindgen]
pub fn get_input_buffer_pointer() -> *const u8 {
  let pointer: *const u8;
  unsafe {
    pointer = INPUT_BUFFER.as_ptr();
  }

  return pointer;
}

// Function to return a pointer to our
// output buffer in wasm memory
#[wasm_bindgen]
pub fn get_output_buffer_pointer() -> *const u8 {
  let pointer: *const u8;
  unsafe {
    pointer = OUTPUT_BUFFER.as_ptr();
  }

  return pointer;
}

// Function to do the amplification.
// By taking the samples currently in the input buffer
// amplifying them, and placing the result in the output buffer
#[wasm_bindgen]
pub fn amplify_audio() {

  // Loop over the samples
  for i in 0..NUMBER_OF_SAMPLES {
    // Load the sample at the index
    let mut audio_sample: u8;
    unsafe {
      audio_sample = INPUT_BUFFER[i];
    }

    // Amplify the sample. All samples
    // Should be implemented as bytes.
    // Byte samples are represented as follows:
    // 127 is silence, 0 is negative max, 256 is positive max
    if audio_sample > 127 {
      let audio_sample_diff = audio_sample - 127;
      audio_sample = audio_sample + audio_sample_diff;
    } else if audio_sample < 127 {
      audio_sample = audio_sample / 2;
    }

    // Store the audio sample into our output buffer
    unsafe {
      OUTPUT_BUFFER[i] = audio_sample;
    }
  }
}

上述文件归功于这篇文章的作者,我的演示程序就是从这篇文章中得到灵感的。

这个程序定义了两个缓冲区:一个输入缓冲区将存储从JavaScript生成的原始音频样本,另一个输出缓冲区包含输入缓冲区数据的放大版本。

现在我们将把我们的Rust程序编译成WebAssembly,并生成一个JavaScript "胶水 "代码,这是一个围绕.wasm代码的包装文件,这样它就可以被导入我们的React应用程序中。

audio-wasm-library 文件夹中,运行以下命令。

wasm-pack build

如果一切按预期进行,你应该有一个pkg 文件夹,其中包含一个.wasm 模块、一个package.json 文件和一个JavaScript模块,我们将把它导入到我们的React应用程序。理想情况下,pkg 应该在npm上发布,并像其他依赖关系一样安装它。为了演示,我们将pkg 文件夹复制并粘贴到我们应用程序的根文件夹中。

最后,在pkg ,打开audio_wasm_library_bg.js ,在文件末尾添加以下代码。

export const memory = wasm.memory;

上面这行导出Wasm线性内存,使其可以从我们的应用程序中访问和改变。由于我们将在JavaScript和WebAssembly之间来回传递音频样本数据,我们必须将这些值写入Wasm内存中。

创建React应用程序

现在,我们的Wasm模块已经准备好在Web应用程序中使用,让我们去使用create-react-app boilerplate创建一个React应用程序。

npx create-react-app demo-app

默认情况下,我们的React应用程序将不支持WebAssembly。这是因为create-react-app's default webpack config不知道如何解析.wasm 文件。因此,我们将不得不对webpack配置做一些修改,以支持Wasm模块。

开箱后,create-react-app不弹出的情况下不会暴露其webpack配置文件。幸运的是,有一个react-app-rewirednpm 包,允许我们在不弹出的情况下修改create-react-app的 webpack 配置。

在我们的react应用程序中,安装以下包作为dev dependencies。

npm install react-app-rewired wasm-loader -D

在我们应用程序的根部,创建一个名为config-overrides.js 的文件,作为react-app-rewired 的入口点。

const path = require('path');

module.exports = function override(config, env) {
  const wasmExtensionRegExp = /\.wasm$/;

  config.resolve.extensions.push('.wasm');

  config.module.rules.forEach(rule => {
    (rule.oneOf || []).forEach(oneOf => {
      if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) {
        // make file-loader ignore WASM files
        oneOf.exclude.push(wasmExtensionRegExp);
      }
    });
  });

  // add a dedicated loader for WASM
  config.module.rules.push({
    test: wasmExtensionRegExp,
    include: path.resolve(__dirname, 'src'),
    use: [{ loader: require.resolve('wasm-loader'), options: {} }]
  });

  return config;
};

上述配置文件来自Github上的这个问题。它也是为了实现使用WebAssembly与create-react-app 的相同目标。

为了让我们的应用程序在运行npm start 时使用自定义的webpack配置,我们需要更新package.json ,通过react-app-rewired 来调用脚本。

"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "test": "react-scripts test"
}

添加WebAssembly

现在应用程序可以支持WebAssembly,我们需要将Wasm模块安装为本地依赖。为此,我们需要将RustWasm库中的pkg 文件夹的内容复制到我们React应用程序根层的一个名为external 的新文件夹。

接下来,我们将Wasm模块添加到应用程序的package.json ,并将其与npm install

"dependencies": {
  "external": "file:./external"
}

你应该注意,这只是为了演示目的。在真实世界的情况下,我们会把包发布到npm上,作为公共或私人的包,并使用yarn或npm来安装它。

把它连在一起

现在我们已经有了所有的部件,最后一步是开始构建我们的应用程序。

用下面的代码替换App.js 组件。

import React, { useEffect, useState } from "react";
import {
  generateAudioSamples,
  byteSamplesToFloatSamples,
  floatSamplesToByteSamples,
} from "./helpers";

import { NUMBER_OF_SAMPLES } from "./constants";

const App = () => {
  const [audioContext, setAudioContext] = useState({});
  const [audioBuffer, setAudioBuffer] = useState({});
  const [amplifiedAudioSamples, setAmplifiedAudioSamples] = useState(
    new Float32Array(NUMBER_OF_SAMPLES)
  );
  const [audioBufferSource, setAudioBufferSource] = useState(undefined);
  const [originalAudioSamples, setOriginalAudioSamples] = useState(
    new Float32Array(NUMBER_OF_SAMPLES)
  );

  useEffect(() => {
    async function init() {
      try {
        const wasm = await import("external");

        // Create a Uint8Array to give us access to Wasm Memory
        const wasmByteMemoryArray = new Uint8Array(wasm.memory.buffer);

        // Generate 1024 float audio samples that make
        // a quiet and simple square wave
        const originalSamples = new Float32Array(
          generateAudioSamples(NUMBER_OF_SAMPLES)
        );

        // Convert the float audio samples to a byte format
        const originalByteAudioSamples = floatSamplesToByteSamples(
          originalSamples
        );

        // Fill the wasm memory with the converted Audio Samples
        // And store it at our inputPointer location
        // (starting index where the input buffer was stored in the rust code )
        const inputPointer = wasm.get_input_buffer_pointer();
        wasmByteMemoryArray.set(originalByteAudioSamples, inputPointer);

        // Amplify our loaded samples with our export Wasm function
        wasm.amplify_audio();

        // Get our outputPointer (index were the sample buffer was stored)
        // Slice out the amplified byte audio samples
        const outputPointer = wasm.get_output_buffer_pointer();
        const outputBuffer = wasmByteMemoryArray.slice(
          outputPointer,
          outputPointer + NUMBER_OF_SAMPLES
        );

        // Convert our amplified byte samples into float samples,
        // and set the outputBuffer to our amplifiedAudioSamples
        setAmplifiedAudioSamples(byteSamplesToFloatSamples(outputBuffer));

        setOriginalAudioSamples(originalSamples);

        // Create our audio context
        const context = new (window.AudioContext ||
          window.webkitAudioContext)();
        setAudioContext(context);

        // Create an empty stereo buffer at the sample rate of the AudioContext
        setAudioBuffer(
          context.createBuffer(2, NUMBER_OF_SAMPLES, context.sampleRate)
        );
      } catch (err) {
        console.error(`Unexpected error in init. [Message: ${err.message}]`);
      }
    }
    init();
  }, []);

//...

}

我不会多说与WebAssembly代码交互的细节,如果你想了解更多,可以参考我之前的文章。尽管如此,有几件事还是值得讨论的。

首先,我使用的是Hooks,它依赖于React 16.8或更高版本。如果你使用create-react-app boilerplate创建应用程序,你会有很好的状态。第二,所有的初始化代码都在useEffect 钩子里,它只在App.js 组件完成渲染后运行一次。在这个钩子里,我们用下面的代码加载和实例化我们的模块。

 const wasm = await import("external");

上面的代码是异步的,你可能想知道为什么我们没有像其他模块那样在文件的顶部导入我们的Wasm模块(即import wasm from "external" )?嗯,原因是当我们试图同步加载我们的Wasm模块时,浏览器会产生以下错误。

WebAssembly module is included in the initial chunk.
This is not allowed, because WebAssembly download and compilation must happen asynchronously.

之后,我们进行我们实际的音频生成和放大。同样,这篇文章假设你已经知道与Wasm内存互动的机制。这里剩下的大部分逻辑是使用WebAssembly线性内存,但在React应用程序的背景下。这里需要注意的是我们是如何使用.slice 调用从Wasm内存中读取的。最后,我们设置了AudioContextAudiobuffer 接口,稍后我们将使用它们来播放原始和放大的音频样本。

App.js 组件中,我们添加一些事件处理函数,以提供一种使用AudioBufferSourceNode 接口播放/暂停音频缓冲区的方法。它看起来和这个例子差不多,只是做了一些小改动,使用了我们之前定义的状态变量。

const App = () => {

    //...

    const beforePlay = () => {
        // Check if context is in suspended state
        if (audioContext.state === "suspended") {
        audioContext.resume();
        }
    };

    const stopAudioBufferSource = () => {
        // If we have an audioBufferSource
        // Stop and clear our current audioBufferSource
        if (audioBufferSource) {
        audioBufferSource.stop();
        setAudioBufferSource(undefined);
        }
    };

    const createAndStartAudioBufferSource = () => {
        // Stop the the current audioBufferSource
        stopAudioBufferSource();

        // Create an AudioBufferSourceNode.
        // This is the AudioNode to use when we want to play an AudioBuffer,
        // Set the buffer to our buffer source,
        // And loop the source so it continuously plays
        const bufferSource = audioContext.createBufferSource();
        bufferSource.buffer = audioBuffer;
        bufferSource.loop = true;

        // Connect our source to our output, and start! (it will play silence for now)
        bufferSource.connect(audioContext.destination);
        bufferSource.start();
        setAudioBufferSource(bufferSource);
    };

    const playOriginal = () => {
        beforePlay();
        // Set the float audio samples to the left and right channel
        // of our playing audio buffer
        audioBuffer.getChannelData(0).set(originalAudioSamples);
        audioBuffer.getChannelData(1).set(originalAudioSamples);

        createAndStartAudioBufferSource();
    };

    const playAmplified = () => {
        beforePlay();
        // Set the float audio samples to the left and right channel
        // of our playing audio buffer
        audioBuffer.getChannelData(0).set(amplifiedAudioSamples);
        audioBuffer.getChannelData(1).set(amplifiedAudioSamples);

        createAndStartAudioBufferSource();
    };

    const pause = () => {
        beforePlay();
        stopAudioBufferSource();
    };

    //...

}

然后我们渲染下面的JSX。

return (
    <div>
      <h1>Watch out when using headphones!!</h1>
      <h1>Original Sine Wave</h1>
      <div>
        <button
          className="original"
          onClick={() => {
            playOriginal();
          }}
        >
          Play
        </button>
      </div>
      <hr />
      <h1>Amplified Sine Wave</h1>
      <div>
        <button
          className="amplified"
          onClick={() => {
            playAmplified();
          }}
        >
          Play
        </button>
      </div>
      <hr />
      <h1>Pause</h1>
      <div>
        <button
          className="pause"
          onClick={() => {
            pause();
          }}
        >
          Pause
        </button>
      </div>
    </div>
)

最后,如果你注意到,我们在App组件的最顶端导入了一些辅助函数和一个常量。我们需要创建这些来完成我们的应用程序。

用这个路径创建一个文件src/helpers/index.js 并添加以下代码。

export const floatSamplesToByteSamples = (floatSamples) => {
  const byteSamples = new Uint8Array(floatSamples.length);
  for (let i = 0; i < floatSamples.length; i++) {
    const diff = floatSamples[i] * 127;
    byteSamples[i] = 127 + diff;
  }
  return byteSamples;
};

export const byteSamplesToFloatSamples = (byteSamples) => {
  const floatSamples = new Float32Array(byteSamples.length);
  for (let i = 0; i < byteSamples.length; i++) {
    const byteSample = byteSamples[i];
    const floatSample = (byteSample - 127) / 127;
    floatSamples[i] = floatSample;
  }
  return floatSamples;
};

export const generateAudioSamples = (numberOfSamples) => {
  const audioSamples = [];

  const sampleValue = 0.3;
  for (let i = 0; i < numberOfSamples; i++) {
    if (i < numberOfSamples / 2) {
      audioSamples[i] = sampleValue;
    } else {
      audioSamples[i] = sampleValue * -1;
    }
  }
  return audioSamples;
};

下面这个有点不必要,因为我们只有一个常量,但它显示了React应用项目结构方面的一些良好做法。

src/constants/index.js:

export const NUMBER_OF_SAMPLES = 1024;

这标志着我们的演示应用程序的结束,如果你遵循了上面的所有步骤,你最终应该得到与类似的东西。现在,剩下的就是用npm start 来启动这个应用程序。

Web browser screenshot

祝贺你,你已经成功地在React应用程序中导入并运行了Wasm库

总结

总而言之,我们从头开始创建了一个React应用程序,使用WebAssembly来生成和放大音频数据样本。我们从一个Rust程序开始,将其编译成一个Wasm模块,配置create-react-app ,以支持WebAssembly,并将该模块作为npm依赖项安装。我希望这个教程可以作为构建更复杂的React应用的基础。从现在开始,我计划更深入地研究WebAssembly,并将其用于更有野心的项目,如移植更大的库。