1

I have a method that handles incoming MIDI messages just fine.

It listen on ALL input devices, and not just on the first one.

I would like to write it using RxJS observables.

Here is the working (non RxJS observables) one:

public initMidiInputBis() {
  if (navigator.requestMIDIAccess) {
    console.log('This browser supports MIDI');
    navigator.requestMIDIAccess().then(
      (midiAccess: WebMidi.MIDIAccess) => this.onMIDISuccess(midiAccess),
      (midiAccess: WebMidi.MIDIAccess) => this.onMIDIFailure(midiAccess)
    );
  } else {
    console.log('This browser does not support MIDI');
  }
}

private onMIDISuccess(midiAccess: WebMidi.MIDIAccess) {
  for (const inputDevice of Array.from(midiAccess.inputs)) {
    inputDevice[1].onmidimessage = (message: WebMidi.MIDIMessageEvent) => this.onMIDIMessage(message);
  }
}

private onMIDIFailure(error: any) {
  console.log('The browser could not access any MIDI device');
  console.log(error);
}

This above method works fine, the VMPK virtual keyboard is seen as an input MIDI device and its MIDI messages are received.

Here is my attempt at writing it again using observables:

public initMidiInput() {
  if (navigator.requestMIDIAccess) {
    console.log('This browser supports MIDI');
    const midiAccess$: Observable<WebMidi.MIDIAccess> = from(navigator.requestMIDIAccess());
    const midiInput$: Observable<WebMidi.MIDIInput> = midiAccess$.pipe(
      map((midiAccess: WebMidi.MIDIAccess) => {
        return midiAccess.inputs.values().next().value;
      })
    );
    midiInput$.pipe(
      flatMap((midiInput: WebMidi.MIDIInput) => this.midiMessageAsObservable(midiInput)),
    ).subscribe((message: WebMidi.MIDIMessageEvent) => {
      this.onMIDIMessage(message);
    });
    midiAccess$.subscribe(midiAccess => {
      this.logOutputDevices(midiAccess);
    });
  } else {
    console.log('This browser does not support MIDI');
  }
}

With this above method, the VMPK virtual keyboard is NOT seen as an input MIDI device.

This method contains the following line of code midiAccess.inputs.values().next().value; which only retrieves the first input MIDI device.

How can I consider ALL input MIDI devices ?

UPDATE: After adding some logger I can see the VMPK virtual keyboard is listed as an input device:

MIDIInput {onmidimessage: null, connection: "closed", id: "2A23249CBCD506F53AE4731A4FE186844C42191D517EC71DFC9813E84A1D2F87", manufacturer: "", name: "VMPK Output", …}
connection: "closed"
id: "2A23249CBCD506F53AE4731A4FE186844C42191D517EC71DFC9813E84A1D2F87"
manufacturer: ""
name: "VMPK Output"
onmidimessage: null
onstatechange: null
state: "connected"
type: "input"
version: "ALSA library version 1.1.6"

I find puzzling that its connection is closed while its state is connected.

Here is how I logged the above output:

const midiAccess$: Observable<WebMidi.MIDIAccess> = from(navigator.requestMIDIAccess());
const midiInput$: Observable<WebMidi.MIDIInput> = midiAccess$.pipe(
  map((midiAccess: WebMidi.MIDIAccess) => {
    Array.from(midiAccess.inputs.entries()).forEach(entry => {
      console.log('Key: ' + entry[0]);
      console.log(entry[1]);
    });
    return midiAccess.inputs.values().next().value;
  })
);
Stephane
  • 11,836
  • 25
  • 112
  • 175

2 Answers2

2

I could solve the issue with the following method:

public initMidiInput() {
  if (navigator.requestMIDIAccess) {
    console.log('This browser supports MIDI');
    const midiAccess$: Observable<WebMidi.MIDIAccess> = from(navigator.requestMIDIAccess());
    midiAccess$.pipe(
      flatMap((midiAccess: WebMidi.MIDIAccess) => {
        return midiAccess.inputs;
      }),
      map((midiInput) => {
        return midiInput[1];
      }),
      filter((midiInput: WebMidi.MIDIInput) => midiInput !== undefined),
      flatMap((midiInput: WebMidi.MIDIInput) => this.midiMessageAsObservable(midiInput)),
    ).subscribe((message: WebMidi.MIDIMessageEvent) => {
      this.onMIDIMessage(message);
    });
    midiAccess$.subscribe(midiAccess => {
      this.logOutputDevices(midiAccess);
    });
  } else {
    console.log('This browser does not support MIDI');
  }
}

private midiMessageAsObservable(inputDevice: WebMidi.MIDIInput): Observable<WebMidi.MIDIMessageEvent> {
  const source: Subject<WebMidi.MIDIMessageEvent> = new Subject<WebMidi.MIDIMessageEvent>();
  inputDevice.onmidimessage = (note: WebMidi.MIDIMessageEvent) => source.next(note);
  return source.asObservable();
}

I had to remove the call to the .next() method and return the value of the map.

Stephane
  • 11,836
  • 25
  • 112
  • 175
0

Try this out.

import { of, from, throwError } from 'rxjs';
import { map, switchMap, catchError } from 'rxjs/operators';

public initMidiInput() {
  if (navigator.requestMIDIAccess) {
    console.log('This browser supports MIDI');
    of(navigator.requestMIDIAccess())
    .pipe(
      map((midiAccess: WebMidi.MIDIAccess) => midiAccess.inputs),
      switchMap((inputs) => from(inputs)),
      catchError(error => {
        return throwError(error);
      })
    )
    .subscribe(inputDevice => {
      inputDevice[1].onmidimessage = (message: WebMidi.MIDIMessageEvent) => this.onMIDIMessage(message);
    }, error => {
        console.log('The browser could not access any MIDI device');
        console.log(error);
    });
  }
}

Veeraragavan
  • 343
  • 1
  • 8
  • I get the error message `The browser could not access any MIDI device` even if I had already fired up the `VMPK` virtual keyboard. It also complains of `TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.` on the line `console.log(error);` following right after the line `console.log('The browser could not access any MIDI device');` – Stephane May 27 '19 at 16:14