All the examples for this article come from Wasm By Example

What is WebAssembly?

  • WebAssembly is a compiled language that runs bytecode on the Web.
  • WebAssembly provides predictable performance compared to Javascript. It’s not always faster than JavaScript, but in the right use cases, it can run faster than JavaScript. Examples are computationally intensive tasks, such as nesting loops or processing large amounts of data. WebAssembly is therefore a complement to JavaScript, not a replacement.
  • WebAssembly is extremely portable. WebAssembly runs on: all major Web browsers, V8 runtime environments such as Node.js, and standalone Wasm runtime environments such asWasmtime.LucetWasmer).
  • WebAssembly has linear memory, in other words, a large expandable array. And in the context of Javascript, it can be accessed synchronously through Javascript and Wasm.
  • WebAssembly can export functions and constants, and can be accessed simultaneously by Javascript and Wasm in a Javascript context.
  • WebAssembly handles only integer and floating point numbers in its current MVP. However, tools and libraries exist to facilitate the passing of advanced data types.

What is AssemblyScript?

AssemblyScript is essentially TypeScript for the WebAssembly compiler. It’s for front-end developers who want to write WebAssembly without learning a new language. However, because WebAssembly differs from TypeScript/JavaScript, we can’t directly compile our TypeScript applications (such as React + TypeScript’s technology stack) to WebAssembly. This is due to language differences and limitations. Some JavaScript features and Typescript types are not available. But AssemblyScript is still a good choice if you’re building a WebAssembly application from scratch.

WebAssembly in the browser

The WebAssembly JavaScript object is the namespace for all WebAssembly related functionality.

Unlike most global objects, WebAssembly is not a constructor (it is not a function object). It is similar to the Math object or Intl object, which is also a namespace object for holding mathematical constants and functions, and Intl, which is a namespace object for internationalization and other language-related functions.

Relevant concepts

To understand how WebAssembly works in a browser, you need to understand a few concepts.

The following concepts are abstracted into an object

Module

Contains stateless WebAssembly code already compiled by the browser, which can be efficiently shared with Workers (via the postMessage() function), cached in IndexedDB, and instantiated multiple times. A module can be imported and exported as a JavaScript module.

Create method:

  • WebAssembly.Module()The constructor can be used to compile a given WebAssembly binary synchronously.
  • By asynchronously compiling functions such asWebAssembly.compile()Method, or read the Module object through IndexedDB.

Note: Because compiling large modules can be resource-intensive, developers use the Module() constructor only when synchronous compilation is absolutely necessary; In other cases, the asynchronous webassembly.compile () method should be used.

Memory

A resizable ArrayBuffer that stores the raw bytecode of memory accessed by a WebAssembly instance. Memory created from JavaScript or WebAssembly can be visited by JavaScript or WebAssembly to ask for changes.

Create method:

  • WebAssembly.Memory()Constructor creates a new oneMemoryObject. The object ofbufferProperty to get the desiredArrayBuffer

Table 1.

Has a class array structure that stores multiple function references. Table objects created in Javascript or WebAssemble can be accessed and changed by Javascript or WebAssemble at the same time.

Create method:

  • It usually goes throughWebAssembly.Table()The constructor creates a Table object based on the given size and element type.

Instance

An executable instance of WebAssembly.Module that has a state of its own and an Exports object that contains all WebAssembly exports and can call WebAssembly code using JavaScript.

Create method:

  • useWebAssembly.Instance()The constructor instantiates one synchronouslyWebAssembly.ModuleObject.
  • Through asynchronous functionsWebAssembly.instantiate() .

Note: Because large modules are expensive to instantiate, developers should only use Instance() when synchronous instantiation is necessary, mostly using the asynchronous method webAssembly.instantiate ().

Global

We saw modularity as a feature of WebAssembly earlier, but WebAssembly also allows us to share memory information across multiple modules, across one or more instances of WebAssembly.Module, and be dynamically connected by multiple modules.

Create method:

  • useWebAssembly.GlobalObject represents an instance of a global variable that can be used by JavaScript andimportable/exportableAccess.

methods

The following method besides WebAssembly. The validate () is returned by the Promise, mainly see WebAssembly. Instantiate () and WebAssembly instantiateStreaming () two methods, These two methods are generally used to load WBSM modules

