2.0.2 • Published 4 years ago

@midiate/midiate v2.0.2

Weekly downloads
6
License
Apache-2.0
Repository
-
Last release
4 years ago

MIDIate

MIDIate is a platform of MIDI-based apps, designed to be easily extensible (via React).

Visit midiate.now.sh to see the latest version.

Features

  • Broad MIDI-based platform with intuitive React APIs
  • Uses WebMIDI interface to instantly allow connection
  • Several built-in apps to get started

APIs

Basic app example

Here's a quick guide of how to write a simple app using the most common hooks.

In this example we assume the app is located in /src/apps/<app-name>.js.

Import APIs and other libraries

import React, { useCallback, useEffect } from 'react'
import MusicNoteIcon from '@material-ui/icons/MusicNote'
import { useLastEvent } from '../api/events'
import { useNotes } from '../api/notes'
import { useChords } from '../api/chords'
import { useSessionValue } from '../api/settings'

Define session values

// it's recommended to put settings in a common place
const useNotesHistory = () => 
  useSessionValue('notesHistory', [])

Export main view (with more detailed information)

/* show played notes and chords */
export default function () {
  const notes = useNotes()
  const [chords,] = useChords()
  const [notesHistory,] = useNotesHistory()

  return (
    <div>
      <b>chords:</b> 
      <ul>
        {chords.map(chord => <li>{chord}</li>)}
      </ul>
      <b>notes:</b>
      <ul>
        {notes.map(note => <li>{note}</li>)}
      </ul>
      <b>history:</b>
      <ul>
        {notesHistory.map(note => <li>{note}</li>)}
      </ul>
    </div>
  )
}

Export a background task to collect notes in the background

/* collect notes even when not on main app view */
export function BackgroundTask() {
  const lastEvent = useLastEvent()
  const [, setNotesHistory] = useNotesHistory()

  // add note to history whenever a new note is played
  useEffect(() => {
    // lastEvent is null on the first run
    if (!lastEvent)
      return

    if (lastEvent.messageType === 'noteon') { 
      setNotesHistory(notesHistory => {
        const newHistory = [...notesHistory]
        return newHistory.concat(lastEvent.note)
      })
    }
  }, [lastEvent, setNotesHistory])

  // always render nothing from background tasks
  return null
}

Export a status bar component with some aggregated data

/* shows history count on status bar */
export function StatusBar() {
  const [notesHistory,] = useNotesHistory()  // don't need to set

  return notesHistory.length
}

Lastly, export config

// make app accessible with a friendly name
export const config = {
  id: "NOTES_VIEWER",
  name: "Notes Viewer",
  icon: MusicNoteIcon,
}

Using this as a reference, let's go through the APIs.

Semantics

There are several concepts in MIDIate:

  • The UI consists of two parts: the app view and a status bar.
  • Apps are JS libraries that export React components.

    Apps get control in three different ways - as a BackgroundTask, in the StatusBar or as a full app (default export).

    • System apps are special apps that have broad effects on the system.
  • Hooks are used as the default APIs of MIDIate. They use React Hooks in order to provide the relevant arguments.

    All hooks exist under /src/api.

    • useLastEvent, useNotes and useChords are examples of (the most popular) hooks.

Writing an app

As we described, apps are the breathing core of MIDIate. They can access all the events in the system and show processed data to the user in several ways.

Each app has to follow a specific structure:

  1. Provide an export to a config object (see "app config" below for details and options)
  2. Provide an export to default (main view), StatusBar (cross-app status bar), BackgroundTask (invisible background processing unit), or a combination of these. The exported should be valid React elements.

App hooks

All the hooks are under /src/api, separated to logical libraries.

api/events.js

Receive and send MIDI events.

Events are the bare elements of MIDIate and are the base for the rest of the musical data.

  • useLastEvent()->{...} - returns the last event received from any input (see "events" below for details).
  • useSendEvent() - returns a function with the signature sendCustomEvent(deltaTime, msg, appId) to send ("dispatch") curated MIDI messages from apps.

api/notes.js

Notes are smarter version of events.

They track note_on and note_off events to provide a coherent list of currently-played notes. The hooks also provide a convenient heuristic regarding the possible notes the player intended to play (mostly useful for chord-based scenarios).

  • useNotes(config?)->[notes...] - returns a list of currently-played notes.

    Optional config can be provided with {data: "simple" | "extended"} as an argument. While "simple" mode only returns the played notes, "extended" mode returns the events that triggered those notes.

  • useSmartNotes(config?)->{events,id} - returns a list of played notes with an associated "detection ID". Written with chord recognition in mind. config has same behavior as useNotes().

    The return value respects some assumptions:

    • events only change when notes are added or change - not when they are removed (unless all notes are removed)
    • id only changes when all notes change

