npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

effect-web-midi

v1.0.0

Published

Effect WEB MIDI API wrapper

Readme

Effect Web MIDI API wrapper

Huge thanks to Web MIDI spec!

Early Adopters

  1. Etude (GitHub, Web site) -- A self-hosted piano practice tool built by Michael Vessia (GitHub @michaelvessia, Web Site) that scores your performance against sheet music. Connect a MIDI keyboard, load a piece, play through it, and get real-time feedback on accuracy.
  2. And, hopefully, You 😉 🫶 Please 🙏

Potential Adopters

  1. chris-albert/midi-structor
  2. Search on GitHub for effect and requestMIDIAccess...

Devices the lib was manually tested on

  1. Novation Launchpad X
  2. KORG NanoPad v1
  3. If you tested it on some devices not listed here, feel free to report your results through GitHub issues

Demo

'use client';
import { Atom, Result, useAtom, useAtomValue } from '@effect-atom/atom-react';
import { styled } from '@linaria/react';
import * as Cause from 'effect/Cause';
import * as Console from 'effect/Console';
import * as Effect from 'effect/Effect';
import { flow, pipe } from 'effect/Function';
import * as Record from 'effect/Record';
import * as Stream from 'effect/Stream';
import * as EMIDIAccess from 'effect-web-midi/EMIDIAccess';
import * as EMIDIInput from 'effect-web-midi/EMIDIInput';
import * as EMIDIOutput from 'effect-web-midi/EMIDIOutput';
import * as EMIDIPort from 'effect-web-midi/EMIDIPort';
import * as Parsing from 'effect-web-midi/Parsing';
import * as Util from 'effect-web-midi/Util';

const nanoPadInputId = EMIDIInput.Id(
  '380CB1F6BC0A21C5AAA0CDF95170A996937337454636CDDE223EEA6812283B07'
);

const nanoPadOutputId = EMIDIOutput.Id(
  '045297AACE044DA224B59E92DBC0E874160B85D0D43BE505CBB053747F4AA90A'
);

const globalDataDumpRequest = new Uint8Array([
  0xf0, 0x42, 0x40 /* 4g */,
  //
  0x00, 0x01, 0x03, 0x00,
  //
  0x1f, 0x0e, 0x00, 0xf7,
]);

const _MIDIDeviceConnectionEventsStringLog = pipe(
  // The lib supports both ways of working with access. You can either work with
  // its instance directly, or provide it through DI. All of the functions have
  // equivalents supporting both approaches. In this case we work with it directly
  EMIDIAccess.request(),
  //          ⬆️ Effect<
  //               EMIDIAccess.EMIDIAccessInstance,
  //               | AbortError
  //               | UnderlyingSystemError
  //               | MIDIAccessNotSupportedError
  //               | MIDIAccessNotAllowedError,
  //               never
  //             >
  Effect.catchTag(
    'MIDIAccessNotSupportedError',
    flow(Console.log, Effect.andThen(Effect.never))
  ),
  // notice that function below consumed access. It can consume access
  // regardless if it's wrapped in effect or not. It also has both
  // data-first and data-last signatures supported. Every function in this lib
  // conforms that rule
  EMIDIAccess.makeAllPortsStateChangesStream(),
  //          ⬆️ Stream<
  //               {
  //                 readonly _tag: "MIDIPortStateChange";
  //                 readonly cameFrom: EMIDIAccess.EMIDIAccessInstance;
  //                 readonly capturedAt: Date;
  //                 readonly newState: {
  //                   readonly ofDevice: MIDIPortDeviceState;
  //                   readonly ofConnection: MIDIPortConnectionState;
  //                 };
  //                 readonly port: EMIDIOutput | EMIDIInput;
  //               },
  //               | AbortError
  //               | UnderlyingSystemError
  //               | MIDIAccessNotAllowedError,
  //               never
  //             >
  // This is just a helper to render a list of objects to a string in nice-ish way
  Util.mapToGlidingStringLogOfLimitedEntriesCount(50, 'latestFirst', (_) => ({
    time: _.capturedAt.toISOString(),
    id: _.port.id.slice(-10),
    device: _.newState.ofDevice.padStart(12),
    connection: _.newState.ofConnection.padStart(7),
  }))
  // ⬆️ Stream<
  //      string,
  //      | AbortError
  //      | UnderlyingSystemError
  //      | MIDIAccessNotAllowedError,
  //      never
  //    >
);

