Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use HTML5 AudioWorklet beside ScriptProcessor for passing audio. #642

Merged
merged 2 commits into from
Aug 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions src.csharp/AlphaTab.Windows/NAudioSynthOutput.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ namespace AlphaTab
public class NAudioSynthOutput : WaveProvider32, ISynthOutput, IDisposable
{
private const int BufferSize = 4096;
private const int BufferCount = 10;
private const int PreferredSampleRate = 44100;

private const int TotalBufferTimeInMilliseconds = 5000;

private DirectSoundOut _context;
private CircularSampleBuffer _circularBuffer;
private int _bufferCount = 0;
private int _requestedBufferCount = 0;

/// <inheritdoc />
public double SampleRate => PreferredSampleRate;
Expand All @@ -41,8 +43,12 @@ public void Activate()
/// <inheritdoc />
public void Open()
{
_circularBuffer = new CircularSampleBuffer(BufferSize * BufferCount);

_bufferCount = (int)(
(TotalBufferTimeInMilliseconds * PreferredSampleRate) /
1000 /
BufferSize
);
_circularBuffer = new CircularSampleBuffer(BufferSize * _bufferCount);
_context = new DirectSoundOut(100);
_context.Init(this);

Expand Down Expand Up @@ -88,6 +94,7 @@ public void Pause()
public void AddSamples(Float32Array f)
{
_circularBuffer.Write(f, 0, f.Length);
_requestedBufferCount--;
}

/// <inheritdoc />
Expand All @@ -100,12 +107,19 @@ private void RequestBuffers()
{
// if we fall under the half of buffers
// we request one half
const int count = BufferCount / 2 * BufferSize;
if (_circularBuffer.Count < count && SampleRequest != null)
var halfBufferCount = _bufferCount / 2;
var halfSamples = halfBufferCount * BufferSize;
// Issue #631: it can happen that requestBuffers is called multiple times
// before we already get samples via addSamples, therefore we need to
// remember how many buffers have been requested, and consider them as available.
var bufferedSamples = _circularBuffer.Count + _requestedBufferCount * BufferSize;

if (bufferedSamples < halfSamples)
{
for (var i = 0; i < BufferCount / 2; i++)
for (var i = 0; i < halfBufferCount; i++)
{
((EventEmitter) SampleRequest).Trigger();
_requestedBufferCount++;
}
}
}
Expand Down
14 changes: 12 additions & 2 deletions src/Environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import { CapellaImporter } from './importer/CapellaImporter';
import { ResizeObserverPolyfill } from './platform/javascript/ResizeObserverPolyfill';
import { WebPlatform } from './platform/javascript/WebPlatform';
import { IntersectionObserverPolyfill } from './platform/javascript/IntersectionObserverPolyfill';
import { AlphaSynthWebWorklet } from './platform/javascript/AlphaSynthAudioWorkletOutput';

export class LayoutEngineFactory {
public readonly vertical: boolean;
Expand Down Expand Up @@ -205,6 +206,13 @@ export class Environment {
return 'WorkerGlobalScope' in Environment.globalThis;
}

/**
* @target web
*/
public static get isRunningInAudioWorklet(): boolean {
return 'AudioWorkletGlobalScope' in Environment.globalThis;
}

/**
* @target web
* @partial
Expand All @@ -221,7 +229,7 @@ export class Environment {
* @target web
*/
private static detectScriptFile(): string | null {
if (Environment.isRunningInWorker || Environment.webPlatform !== WebPlatform.Browser) {
if (!('document' in Environment.globalThis)) {
return null;
}
return (document.currentScript as HTMLScriptElement).src;
Expand Down Expand Up @@ -462,7 +470,9 @@ export class Environment {
* @partial
*/
public static platformInit(): void {
if (Environment.isRunningInWorker) {
if (Environment.isRunningInAudioWorklet) {
AlphaSynthWebWorklet.init();
} else if (Environment.isRunningInWorker) {
AlphaTabWebWorker.init();
AlphaSynthWebWorker.init();
} else if (Environment.webPlatform === WebPlatform.Browser) {
Expand Down
193 changes: 193 additions & 0 deletions src/platform/javascript/AlphaSynthAudioWorkletOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { CircularSampleBuffer } from '@src/synth/ds/CircularSampleBuffer';
import { Environment } from '@src/Environment';
import { Logger } from '@src/Logger';
import { AlphaSynthWorkerSynthOutput } from './AlphaSynthWorkerSynthOutput';
import { AlphaSynthWebAudioOutputBase } from './AlphaSynthWebAudioOutputBase';

/**
* @target web
*/
interface AudioWorkletProcessor {
readonly port: MessagePort;
process(inputs: Float32Array[][], outputs: Float32Array[][], parameters: Record<string, Float32Array>): boolean;
}

/**
* @target web
*/
declare var AudioWorkletProcessor: {
prototype: AudioWorkletProcessor;
new (options?: AudioWorkletNodeOptions): AudioWorkletProcessor;
};

/**
* This class implements a HTML5 Web Audio API based audio output device
* for alphaSynth using the modern Audio Worklets.
* @target web
*/
export class AlphaSynthWebWorklet {
public static init() {
(Environment.globalThis as any).registerProcessor(
'alphatab',
class AlphaSynthWebWorkletProcessor extends AudioWorkletProcessor {
public static readonly BufferSize: number = 4096;
private static readonly TotalBufferTimeInMilliseconds: number = 5000;

private _outputBuffer: Float32Array = new Float32Array(0);
private _circularBuffer!: CircularSampleBuffer;
private _bufferCount: number = 0;
private _requestedBufferCount: number = 0;

constructor(...args: any[]) {
super(...args);

this._bufferCount = Math.floor(
(AlphaSynthWebWorkletProcessor.TotalBufferTimeInMilliseconds *
Environment.globalThis.sampleRate) /
1000 /
AlphaSynthWebWorkletProcessor.BufferSize
);
this._circularBuffer = new CircularSampleBuffer(
AlphaSynthWebWorkletProcessor.BufferSize * this._bufferCount
);

this.port.onmessage = this.handleMessage.bind(this);
}

private handleMessage(e: MessageEvent) {
let data: any = e.data;
let cmd: any = data.cmd;
switch (cmd) {
case AlphaSynthWorkerSynthOutput.CmdOutputAddSamples:
const f: Float32Array = data.samples;
this._circularBuffer.write(f, 0, f.length);
this._requestedBufferCount--;
break;
case AlphaSynthWorkerSynthOutput.CmdOutputResetSamples:
this._circularBuffer.clear();
break;
}
}

public process(
_inputs: Float32Array[][],
outputs: Float32Array[][],
_parameters: Record<string, Float32Array>
): boolean {
if (outputs.length !== 1 && outputs[0].length !== 2) {
return false;
}
let left: Float32Array = outputs[0][0];
let right: Float32Array = outputs[0][1];
let samples: number = left.length + right.length;
let buffer = this._outputBuffer;
if (buffer.length !== samples) {
buffer = new Float32Array(samples);
this._outputBuffer = buffer;
}
this._circularBuffer.read(buffer, 0, Math.min(buffer.length, this._circularBuffer.count));
let s: number = 0;
for (let i: number = 0; i < left.length; i++) {
left[i] = buffer[s++];
right[i] = buffer[s++];
}
this.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed,
samples: left.length
});
this.requestBuffers();

return true;
}

private requestBuffers(): void {
// if we fall under the half of buffers
// we request one half
const halfBufferCount = (this._bufferCount / 2) | 0;
let halfSamples: number = halfBufferCount * AlphaSynthWebWorkletProcessor.BufferSize;
// Issue #631: it can happen that requestBuffers is called multiple times
// before we already get samples via addSamples, therefore we need to
// remember how many buffers have been requested, and consider them as available.
let bufferedSamples =
this._circularBuffer.count +
this._requestedBufferCount * AlphaSynthWebWorkletProcessor.BufferSize;
if (bufferedSamples < halfSamples) {
for (let i: number = 0; i < halfBufferCount; i++) {
this.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputSampleRequest
});
}
this._requestedBufferCount += halfBufferCount;
}
}
}
);
}
}

/**
* This class implements a HTML5 Web Audio API based audio output device
* for alphaSynth. It can be controlled via a JS API.
* @target web
*/
export class AlphaSynthAudioWorkletOutput extends AlphaSynthWebAudioOutputBase {
private _worklet: AudioWorkletNode | null = null;

public open() {
super.open();
this.onReady();
}

public play(): void {
super.play();
let ctx = this._context!;
// create a script processor node which will replace the silence with the generated audio
ctx.audioWorklet.addModule(Environment.scriptFile!).then(
() => {
this._worklet = new AudioWorkletNode(ctx!, 'alphatab');
this._worklet.port.onmessage = this.handleMessage.bind(this);

this._source!.connect(this._worklet, 0, 0);
this._source!.start(0);
this._worklet.connect(ctx!.destination);
},
reason => {
Logger.debug('WebAudio', `Audio Worklet creation failed: reason=${reason}`);
}
);
}

private handleMessage(e: MessageEvent) {
let data: any = e.data;
let cmd: any = data.cmd;
switch (cmd) {
case AlphaSynthWorkerSynthOutput.CmdOutputSamplesPlayed:
this.onSamplesPlayed(data.samples);
break;
case AlphaSynthWorkerSynthOutput.CmdOutputSampleRequest:
this.onSampleRequest();
break;
}
}

public pause(): void {
super.pause();
if (this._worklet) {
this._worklet.disconnect(0);
}
this._worklet = null;
}

public addSamples(f: Float32Array): void {
this._worklet?.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputAddSamples,
samples: f
});
}

public resetSamples(): void {
this._worklet?.port.postMessage({
cmd: AlphaSynthWorkerSynthOutput.CmdOutputResetSamples
});
}
}
98 changes: 98 additions & 0 deletions src/platform/javascript/AlphaSynthScriptProcessorOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { CircularSampleBuffer } from '@src/synth/ds/CircularSampleBuffer';
import { AlphaSynthWebAudioOutputBase } from './AlphaSynthWebAudioOutputBase';

// tslint:disable: deprecation

/**
* This class implements a HTML5 Web Audio API based audio output device
* for alphaSynth using the legacy ScriptProcessor node.
* @target web
*/
export class AlphaSynthScriptProcessorOutput extends AlphaSynthWebAudioOutputBase {
private _audioNode: ScriptProcessorNode | null = null;
private _circularBuffer!: CircularSampleBuffer;
private _bufferCount: number = 0;
private _requestedBufferCount: number = 0;

public open() {
super.open();
this._bufferCount = Math.floor(
(AlphaSynthWebAudioOutputBase.TotalBufferTimeInMilliseconds * this.sampleRate) /
1000 /
AlphaSynthWebAudioOutputBase.BufferSize
);
this._circularBuffer = new CircularSampleBuffer(AlphaSynthWebAudioOutputBase.BufferSize * this._bufferCount);
this.onReady();
}

public play(): void {
super.play();
let ctx = this._context!;
// create a script processor node which will replace the silence with the generated audio
this._audioNode = ctx.createScriptProcessor(4096, 0, 2);
this._audioNode.onaudioprocess = this.generateSound.bind(this);
this._circularBuffer.clear();
this.requestBuffers();
this._source = ctx.createBufferSource();
this._source.buffer = this._buffer;
this._source.loop = true;
this._source.connect(this._audioNode, 0, 0);
this._source.start(0);
this._audioNode.connect(ctx.destination, 0, 0);
}

public pause(): void {
super.pause();
if (this._audioNode) {
this._audioNode.disconnect(0);
}
this._audioNode = null;
}

public addSamples(f: Float32Array): void {
this._circularBuffer.write(f, 0, f.length);
this._requestedBufferCount--;
}

public resetSamples(): void {
this._circularBuffer.clear();
}

private requestBuffers(): void {
// if we fall under the half of buffers
// we request one half
const halfBufferCount = (this._bufferCount / 2) | 0;
let halfSamples: number = halfBufferCount * AlphaSynthWebAudioOutputBase.BufferSize;
// Issue #631: it can happen that requestBuffers is called multiple times
// before we already get samples via addSamples, therefore we need to
// remember how many buffers have been requested, and consider them as available.
let bufferedSamples =
this._circularBuffer.count + this._requestedBufferCount * AlphaSynthWebAudioOutputBase.BufferSize;
if (bufferedSamples < halfSamples) {
for (let i: number = 0; i < halfBufferCount; i++) {
this.onSampleRequest();
}
this._requestedBufferCount += halfBufferCount;
}
}

private _outputBuffer: Float32Array = new Float32Array(0);
private generateSound(e: AudioProcessingEvent): void {
let left: Float32Array = e.outputBuffer.getChannelData(0);
let right: Float32Array = e.outputBuffer.getChannelData(1);
let samples: number = left.length + right.length;
let buffer = this._outputBuffer;
if (buffer.length !== samples) {
buffer = new Float32Array(samples);
this._outputBuffer = buffer;
}
this._circularBuffer.read(buffer, 0, Math.min(buffer.length, this._circularBuffer.count));
let s: number = 0;
for (let i: number = 0; i < left.length; i++) {
left[i] = buffer[s++];
right[i] = buffer[s++];
}
this.onSamplesPlayed(left.length);
this.requestBuffers();
}
}
Loading