Web MIDI API is an interesting beast. Although it has been around for almost five years, it is still supported only by Chromium. But that won't stop us from creating a full fledged synthesizer in Angular. It's time to take the Web Audio API to the next level!
, , , ? 80- β MIDI. , Chrome . , , MIDI-, , . , . , - Angular.
Web MIDI API
API, . MIDI- navigator
Promise
. β β EventTarget
. MIDIMessageEvent
, Uint8Array
. 3 . status byte. , . , , β . MIDI. Angular Observable
, Web MIDI API RxJs.
Dependency Injection
, MIDIAccess
-, . navigator
Promise
, RxJs Observable
. InjectionToken
, NAVIGATOR
@ng-web-apis/common. :
export const MIDI_ACCESS = new InjectionToken<Promise<MIDIAccess>>(
'Promise for MIDIAccess object',
{
factory: () => {
const navigatorRef = inject(NAVIGATOR);
return navigatorRef.requestMIDIAccess
? navigatorRef.requestMIDIAccess()
: Promise.reject(new Error('Web MIDI API is not supported'));
},
},
);
MIDI-. Observable
:
- ,
Observable
, Geolocation API - ,
Promise
Observable
, . :
export const MIDI_MESSAGES = new InjectionToken<Observable<MIDIMessageEvent>>(
'All incoming MIDI messages stream',
{
factory: () =>
from(inject(MIDI_ACCESS).catch((e: Error) => e)).pipe(
switchMap(access =>
access instanceof Error
? throwError(access)
: merge(
...Array.from(access.inputs).map(([_, input]) =>
fromEvent(
input as FromEventTarget<MIDIMessageEvent>,
'midimessage',
),
),
),
),
share(),
),
},
);
- , , MIDIAccess
. :
export function outputById(id: string): Provider[] {
return [
{
provide: MIDI_OUTPUT_QUERY,
useValue: id,
},
{
provide: MIDI_OUTPUT,
deps: [MIDI_ACCESS, MIDI_OUTPUT_QUERY],
useFactory: outputByIdFactory,
},
];
}
export function outputByIdFactory(
midiAccess: Promise<MIDIAccess>,
id: string,
): Promise<MIDIOutput | undefined> {
return midiAccess.then(access => access.outputs.get(id));
}
, ,Provider[]
, ? providers@Directive
, :
providers: [
outputById(βsomeIdβ),
ANOTHER_TOKEN,
SomeService,
]
Angular β .
, .
. , .
:
- . , . , .
- . . , , .
:
export function filterByChannel(
channel: MidiChannel,
): MonoTypeOperatorFunction<MIDIMessageEvent> {
return source => source.pipe(filter(({data}) => data[0] % 16 === channel));
}
Status byte 16: 128β143 (noteOn
) 16 . 144β159 β (noteOff
). , 16 β .
, :
export function notes(): MonoTypeOperatorFunction<MIDIMessageEvent> {
return source =>
source.pipe(
filter(({data}) => between(data[0], 128, 159)),
map(event => {
if (between(event.data[0], 128, 143)) {
event.data[0] += 16;
event.data[2] = 0;
}
return event;
}),
);
}
MIDI-noteOff
-, .noteOn
. ,noteOn
. status byte 16,noteOff
-noteOn
, .
, , :
readonly notes$ = this.messages$.pipe(
catchError(() => EMPTY),
notes(),
toData(),
);
constructor(
@Inject(MIDI_MESSAGES)
private readonly messages$: Observable<MIDIMessageEvent>,
) {}
!
Web Audio API, . , .
. , . scan:
readonly notes$ = this.messages$.pipe(
catchError(() => EMPTY),
notes(),
toData(),
scan(
(map, [_, note, volume]) => map.set(note, volume), new Map()
),
);
, ADSR-. . , ADSR β :
, , , .
@Pipe({
name: 'adsr',
})
export class AdsrPipe implements PipeTransform {
transform(
value: number,
attack: number,
decay: number,
sustain: number,
release: number,
): AudioParamInput {
return value
? [
{
value: 0,
duration: 0,
mode: 'instant',
},
{
value,
duration: attack,
mode: 'linear',
},
{
value: sustain,
duration: decay,
mode: 'linear',
},
]
: {
value: 0,
duration: release,
mode: 'linear',
};
}
}
, , attack. sustain decay. , release.
:
<ng-container
*ngFor="let note of notes | keyvalue; trackBy: noteKey"
>
<ng-container
waOscillatorNode
detune="5"
autoplay
[frequency]="toFrequency(note.key)"
>
<ng-container
waGainNode
gain="0"
[gain]="note.value | adsr: 0:0.1:0.02:1"
>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
</ng-container>
<ng-container
waOscillatorNode
type="sawtooth"
autoplay
[frequency]="toFrequency(note.key)"
>
<ng-container
waGainNode
gain="0"
[gain]="note.value | adsr: 0:0.1:0.02:1"
>
<ng-container waAudioDestinationNode></ng-container>
<ng-container [waOutput]="convolver"></ng-container>
</ng-container>
</ng-container>
</ng-container>
<ng-container
#convolver="AudioNode"
waConvolverNode
buffer="assets/audio/response.wav"
>
<ng-container waAudioDestinationNode></ng-container>
</ng-container>
keyvalue
, . , . β ConvolverNode
. , , . Chrome:
https://ng-web-apis.github.io/midi
MIDI- β .
, MIDI iframe: https://stackblitz.com/edit/angular-midi
Angular RxJs. Web MIDI API DOM-. MIDI Angular . open-source @ng-web-apis/midi. , Web APIs for Angular. β API Angular . , , Payment Request API Intersection Observer β .
, Angular Web MIDI API β Jamigo.app