WebAssembly.compile()

The webAssembly.pile () method compiles the WebAssembly binary into a Webassembly.module object. This method is useful if it is necessary to compile a module prior to instantiation (otherwise, the webassembly.instantiate () method will be used).

WebAssembly.compileStreaming()

WebAssembly.com pileStreaming () method is used to directly from a current source compile a WebAssembly Module. This method is useful when modules need to be compiled before being instantiated. If from the current source to instantiate a module should use WebAssembly. InstantiateStreaming () method.

WebAssembly.validate()

The webassembly.validate () method is used to verify that a typed array containing WebAssembly binals is valid. It returns true if the typed array constitutes a valid WASM module, and false otherwise.

WebAssembly.instantiate()

The main API for compiling and instantiating WebAssembly code that returns a Module and its first Instance. This method has two overloads:

  • The first major method of reloading uses WebAssembly binariestyped arrayArrayBufferTo compile and instantiate together. The returnedPromiseWill carry compiledWebAssembly.ModuleAnd its first instantiation objectWebAssembly.Instance.
  • The second type of overloading uses compiledWebAssembly.ModuleAnd the returnedPromiseCarry aModuleInstantiate object ofInstance.If thisModuleThis is useful if it has already been compiled or retrieved from the cache.

Note: This method is not the most efficient way to fetch and instantiate wASM modules. . If possible, should be to switch to a new WebAssembly instantiateStreaming () method, this method is directly obtained directly from the original bytecode, compile and instantiation module, so there is no need to convert ArrayBuffer.

WebAssembly.instantiateStreaming()

Compile and instantiate the WebAssembly Module directly from the streaming underlying source, returning the Module and its first Instance.

Here is a wrapper around the above two methods:

