I have an Angular 13 audio application that uses an audio worklet. The UI is Angular/Typescript but the worklet is straight Javascript. The worklet is loaded by calling Worklet.addModule()
with the path to the javascript file stored in the assets
folder.
I would like to develop the worklet in Typescript and share some of the code with the UI. Since addModule()
needs a single Javascript file I'm trying to learn how to get the worklet code and any imported modules transpiled into a standalone Javascript file while the UI part of the project is bundled as usual.
I'm pretty comfortable with Angular and Typescript but have just started to learn more about tsc and webpack because I feel like the solution is somewhere in there. Am I on the right track?
I've poked around for solutions and have found a few hits related to Angular and AudioWorklet, but no breakthroughs. I've gotten web workers to work with Angular but haven't found a way to extend that to worklets.
I found a solution that gives me the two requirements I was after:
Since modernizing the worklet code was also a goal I upgraded Angular from 13 to 16, Webpack to version 5 and Typescript to 5.0.2. I don't see any reason why the following solution wouldn't work for older versions.
custom-webpack
NPM package@types/audioworklet
NPM packageThe angular.json
settings for the original project are unchanged. The worklet sub-project build settings are configured to use custom-webpack
as the builder
"architect": {
"build": {
"builder": "@angular-builders/custom-webpack:browser",
"options": {
"customWebpackConfig": {
"path": "projects/reader-worklet/webpack-reader.config.js",
"verbose": {
"properties": [
"entry"
]
}
},
"outputPath": "dist/reader-worklet",
"main": "projects/reader-worklet/src/worklet.ts",
"tsConfig": "projects/reader-worklet/tsconfig.app.json",
...
The webpack configuration for the worklet is minimal. The key settings that made a difference were to set target:"webworker"
to avoid DOM references and optimization.runtimeChunk: false
so that rumtime.js
is not built as a separate .js file, but rather included in the single output file render-worklet.js
. Yes, I call it reader-worklet
in one place and render-worklet
in another, I'm still not sure what to call it :)
"use strict";
const path = require('path');
const mode = process.env.NODE_ENV === "production" ? "production" : "development";
module.exports = {
// WARNING: MUST set the 'mode' manually because it isn't done by NX/NG cli
mode,
entry: {
main: {
import: path.resolve(__dirname, './src/worklet.ts'),
filename: "render-worklet.js",
},
},
output: {
clean: true
},
optimization: {
runtimeChunk: false
},
target: "webworker",
devtool: 'source-map',
module: {
rules: [],
},
plugins: []
};
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
"module": "es2022",
"esModuleInterop": true,
"target": "es2022",
"lib": ["es2022"],
"moduleResolution": "node",
"sourceMap": true,
"types": [ "@types/audioworklet"]
},
"files": ["src/worklet.ts"],
"include": [ "src/**/*.d.ts"]
}
Here is a basic class that extends AudioWorkletProcessor
. It imports a class WTrackSettings
that is shared with the UI code. It's not used for anything here, just dumped to the console to verify that it's able to be imported and used in the worklet. The process()
method generates a square wave output.
import {WTrackSettings} from "../../../src/app/include/shared_defs";
export class TSWorklet extends AudioWorkletProcessor {
active = true;
constructor() {
super();
this.sayHello();
}
public sayHello(): void {
const ts: WTrackSettings = new WTrackSettings();
console.table(ts);
console.log("Hello!!!");
}
process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record<string, Float32Array>): boolean {
let buflen = 0;
let outBuffer = outputs[0];
if (outBuffer.length > 0) {
buflen = outBuffer[0].length;
}
let frameCount = 0;
for (; frameCount < buflen / 2; frameCount++) {
outBuffer[0][frameCount] = 1;
outBuffer[1][frameCount] = 0
}
for (; frameCount < buflen; frameCount++) {
outBuffer[0][frameCount] = 0;
outBuffer[1][frameCount] = 1
}
return this.active;
}
}
registerProcessor('ts-reader-worklet', TSWorklet);
I haven't shown the AudioWorkletNode
implementation because it's exactly the same as when the AudioWorkletProcessor
is written in Javascript.