api/chords.js

Chords are detected sequences of notes.

They use useSmartNotes() and return information about the currently-played chords.

We use @tonaljs/chord-detect (with minor alterations) as our detector.

  • useChords(filterFunction?)->[chords,id] - returns a tuple of possible chord detections ([chord1, chord2, ...]) and a detection ID that changes when the player completely lets go of the piano.
    • Optional filterFunction receives a list of events and returns note names (C4, Ab6 etc.)
  • useRelativeChords(relativeScale, filterFunction?)->[chords,id] - returns a tuple similar to useChords(), but normalized to roman notation if present (Em6 is IIIm6 when C is the relative scale).

api/settings.js

Convenience wrappers for app configurations and inter-process communication between the app components.

Let's say you want to load a resource in your background task and show this on the status bar. You would possibly configure a redux store in order to send pieces of data between them. Also, you might want to serialize the user-selected resource to localStorage, allowing it to survive a page refresh.

This is exactly the functionality the settings APIs provide.

  • useSetting(name, defaultValue)->[value,setValue] - behaves a lot like useState() but automatically serializes the written value to localStorage. Allows cross-component settings (for instance, a setting set in the main view can be read using the same name in the status bar or the background task).
  • useSessionValue(name, defaultValue)->[value,setValue] - similar to useSetting() but only stores the data for until the refresh. Useful for volatile states (loading a resource, timed states etc.)

api/context.js

Lets apps use their configuration.

  • useConfig()->{...} returns a config object for the current app.

    Can be used in a BackgroundTask, in the StatusBar and in the default export.

api/midi.js

Gives apps access to the raw WebMIDI APIs.

  • useMidiOutputs()->[outputs...] - returns a list of WebMIDI outputs.

App configuration

Most apps can just export a simple config with a name and an icon. The full config format is:

{
  // app id (required and should be unique)
  id: "EXAMPLE",
  // app name (required when shows in menu)
  name: "Example App", 
  // custom app icon for app menu (optional)
  icon: ReactComponent,
  // should app show in app menu? (optional)
  showInMenu: bool, // = true 
  // override default status bar onClick action (optional, defaults to this app page)
  statusBarAction: string, //. e.g. SETTINGS_APP_ID or DEFAULT_APP_ID (from /src/constants.js) 
}

Events

MIDI events have the following structure:

{
  // time between last message and this message in ms
  deltaTime:  float,
  // frequency of MIDI note
  freq:  float,
  // key of MIDI note [0-127]
  key:  int,
  // parsed message type ("noteon", "noteoff", "programchange", "controlchange", etc.)
  messageType:  string,
  // parsed note name ("C5", "G#3", etc.)
  note:  string,
  // source of midi message
  source:  {
	// source type ("midi", "keyboard", "server", "app")
	type: string,
	// ...extra per-source data (id and name for "midi", host for "server", id for "app"
  },
  // ...extra per-message data (velocity, program, pressure, etc.)

  // raw message data
  _data:  Uint8Array(3),
  // parsed MIDI message code [0-255]
  _messageCode:  int,
}

Events are first parsed with MIDIMessage and then enriched with @tonaljs and some custom logic.

Apps vs. System apps

The two are actually very similar. The main difference is the logical association (system apps provide cross-app/core functionality, while regular apps let users experience a specific narrative).

The only technical subtlety is that system apps can provide an export to settings that automatically show in the settings app - while regular apps need to write their own settings layout.

If you're about to write an app, unless you're trying to expand the core functionality (event collection, output methods, traversal etc.) - you probably want to write a regular app.

Development

To get started with MIDIate locally, just follow these three steps: 1. clone the repository (and cd into the directory) 2. run yarn 3. run yarn start.

Browse http://localhost:3000 in order to see in in development mode, or run yarn build in order to compile.

MIDIate is based on create-react-app.

Frequently Asked

  • Can I use MIDIate as an external package?

    It's currently not possible to easily use MIDIate APIs as a library/npm package, but future versions may support a @midiate/api import that makes it easier.

  • Is it possible to connect to MIDIate remotely?

    The package currently supports WebSockets as an external MIDI source. It means it's easy mostly within the LAN.

    Check out midiate-utils for compatible streaming servers (depending on your operating system, they may require system libraries).