// Encapsulates the WebAssembly module's read function
export const wasmBrowserInstantiate = async (
  wasmModuleUrl,
  importObject
) => {
  let response

  // The passed importObject needs to provide env.abort()
  if (typeofimportObject? .env? .abort ! = ='function') {
    importObject = Object.assign({}, importObject, {
      env: {
        abort: () = > console.log('Abort! ')}}}// Check whether streaming Instantiation is supported
  if (WebAssembly.instantiateStreaming) {
    // The request module is then initialized
    response = await WebAssembly.instantiateStreaming(
      fetch(wasmModuleUrl),
      importObject
    )
  } else {
    // Fallback is not supported
    const fetchAndInstantiateTask = async() = > {const wasmArrayBuffer = await fetch(wasmModuleUrl).then((response) = >
        response.arrayBuffer()
      )
      return WebAssembly.instantiate(wasmArrayBuffer, importObject)
    }
    response = await fetchAndInstantiateTask()
  }

  return response
}
Copy the code

How to use:

import { wasmBrowserInstantiate } from './wbsm-util'

// Parsed WASM module
const wasmModule = await wasmBrowserInstantiate('www.xxx.com/index.wasm') 
Copy the code

Details on how to use the WASM module will be covered below.

The use of AssemblyScript

start

Build an AssemblyScript application

If you don’t want to build your project locally, try building it online using WebAssembly Studio

For front-end developers, it is very simple to download dependencies using NPM:

First create an empty project and then download the following two dependencies:

npm install --save @assemblyscript/loader
npm install --save-dev assemblyscript
Copy the code

Run:

npx asinit .
Copy the code

It automatically creates the initial recommended directory structure and configuration files for development in our project:

The directory structure is as follows:

Among them:

  • Asconfig is the configuration file of the ASC command, which is used to generate the corresponding. Wasm file

  • The Assembly directory is our actual development directory

  • The Build directory is our default build directory after converting TypeScript to WebAssembly

  • The index.js file is used to import our compiled. Wasm file and parse it using the appropriate loader for the rest of the file:

    // As you can see, the current environment is node environment, we can use the browser response API to read wASM
    const fs = require("fs");
    const loader = require("@assemblyscript/loader");
    // Used to use JavaScript modules in WebAssembly, as discussed later
    const imports = { /* imports go here */ };
    // Parse Module here
    const wasmModule = loader.instantiateSync(fs.readFileSync(__dirname + "/build/optimized.wasm"), imports);
    module.exports = wasmModule.exports;
    Copy the code
  • The tests directory is the test directory. The files below will import index.js to test the compiled modules, but this is too cumbersome. A JEST based unit test library has been developed for AssemblyScript, which will be explained later, so this directory can be removed.

Introduce the WASM module in the browser

Now open assembly/index.ts and see the following code:

// The entry file of your WebAssembly module.

export function add(a: i32, b: i32) :i32 {
  return a + b;
}
Copy the code

We’ll see a different TypeScript type i32, but it’s actually defined like this:

/** A 32-bit signed integer. */
declare type i32 = number;
Copy the code

WebAseembly represents a 32-bit signed integer, as well as i64, F32, f64, and other types that individually represent 64-bit integers and floating-point types. You can see that webAseembly, like Java and others, declares integers and floating-point values separately. Because we need to conform to WebAssembly features, we can no longer specify types like normal TypeScript syntax.

Assembly/hello-world /index.ts/hello-world /index.ts/hello-world /index.ts

npx asc assembly/Hello-World/index.ts -b build/Hello-World/index.wasm
Copy the code

As you can see, the corresponding Hello-world directory and the compiled wASM file are generated under the build directory. Let’s import them into the browser:

// examples/01.hello-world.js
// @assemblyscript/loader can be used in multiple environments
import { instantiate } from '@assemblyscript/loader'

const runWasmAdd = async() = > {// Instantiate the wASM Module
  // This is similar to the previous WBSM API wrapper
  const wasmModule = await instantiate(fetch('.. /build/Hello-World/index.wasm')) // You need to pass in the promise-wrapped file or pass Buffer directly
  // Call add the function we just wrote
  const addResult = wasmModule.exports.add(24.24)

  // Put the value into the body
  document.body.textContent = `Hello World! addResult: ${addResult}`
}
runWasmAdd()
Copy the code
<! DOCTYPEhtml>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Hello World - AssemblyScript</title>
    <! - import - >
    <script type="module" src="./01.hello-world.js"></script>
  </head>
  <body></body>
</html>
Copy the code

Export the WASM module

// assembly/Exports/index.ts
// Export as normal, you can use unexported functions inside exported functions
export function callMeFromJavascript(a: i32, b: i32) :i32 {
  return addIntegerWithConstant(a, b)
}

// Export a constant
export const GET_THIS_CONSTANT_FROM_JAVASCRIPT: i32 = 2424

// No export
function addIntegerWithConstant(a: i32, b: i32) :i32 {
  return a + b + ADD_CONSTANT
}
// No export
const ADD_CONSTANT: i32 = 1
Copy the code

Compile:

npx asc assembly/Exports/index.ts -b build/Exports/index.wasm
Copy the code
// examples/02.exports.js
import { instantiate } from '@assemblyscript/loader'
const runWasm = async() = > {// Instantiate our wasm module
  const wasmModule = await instantiate(fetch('.. /build/Exports/index.wasm'))
  // Get the exports property on the module instance object
  const exports = wasmModule.instance.exports

  console.log(exports.callMeFromJavascript(24.24)) / / 49

  // If we print only variables with no value, but a Global object, we need to get the object's value property to get the value
  console.log(exports.GET_THIS_CONSTANT_FROM_JAVASCRIPT) // GLobal { value: 2424 }
  console.log(exports.GET_THIS_CONSTANT_FROM_JAVASCRIPT.valueOf()) //  2424
  console.log(exports.GET_THIS_CONSTANT_FROM_JAVASCRIPT.value) //  2424
  // Prints an unexported function
  console.log(exports.addIntegerWithConstant) // undefined
}
runWasm()
Copy the code

We can see the print on the console:

Share memory with JavaScript

Linear memory is a contiguous buffer of unsigned bytes that can be read and stored by Wasm and JavaScript. In other words, Wasm memory is an extensible array of bytes that JavaScript and Wasm can read and modify synchronously. Linear memory can be used for many things, one of which is passing values back and forth between Wasm and JavaScript.

// assembly/WebAssembly-Linear-Memory/index.ts
/ / create the memory
// By adding a page (64KB) of Wasm Memory
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Memory#Examples
memory.grow(1)

// Use the store function to store values like an array
const index = 0
const value = 24
store<u8>(index, value)

export function readWasmMemoryAndReturnIndexOne() :i32 {
  // Read the data in memory
  let valueAtIndexOne = load<u8>(1)
  // return the value of index 1 (the value of index 0 in default Settings above)
  return valueAtIndexOne
}
Copy the code

Compile:

npx asc assembly/WebAssembly-Linear-Memory/index.ts -b build/WebAssembly-Linear-Memory/index.wasm
Copy the code
// examples/03.webassembly-linear-memory.js
import { instantiate } from '@assemblyscript/loader'

const runWasm = async() = > {const wasmModule = await instantiate(
    fetch('.. /build/WebAssembly-Linear-Memory/index.wasm'))const exports = wasmModule.instance.exports

  // Get our memory object, where all the memory in Wasm resides
  const memory = exports.memory

  // Create Uint8Array to allow us to access Wasm Memory
  const wasmByteMemoryArray = new Uint8Array(memory.buffer)

  // Access it in js
  console.log(wasmByteMemoryArray[0]) / / 24

  // Change the value in memory in js
  wasmByteMemoryArray[1] = 25
  // Print the value with index 1 correctly
  console.log(exports.readWasmMemoryAndReturnIndexOne()) / / 25
}
runWasm()
Copy the code

Import JavaScript functions into WebAssembly

Let’s take a look at the imports object initially mentioned in the application building AssemblyScript above, which allows us to inject JavaScript variables or functions into the Wasm module and call them there.

Note: Compiled modules must have a matching attribute for each imported value, otherwise an error will be thrown.

// assembly/Importing-Javascript-Functions-Into-WebAssembly/index.ts
// Define a function for the imports object
declare function consoleLog(arg0: i32) :void// Call this function directlyconsoleLog(24)
Copy the code

Compile:

npx asc assembly/Importing-Javascript-Functions-Into-WebAssembly/index.ts -b build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm
Copy the code
// examples/04-importing-javascript-functions-into-webAssembly.js
import { instantiate } from '@assemblyscript/loader'
const runWasm = async() = > {const wasmModule = await instantiate(
    fetch(
      '.. /build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm'
    ),
    // The second argument importObject is passed to the instantiation
    {
      AssemblyScript mounts all imported values into a module named after the file name by default
      index: {
        consoleLog: (value) = > console.log(value)
      }
    }
  )
}
runWasm()
Copy the code

Custom import name

We can use the @external decorator to change the name of the function we want to import from the outside world. The @external decorator can pass in one or two arguments, one to change the function or variable name, and two to change the module name and function or variable name.

// Define a function for the imports object
// @ts-ignore
// Change only the function name
@external("consoleLog3")
declare function consoleLog(arg0: i32) :void
// import consoleLog3 from index/ / @ts-ignore// Change the module name and variable name @external("foo"."consoleLog4")
declare function consoleLog2(arg0: i32) :void
// import consoleLog4 from foo

consoleLog(24)
consoleLog2(25)
Copy the code

You can see that AssemblyScript, even within the same file, can be separately allocated for use in different modules at compile time.

import { instantiate } from '@assemblyscript/loader'
const runWasm = async() = > {const wasmModule = await instantiate(
    fetch(
      '.. /build/Importing-Javascript-Functions-Into-WebAssembly/index.wasm'
    ),
    {
      // The default file name
      index: {
        consoleLog3: (value) = > console.log(value)
      },
      // Our custom module name
      foo: {
        consoleLog4: (value) = > console.log(value)
      }
    }
  )
}
runWasm()
Copy the code

in-depth

Read and write image

WebAssembly is perfect for computationally intensive tasks, and even the official AssemblyScript document covers this. For example, tasks like heavy logic with conditions or nested loops involving big data. Therefore, by moving the above tasks to WebAssembly, you can generate/render graphics, which can be significantly faster.

In the example below, we will generate 20×20 color checkerboard images per second and display them on the canvas using Pixel Manipulation on the ImageData object. In graphical terms, this is a raster graph.

// assembly/Reading-and-Writing-Graphics/index.ts	
/ / create the memory
memory.grow(1)

// Define the number of rasters
const CHECKERBOARD_SIZE: i32 = 20

// Create a buffer/pointer (array index and size) that points to the pixels we store in memory.
// memoryBase, the initial offset of memory thrown outward
export const CHECKERBOARD_BUFFER_POINTER: i32 = 0
// The total amount of memory required, multiplied by 4 because each cell needs to store (r,g,b,a) four values
export const CHECKERBOARD_BUFFER_SIZE: i32 =
  CHECKERBOARD_SIZE * CHECKERBOARD_SIZE * 4

// Generate the raster step by pixel, passing in RGB light and dark values
export function generateCheckerBoard(darkValueRed: i32, darkValueGreen: i32, darkValueBlue: i32, lightValueRed: i32, lightValueGreen: i32, lightValueBlue: i32) :void {
  Since WebAssembly's linear memory is a one-dimensional array, we need to do a 2-d to 1-D mapping
  for (let x: i32 = 0; x < CHECKERBOARD_SIZE; x++) {
    for (let y: i32 = 0; y < CHECKERBOARD_SIZE; y++) {
      // The current grid has two options: light and dark
      let isDarkSquare: boolean = true

      // Check the status of the current grid
      // Light if y is even
      if (y % 2= = =0) {
        isDarkSquare = false
      }
      // If x is even, the brightness is reversed
      if (x % 2= = =0) { isDarkSquare = ! isDarkSquare }// Assign an RGB value to the current grid
      let squareValueRed = darkValueRed
      let squareValueGreen = darkValueGreen
      let squareValueBlue = darkValueBlue

      if(! isDarkSquare) { squareValueRed = lightValueRed squareValueGreen = lightValueGreen squareValueBlue = lightValueBlue }// The index is computed by a 2-d to 1-D mapping
      let squareNumber = y * CHECKERBOARD_SIZE + x
      // Remember to multiply by 4, because each grid needs to hold 4 values (r, G.B.A)
      let squareRgbaIndex = squareNumber * 4

      // Store all values in memory
      store<u8>(
        CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 0,
        squareValueRed
      ) // Red
      store<u8>(
        CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 1,
        squareValueGreen
      ) // Green
      store<u8>(
        CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 2,
        squareValueBlue
      ) // Blue
      store<u8>(CHECKERBOARD_BUFFER_POINTER + squareRgbaIndex + 3.255) // Alpha (always opaque)}}}Copy the code

Compile:

npx asc assembly/Reading-and-Writing-Graphics/index.ts -b build/Reading-and-Writing-Graphics/index.wasm
Copy the code
// examples/05-reading-and-writing-graphics.js
import { instantiate } from '@assemblyscript/loader'
const runWasm = async() = > {const wasmModule = await instantiate(
    fetch('.. /build/Reading-and-Writing-Graphics/index.wasm'))const exports = wasmModule.instance.exports

  // Get our memory object, where all the memory in Wasm resides
  const memory = exports.memory

  // Create Uint8Array to allow us to access Wasm Memory
  const wasmByteMemoryArray = new Uint8Array(memory.buffer)

  const canvasElement = document.querySelector('canvas')

  const canvasContext = canvasElement.getContext('2d')
  // Draw width and height
  const canvasImageData = canvasContext.createImageData(
    canvasElement.width,
    canvasElement.height
  )

  // Get the value randomly
  const getDarkValue = () = > {
    return Math.floor(Math.random() * 100)}const getLightValue = () = > {
    return Math.floor(Math.random() * 127) + 127
  }

  const drawCheckerBoard = () = > {
    exports.generateCheckerBoard(
      getDarkValue(),
      getDarkValue(),
      getDarkValue(),
      getLightValue(),
      getLightValue(),
      getLightValue()
    )

    // Take the raster value we saved from memory, just take the space occupied by the raster from the beginning to the end
    const imageDataArray = wasmByteMemoryArray.slice(
      // The value we exported in Wasm
      exports.CHECKERBOARD_BUFFER_POINTER.value,
      exports.CHECKERBOARD_BUFFER_SIZE.value
    )

    / / set the values
    canvasImageData.data.set(imageDataArray)

    / / empty canvas
    canvasContext.clearRect(0.0, canvasElement.width, canvasElement.height)

    // Draw a new graph
    canvasContext.putImageData(canvasImageData, 0.0)
  }

  drawCheckerBoard()
  setInterval(() = > {
    drawCheckerBoard()
  }, 1000)
}
runWasm()
Copy the code
<! DOCTYPEhtml>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Reading-and-Writing-Graphics</title>
    <script type="module" src="./05-reading-and-writing-graphics.js"></script>
  </head>
  <body>
    <canvas
      width="20"
      height="20"
      style=" image-rendering: pixelated; image-rendering: crisp-edges; width: 100%; "
    >
    </canvas>
  </body>
</html>
Copy the code

Read and write audio

This example is just to show you how to read and write audio in AssemblyScript, but production doesn’t actually do that, and we did it for learning purposes.

This example makes noise, watch the volume!!

Similarly, audio samples can be generated/rendered in WebAssembly for a significant speed increase.

In the following example, we will use the Web Audio API to enlarge the Audio sample in the AudioBuffer.

Note: This can and should be done via GainNode, but this is mainly for demonstration purposes. Or we can imagine implementing unsupported Web Audio API effects, such as BitCrusher or ogG decoder for unsupported browsers.

// assembly/Reading-and-Writing-Audio/index.ts
memory.grow(1)

// Write to INPUT_BUFFER in JavaScript
export const INPUT_BUFFER_POINTER: i32 = 0
export const INPUT_BUFFER_SIZE: i32 = 1024
// Write the result to OUTPUT_BUFFER in Wasm
export const OUTPUT_BUFFER_POINTER: i32 =
  INPUT_BUFFER_POINTER + INPUT_BUFFER_SIZE
export const OUTPUT_BUFFER_SIZE: i32 = INPUT_BUFFER_SIZE

// Zoom in
export function amplifyAudioInBuffer() :void {
  for (let i = 0; i < INPUT_BUFFER_SIZE; i++) {
    // Load all specified audioSample
    let audioSample: u8 = load<u8>(INPUT_BUFFER_POINTER + i)

    / / audioSample amplification
    // Use 127 as the boundary (up to 127 is negative, above 127 is positive), 0 is negative, 256 is positive
    if (audioSample > 127) {
      let audioSampleDiff = audioSample - 127
      audioSample = audioSample + audioSampleDiff
    } else if (audioSample < 127) {
      audioSample = audioSample / 2
    }

    // Save the converted audioSample to OUTPUT_BUFFER
    store<u8>(OUTPUT_BUFFER_POINTER + i, audioSample)
  }
}
Copy the code

Compile:

npx asc assembly/Reading-and-Writing-Audio/index.ts -b build/Reading-and-Writing-Audio/index.wasm
Copy the code
// examples/06-reading-and-writing-audio.js
import { instantiate } from '@assemblyscript/loader'

// Create audio context
const audioContext = new (window.AudioContext || window.webkitAudioContext)()
// The number of sample frames in buffer
const numberOfSamples = 1024
// Create an empty audio clip with the sampling rate of the audioContext
const audioBuffer = audioContext.createBuffer(
  // 2 audio channels for stereo and 1 for mono
  2.// Playing in an audio environment with the frequency audioContext.samplerate lasts numberOfSamples/AudioContext.samplerate for seconds
  numberOfSamples,
  audioContext.sampleRate
)

// Create the original sample buffer and enlarge the sample buffer area
const originalAudioSamples = new Float32Array(numberOfSamples)
const amplifiedAudioSamples = new Float32Array(numberOfSamples)

// Convert a floating point sample to a byte sample
const floatSamplesToByteSamples = (floatSamples) = > {
  const byteSamples = new Uint8Array(floatSamples.length)
  // Use 127 as the boundary (up to 127 is negative, above 127 is positive), 0 is negative, 256 is positive
  for (let i = 0; i < floatSamples.length; i++) {
    const diff = floatSamples[i] * 127
    byteSamples[i] = 127 + diff
  }
  return byteSamples
}

// Convert a byte sample to a floating point sample
const byteSamplesToFloatSamples = (byteSamples) = > {
  const floatSamples = new Float32Array(byteSamples.length)
  // Use 127 as the boundary (up to 127 is negative, above 127 is positive), 0 is negative, 256 is positive
  for (let i = 0; i < byteSamples.length; i++) {
    const byteSample = byteSamples[i]
    const floatSample = (byteSample - 127) / 127
    floatSamples[i] = floatSample
  }
  return floatSamples
}

const runWasm = async() = > {const wasmModule = await instantiate(
    fetch('.. /build/Reading-and-Writing-Audio/index.wasm'))const exports = wasmModule.instance.exports

  const memory = exports.memory

  const wasmByteMemoryArray = new Uint8Array(memory.buffer)

  // Generate 1024 floating point audio samples
  // Our floating point sample value is 0.3
  const sampleValue = 0.3
  for (let i = 0; i < numberOfSamples; i++) {
    if (i < numberOfSamples / 2) {
      originalAudioSamples[i] = sampleValue
    } else {
      originalAudioSamples[i] = sampleValue * -1}}// The converted sample of the original bytes
  const originalByteAudioSamples = floatSamplesToByteSamples(
    originalAudioSamples
  )

  // Fill the WASM memory with the converted audio sample stored in the INPUT_BUFFER_POINTER index
  wasmByteMemoryArray.set(
    originalByteAudioSamples,
    exports.INPUT_BUFFER_POINTER.value
  )

  // Execute the audio amplification function
  exports.amplifyAudioInBuffer()

  // Get the amplified audio buffer
  const outputBuffer = wasmByteMemoryArray.slice(
    exports.OUTPUT_BUFFER_POINTER.value,
    exports.OUTPUT_BUFFER_POINTER.value + exports.OUTPUT_BUFFER_SIZE.value
  )

  // Convert the enlarged byte sample back to a floating point sample
  const outputFloatAudioSamples = byteSamplesToFloatSamples(outputBuffer)
  // Set outputFloatAudioSamples to amplifiedAudioSamples
  amplifiedAudioSamples.set(outputFloatAudioSamples)
}
runWasm()

function beforePlay() {
  // Check if the context is suspended (autoplay)
  if (audioContext.state === 'suspended') {
    audioContext.resume()
  }
}

// Global audioBufferSource object
let audioBufferSource
function stopAudioBufferSource() {
  // If the audioBufferSource has a value, pause and set it to null
  if (audioBufferSource) {
    audioBufferSource.stop()
    audioBufferSource = undefined}}function createAndStartAudioBufferSource() {
  // Stop the previous one
  stopAudioBufferSource()

  audioBufferSource = audioContext.createBufferSource()
  // The audioBuffer is the empty audio fragment we created earlier
  audioBufferSource.buffer = audioBuffer
  // Loop
  audioBufferSource.loop = true

  // Connect the source and output and start
  audioBufferSource.connect(audioContext.destination)
  audioBufferSource.start()
}

// UI
const playBtn = document.getElementById('play-btn')
const amplifiedBtn = document.getElementById('amplified-btn')
const pauseBtn = document.getElementById('pause-btn')

playBtn.addEventListener('click', playAmplified)
amplifiedBtn.addEventListener('click', playOriginal)
pauseBtn.addEventListener('click', pause)

function playOriginal() {
  beforePlay()
  // Original audio
  // Set the floating audio samples to left and right channels
  audioBuffer.getChannelData(0).set(originalAudioSamples)
  audioBuffer.getChannelData(1).set(originalAudioSamples)

  createAndStartAudioBufferSource()
}

function playAmplified() {
  beforePlay()
  // Zoom in
  // Set the floating audio samples to left and right channels
  audioBuffer.getChannelData(0).set(amplifiedAudioSamples)
  audioBuffer.getChannelData(1).set(amplifiedAudioSamples)

  createAndStartAudioBufferSource()
}

function pause() {
  beforePlay()
  stopAudioBufferSource()
}
Copy the code
<! DOCTYPEhtml>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Reading-and-Writing-Audio</title>
    <script type="module" src="./06-reading-and-writing-audio.js"></script>
  </head>
  <body>
    <h1>NOTE: Be careful if using headphones</h1>

    <h1>Original Sine Wave</h1>
    <div><button id="play-btn">Play</button></div>

    <hr />

    <h1>Amplified Sine Wave</h1>
    <div>
      <button id="amplified-btn">Play</button>
    </div>

    <hr />

    <h1>Pause</h1>
    <div><button id="pause-btn">Pause</button></div>
  </body>
</html>
Copy the code

Different from TypeScript

Since I haven’t used many of these features before, I decided to start with an example of how to use AssemblyScript and how it differs from TypeScript.

AssemblyScript is compiled to WebAssembly and TypeScript is compiled to JavaScript, so there’s a fundamental difference. AssemblyScript is more like C, C++, or Rust, so WebAssembly has much tighter static typing constraints.

More constrained static typing

The first thing to note when writing AssemblyScript is that its base types differ from TypeScript’s because it uses WebAssembly’s more specific integer and floating point types, And JavaScript numbers are just aliases for WebAssembly’s F64.

Whereas the JavaScript JIT tries to figure out the best representation of a numeric value at code execution, assuming that the value will change the type and possibly recompile the code multiple times, AssemblyScript allows the developer to specify the ideal type in advance with all its advantages and benefits.

There is no any and undefined

// bad
function foo(a?) {
  const b = a + 1
  return b
}

// good
function foo(a: i32 = 0) :i32 {
  const b = a + 1
  return b
}
Copy the code

No union type

// bad
function foo(a: i32 | string) :void {}// good
function foo<T> (a: T) :void {}Copy the code

Strictly typed objects

AssemblyScript is more static than TypeScript. Instead of using object literals to declare objects, AssemblyScript uses new Map() to create objects.

// bad
const obj1 = {};
obj.a = 1;

// good
const obj2 = new Map(a); obj2.set('a'.1);
Copy the code

Different full equals (=== =)

The special semantics of the === operator (true only when both value and type match) do not make much sense in AssemblyScript, because it is illegal to compare values of two incompatible types anyway. Thus, the === operator has been re-used to perform identity comparisons and evaluates to true if both operands are identical objects:

const a = "hello"
const b = a
const c = "h" + a.substring(1)

if (a === b) { /* true */ }
if (a === c) { /* false */ }
if (a == c) { /* true */ }
Copy the code

Check for Null values

Like TypeScript, the AssemblyScript compiler checks whether passed values are nullable:

function doSomething(something: string | null) :void {
  if (something) {
    something.length // works}}Copy the code

However, this does not apply to attributes of an object, since their values can change between checking and using values:

class Foo {
  something: string | null = null;
}
function doSomething(foo: Foo) :void {
  if (foo.something) {
    // ... some code ...
    foo.something.length // fails}}Copy the code

If we set foo.something to null at run time, TypeScript will report a runtime error when we access it again.

class Foo {
  something: string | null = null;
}

function doSomething(foo: Foo) {
  if (foo.something) {
    doSomethingElse(foo);
    foo.something.length; // should error, but doesn't}}function doSomethingElse(foo: Foo) {
  foo.something = null;
}
Copy the code

AssemblyScript doesn’t have such runtime errors, however, so local variables must be used to be safe:

function doSomething(foo: Foo) :void {
  let something = foo.something
  if (something) {	
    something.length // works}}Copy the code

Unit testing

For unit tests, we used community-provided AS-Pect.

Integration in the project is also easy, we just need to install AS-Pect in the previously created project

npm install --save-dev @as-pect/cli
Copy the code

Then run the following command:

npx asp --init
Copy the code

The scaffolding will automatically create the corresponding initialization file in our project:

Add a script to package.json:

{
  "scripts": {
    "test": "asp --verbose"."test:ci": "asp --summary"}}Copy the code

By default, the as-pice.config. js file in the root directory is used as the configuration file, or –config can be used to actively specify the configuration file. The specific configuration and use can be viewed in its official documentation, which is not detailed here.

conclusion

To conclude, this article starts with the WebAssembly Api in the browser, introduces its related features and methods, and Outlines how to introduce the WebAssembly module in the browser. We then use several examples to show how to integrate and use AssemScript in development, and compare it to traditional TypeScript, which requires different attention points when compiled. Finally, I briefly introduced the use of AssemblyScript’s unit testing tool.

In general, AssemblyScript provides front-end developers with a great deal of access to the WebAssembly world. You can switch to AssemblyScript to generate WASM modules when you’re doing a lot of computing and can easily put them into production.

Finally, the author has limited skills. If there are any mistakes or omissions, please help to point them out. I will revise them as soon as possible.

All the code for this article has been uploaded to Github

The resources

  • The AssemblyScript Book
  • Wasm By Example
  • as-pect
  • MDN – WebAssembly
  • I met WebAssembly