Labing um teclado MIDI no Angular

A Web MIDI API é uma fera interessante. Embora já exista há quase cinco anos, ainda é suportado apenas pelo Chromium. Mas isso não nos impede de criar um sintetizador completo no Angular. É hora de levar a API de áudio da Web para o próximo nível!





Web Audio API Angular.



, , , ? 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 :



  1. , Observable, Geolocation API
  2. , 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




All Articles