L'API Web Audio existe depuis longtemps et il existe de nombreux articles à ce sujet. Par conséquent, nous ne parlerons pas beaucoup de l'API elle-même. Nous vous dirons que Web Audio et Angular peuvent être les meilleurs amis si vous les introduisez correctement. Faisons cela!

Web Audio API , , . , , . . - :
const context = new AudioContext();
const gainNode = context.createGain();
, , . Web Audio API Angular.
Angular — , . , , :
const context = new AudioContext();
const gainNode = new GainNode(context);
const delayNode = new DelayNode(context);
const audioSource = new MediaElementAudioSourceNode(context, {
mediaElement: audioElement.nativeElement,
});
gainNode.gain.value = 0.5;
delayNode.delayTime.value = 0.2;
audioSource.connect(gainNode);
audioSource.connect(context.destination);
gainNode.connect(delayNode);
delayNode.connect(gainNode);
delayNode.connect(context.destination);
, — . , , connect. HTML audio , , . . AudioContext
Dependency Injection.
GainNode
DelayNode
— . , AudioParam
. .
@Directive({
selector: '[waGainNode]',
inputs: [
'channelCount',
'channelCountMode',
'channelInterpretation'
],
})
export class WebAudioGain extends GainNode {
@Input('gain')
set gainSetter(value: number) {
this.gain.value = value;
}
constructor(@Inject(AUDIO_CONTEXT) context: AudioContext) {
super(context);
}
}
, : channelCount
, channelCountMode
channelInterpretation
. inputs
@Directive
— . DelayNode
. AUDIO_NODE
, :
@Directive({
selector: '[waGainNode]',
inputs: [
'channelCount',
'channelCountMode',
'channelInterpretation'
],
exportAs: 'AudioNode',
providers: [{
provide: AUDIO_NODE,
useExisting: forwardRef(() => WebAudioGain),
}],
})
export class WebAudioGain extends GainNode implements OnDestroy {
@Input('gain')
set gainSetter(value: number) {
this.gain.value = value;
}
constructor(
@Inject(AUDIO_CONTEXT) context: BaseAudioContext,
@SkipSelf() @Inject(AUDIO_NODE) node: AudioNode | null,
) {
super(context);
if (node) {
node.connect(this);
}
}
ngOnDestroy() {
this.disconnect();
}
}
DI . exportAs
— template reference variables. :
<ng-container waGainNode>
<ng-container waDelayNode></ng-container>
</ng-container>
waAudioDestination
:
@Directive({
selector: '[waAudioDestinationNode]',
exportAs: 'AudioNode',
})
export class WebAudioDestination extends GainNode
implements OnDestroy {
constructor(
@Inject(AUDIO_CONTEXT) context: AudioContext,
@Inject(AUDIO_NODE) node: AudioNode | null,
) {
super(context);
this.connect(context.destination);
if (node) {
node.connect(this);
}
}
ngOnDestroy() {
this.disconnect();
}
}
, , Dependency Injection. . , :
@Directive({
selector: '[waOutput]',
})
export class WebAudioOutput extends GainNode implements OnDestroy {
@Input()
set waOutput(destination: AudioNode | undefined) {
this.disconnect();
if (destination) {
this.connect(destination);
}
}
constructor(
@Inject(AUDIO_CONTEXT) context: AudioContext,
@Inject(AUDIO_NODE) node: AudioNode | null,
) {
super(context);
if (node) {
node.connect(this);
}
}
ngOnDestroy() {
this.disconnect();
}
}
GainNode
, . ngOnDestroy
. , , this
.
. -, . audio
, MediaElementAudioSourceNode
:
@Directive({
selector: 'audio[waMediaElementAudioSourceNode]',
exportAs: 'AudioNode',
providers: [
{
provide: AUDIO_NODE,
useExisting: forwardRef(() => WebAudioMediaSource),
},
],
})
export class WebAudioMediaSource extends MediaElementAudioSourceNode
implements OnDestroy {
constructor(
@Inject(AUDIO_CONTEXT) context: AudioContext,
@Inject(ElementRef) {nativeElement}: ElementRef<HTMLMediaElement>,
) {
super(context, {mediaElement: nativeElement});
}
ngOnDestroy() {
this.disconnect();
}
}
:
<audio src="/demo.wav" waMediaElementAudioSourceNode>
<ng-container #node="AudioNode" waDelayNode [delayTime]="0.2">
<ng-container waGainNode [gain]="0.5">
<ng-container [waOutput]="node"></ng-container>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
</ng-container>
<ng-container waAudioDestinationNode></ng-container>
</audio>
Web Audio API , . - . HTML . AudioBufferSourceNode
. — , `AudioBuffer. AudioBuffer
:
@Injectable({
providedIn: 'root',
})
export class AudioBufferService {
private readonly cache = new Map<string, AudioBuffer>();
constructor(
@Inject(AUDIO_CONTEXT) private readonly context: AudioContext
) {}
fetch(url: string): Promise<AudioBuffer> {
return new Promise<AudioBuffer>((resolve, reject) => {
if (this.cache.has(url)) {
resolve(this.cache.get(url));
return;
}
const request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
request.onerror = reject;
request.onabort = reject;
request.onload = () => {
this.context.decodeAudioData(
request.response,
buffer => {
this.cache.set(url, buffer);
resolve(buffer);
},
reject,
);
};
request.send();
});
}
}
AudioBufferSourceNode
, AudioBuffer
, :
export class WebAudioBufferSource extends AudioBufferSourceNode
implements OnDestroy {
@Input('buffer')
set bufferSetter(source: AudioBuffer | null | string) {
this.buffer$.next(source);
}
readonly buffer$ = new Subject<AudioBuffer | null | string>();
constructor(
@Inject(AudioBufferService) service: AudioBufferService,
@Inject(AUDIO_CONTEXT) context: AudioContext,
@Attribute('autoplay') autoplay: string | null,
) {
super(context);
this.buffer$
.pipe(
switchMap(source =>
typeof source === 'string'
? service.fetch(source)
: of(source),
),
)
.subscribe(buffer => {
this.buffer = buffer;
});
if (autoplay !== null) {
this.start();
}
}
ngOnDestroy() {
this.buffer$.complete();
try {
this.stop();
} catch (_) {}
this.disconnect();
}
}
, autoplay
audio
, .
AudioParam
— AudioParam
. GainNode
. . . — , . , AudioParam
. :
@Input('gain')
@audioParam('gain')
gainParam?: AudioParamInput;
:
export type AudioParamDecorator<K extends string> = (
target: AudioNodeWithParams<K>,
propertyKey: string,
) => void;
export function audioParam<K extends string>(
param: K,
): AudioParamDecorator<K> {
return (target, propertyKey) => {
Object.defineProperty(target, propertyKey, {
set(
this: AudioNode & Record<K, AudioParam>,
value: AudioParamInput,
) {
processAudioParam(
this[param],
value,
this.context.currentTime,
);
},
});
};
}
. AudioParamInput
? :
export type AudioParamAutomation = Readonly<{
value: number;
duration: number;
mode: 'instant' | 'linear' | 'exponential';
}>;
processAudioParam
API. , . 0 , 1 , — : {value: 1, duration: 1, mode: ‘linear’}
. .
, duration
, . . . , , :
@Pipe({
name: 'waAudioParam',
})
export class WebAudioParamPipe implements PipeTransform {
transform(
value: number,
duration: number,
mode: AudioParamAutomationMode = 'exponential',
): AudioParamAutomation {
return {
value,
duration,
mode,
};
}
}
AudioParam
. 1, LFO — Low Frequency Oscillator. . — . waOutput
, . exportAs
:
<ng-container waOscillatorNode frequency="0.2" autoplay>
<ng-container waGainNode gain="3000">
<ng-container [waOutput]="filter.frequency"></ng-container>
</ng-container>
</ng-container>
<ng-container waOscillatorNode autoplay [frequency]="note">
<ng-container
#filter="AudioNode"
waBiquadFilterNode
type="bandpass"
frequency="4000"
>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
</ng-container>
Web Audio API : , . :
https://stackblitz.com/edit/angular-web-audio
— -. DI:
export const TICK = new InjectionToken<Observable<number>>('Ticks', {
factory: () => interval(250, animationFrameScheduler)
.pipe(share()),
});
4 . beat
:
kick$ = this.tick$.pipe(map(tick => tick % 4 < 2));
true
false
. :
<ng-container
*ngIf="kick$ | async; else snare"
waAudioBufferSourceNode
autoplay
buffer="kick.wav"
>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
<ng-template #snare>
<ng-container
waAudioBufferSourceNode
autoplay
buffer="snare.wav"
>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
</ng-template>
. , 69 — . . :
const LEAD = [
70, 70, 70, 70, 70, 70, 70, 68,
68, 68, 68, 68, 75, 79, 80, 87,
87, 87, 87, 87, 87, 87, 87, 87,
87, 87, 87, 87, 84, 80, 79, 75,
80, 80, 80, 80, 80, 80, 80, 80,
80, 80, 80, 80, 79, 75, 72, 70,
70, 70, 70, 70, 70, 70, 70, 68,
72, 75, 79, 80, 80, 79, 79, 75,
];
:
notes$ = this.tick$.pipe(map(note => toFrequency(LEAD[note % 64])));
. — ADSR-. ADSR attack, decay, sustain, release, :

, . :
@Pipe({
name: 'adsr',
})
export class AdsrPipe implements PipeTransform {
transform(
value: number,
attack: number,
decay: number,
sustain: number,
release: number,
): AudioParamInput {
return [
{
value: 0,
duration: 0,
mode: 'instant',
},
{
value,
duration: attack,
mode: 'linear',
},
{
value: sustain,
duration: decay,
mode: 'linear',
},
{
value: 0,
duration: release,
mode: 'linear',
},
];
}
}
:
<ng-container *ngIf="notes$ | async as note">
<ng-container
waOscillatorNode
detune="5"
autoplay
[frequency]="note"
>
<ng-container
waGainNode
[gain]="0.1 | adsr : 0 : 0.1 : 0.02 : 0.5"
>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
</ng-container>
<ng-container
waOscillatorNode
type="sawtooth"
autoplay
[frequency]="note"
>
<ng-container
waGainNode
[gain]="0.1 | adsr : 0 : 0.1 : 0.02 : 0.5"
>
<ng-container
#feedback="AudioNode"
waGainNode
gain="0.7"
>
<ng-container waDelayNode delayTime="0.3">
<ng-container [waOutput]="feedback"></ng-container>
</ng-container>
<ng-container waConvolverNode buffer="response.m4a">
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
</ng-container>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
</ng-container>
</ng-container>
? . — ADSR-. , ConvolverNode
. . ConvolverNode
, . . , LFO waAudioParam
.
, . Web Audio API Angular — open-source- @ng-web-apis/audio.
Web Audio API canvas , — SVG.
Web APIs for Angular, — API Angular. , , Payment Request API MIDI- — .