const MIDIMessageEventsStringLog = pipe(
  // if the ID below turned out to be output's, it would cause defect (by design),
  // because messages can only come from inputs. Notice that instead of demanding
  // access from arguments, this particular method expects it to be provided in
  // requirements.
  EMIDIInput.makeMessagesStreamById(nanoPadInputId),
  //         ⬆️ Stream<
  //              {
  //                readonly _tag: "MIDIMessage";
  //                readonly cameFrom: EMIDIInput;
  //                readonly capturedAt: Date;
  //                readonly midiMessage: Uint8Array<ArrayBuffer>;
  //              },
  //              PortNotFoundError,
  //              EMIDIAccess
  //            >
  Parsing.withParsedDataField,
  //      ⬆️ Stream<
  //           {
  //             readonly _tag: "ParsedMIDIMessage";
  //             readonly cameFrom: EMIDIInput;
  //             readonly capturedAt: Date;
  //             readonly midiMessage: | NoteRelease
  //                                   | NotePress
  //                                   | UnknownReply
  //                                   | ControlChange
  //                                   | TouchpadRelease
  //                                   | PitchBendChange
  //           },
  //           PortNotFoundError,
  //           EMIDIAccess
  //         >
  // function below adds new member to the midiMessage union inferred from
  // ControlChange, PitchBendChange, and TouchpadRelease
  Parsing.withTouchpadPositionUpdates,
  Stream.filter(
    (a) =>
      a.midiMessage._tag === 'Touchpad Position Update' ||
      a.midiMessage._tag === 'Touchpad Release' ||
      a.midiMessage._tag === 'Note Press' ||
      a.midiMessage._tag === 'Note Release'
  ),
  // Just for the demo, show only select list of events
  Util.mapToGlidingStringLogOfLimitedEntriesCount(
    50,
    'latestFirst',
    (current) => ({
      time: current.capturedAt.toISOString(),
      id: current.cameFrom.id.slice(-10),
      ...current.midiMessage,
    })
  ),
  // ⬆️ Stream<string, PortNotFoundError, EMIDIAccess>
  Stream.catchTag('PortNotFound', (err) =>
    Stream.fromEffect(
      EMIDIPort.FullRecord.pipe(
        //      ⬆️ Effect<
        //           {
        //             [id: EMIDIPort.Id<'input'>]: EMIDIPort<'input'>
        //           } & {
        //             [id: EMIDIPort.Id<'output'>]: EMIDIPort<'output'>
        //           },
        //           never,
        //           EMIDIAccess.EMIDIAccess
        //         >
        Effect.map(
          Record.reduce(
            `Port with id=${err.portId} is not found. Currently available ports: \n`,
            (acc, { type, version, name }, id) =>
              acc + type.padEnd(7) + id + ' ' + version + ' ' + name + '\n'
          )
        ),
        Effect.flatMap(Console.log),
        Effect.andThen(
          `KORG nanoPAD not found in the list of connected devices`
        )
      )
    )
  ),
  // ⬆️ Stream.Stream<string, never, EMIDIAccess>
  Stream.provideLayer(EMIDIAccess.layerSystemExclusiveSupported),
  // ⬆️ Stream.Stream<
  //   string,
  //   | AbortError
  //   | UnderlyingSystemError
  //   | MIDIAccessNotSupportedError
  //   | MIDIAccessNotAllowedError,
  //   never
  // >
  Stream.catchTag('MIDIAccessNotSupportedError', (e) =>
    Stream.succeed(e.cause.message)
  )
);

const dumpRequester = Atom.fn(() =>
  // A particular example of methods flexibility. Here EMIDIAccess.send is a
  // data-first signature and accepts EMIDIAccess wrapped by Effect
  EMIDIAccess.send(
    EMIDIAccess.request({ sysex: true }),
    //          ⬆️ Effect.Effect<
    //                 EMIDIAccess.EMIDIAccessInstance,
    //                 | AbortError
    //                 | UnderlyingSystemError
    //                 | MIDIAccessNotAllowedError
    //                 | MIDIAccessNotSupportedError,
    //                 never
    //             >
    nanoPadOutputId,
    // ⬆️ string & Brand<"MIDIPortId"> & Brand<"output">
    globalDataDumpRequest
    // ⬆️ Uint8Array<ArrayBuffer>
  )
);

const textAtom = Atom.make(
  // MIDIDeviceConnectionEventsStringLog,
  MIDIMessageEventsStringLog
).pipe(Atom.keepAlive);

export default function Home() {
  const text = useAtomValue(textAtom);
  console.log(text);
  const [, mutate] = useAtom(dumpRequester);

  return (
    <Wrapper>
      <form
        onSubmit={(event) => {
          event.preventDefault();
          mutate();
        }}
      >
        <input type='submit' value='dump' />
      </form>

      {Result.match(text, {
        onFailure: (_) => (
          <>
            failure:
            <br />
            {Cause.pretty(_.cause)}
          </>
        ),
        onInitial: (e) => <>initial waiting: {e.waiting.toString()}</>,
        onSuccess: (s) => <pre>{s.value}</pre>,
      })}
    </Wrapper>
  );
}

const Wrapper = styled.div`
  color: wheat;
`;

photo_2025-11-17_22-00-08

TODO

  1. tests (useful example)