readme
Route based midi generation.
quickstart
Web MIDI needs a secure context, so use localhost or https. Any static server
works; there is nothing to build.
It is published from main to https://bobbymeyer.github.io/gridi/, which is
the secure context the browser wants and the copy the embed below loads. That
needs Pages switched on once, under Settings β Pages β Build and deployment β
Source: GitHub Actions; the workflow cannot do it for itself.
embedding
gridi builds itself into an element, so it can run somewhere that is not its
own page β a note on bobbymeyer.com, for instance.
// Before the import, so the module does not start itself against #app.
window.__gridiEmbedded = true;
const { initGridi, destroyGridi } = await import('https://bobbymeyer.github.io/gridi/src/main.js');
initGridi(element, {
embedded: true, // theme the mount, not the host page's <html>
mark: false, // drop the lockup, keep the transport
storagePrefix: 'note:' // its own patch, not the one you are working on
});
destroyGridi(); // stops the clock, the audio and the MIDI ports
Every lookup is scoped to the mount, so an embedded copy cannot collect the
host page's elements. Its stylesheet is a whole application's worth of chrome
though β a * reset, rules on html, body β so mount it in a shadow root and
let the two documents keep their own styling. tests/embed.html does exactly
that, and is the check that neither side reaches the other.
Give the mount a height. gridi fills it, and lays itself out to the width it
finds there rather than the window's, so the same copy works in a column of
text and on a phone.
Embedded, gridi waits to be clicked into before it answers the keyboard, the
wheel or a paste, and lets go the moment you click out β a reader is reading,
and a figure that swallows space, βS and the scroll wheel has taken the page
over. On its own page it takes all three from the start.
The SoundFont it ships with is thirty-two megabytes, and it is fetched on the
first press of play rather than on load, so a page that merely mentions gridi
costs its readers nothing.
Open http://localhost:8080/tests/audio-check.html for the synth checks, which
need a browser.
the grid
A grid cell is worth a note value. A pulse crosses one cell in that much
musical time, so a line's length is how long its pulses take to walk it.
Moving a node retimes the patch.
| Setting |
Effect |
| Grid |
What one cell is worth: 1/1 to 1/32, plus 1/4T, 1/8T, 1/16T. Default 1/16. |
The field is ruled in three weights: light for the cell, medium for the beat,
heavy for the bar. They follow the grid setting.
Select a line to read its length in cells and what that comes to in beats.
| Node |
What distance does to it |
| Split |
Fans out on one instant; each branch lands by its own length. Level branches arrive together, a row apart flams. |
| Logic |
Its feeds must be drawn the same length, or the pulses it catches never coincide. |
| Param, Node scope |
Writes when the pulse passes it; the target reads when its own pulse arrives. Place it near its target, or use Signal scope, which rides with the pulse. |
| Key, latched |
Writes the project key. Notes read it when they sound, so a chord change lands on the bar however far the pulses have to travel. |
nodes
| Node |
In |
Out |
Does |
| Pulse |
β |
many |
Emits on a beat at project BPM. Owns the feel and the starting key. |
| MIDI In |
β |
many |
A note played on an attached keyboard sends a pulse from here. |
| Split |
1 |
many |
Sends one pulse down every outgoing line at once. |
| Logic |
many |
many |
Fires when incoming pulses line up inside a window. |
| Chance |
1 |
many |
Lets a pulse through a set percentage of the time. |
| Router |
1 |
many |
Sends each pulse down a single outgoing line. |
| Note |
1 |
many |
Fires a scale degree as MIDI on every channel the line carries. |
| Voice |
1 |
many |
Two oscillators, a resonant filter and an ADSR, in the browser. |
| LFO |
1 |
many |
Sends a moving value down its lines many times a beat. |
| Param |
1 |
many |
Changes a value instead of firing a note, then passes the pulse on. |
| Key |
1 |
many |
Rewrites scale and key for everything downstream. |
Pulse
| Control |
Range |
| Active |
on / off |
| Division |
1/1, 1/2, 1/4, 1/8, 1/16, 1/32, 1/2T, 1/4T, 1/8T, 1/16T, 1/4., 1/8. |
| Swing |
0β60%, delays every second step |
| poly steps / in the time of |
1β16 each, steps against divisions |
| Humanize |
0β40 ms |
| Euclid |
hits, steps, rotate |
| Key from |
project / this node |
| Base ch |
1β16 |
| Velocity |
1β127 |
MIDI In
| Control |
Range |
| Listen on |
0β16, zero is omni |
| Played note |
Key, Transpose, Gate |
| Centre |
0β127, the note that means no transposition |
| Use velocity |
on / off |
| Base ch |
1β16 |
Split, Logic, Chance, Router
| Node |
Control |
Range |
| Split |
Stagger |
0β0.5 beat between branches |
| Logic |
Mode |
AND, OR, N, XOR |
| Logic |
Needs |
1β8, in N mode |
| Logic |
Window |
1β120 ms |
| Chance |
Pass |
0β100% |
| Chance |
Mode |
Free, Drift |
| Router |
Mode |
Cycle, Ping, Rand, No rpt |
Note
| Control |
Range |
| Degree |
β21 to 22 |
| Octave |
β1 to 8 |
| Overflow |
Extend, Fold, Clamp |
| Velocity |
0β127, zero follows the line |
| Length |
0.02β4 beats |
| Ratchet |
1β8 |
| Send MIDI |
on / off |
| Audible |
on / off, the built-in blip |
Voice
| Control |
Range |
| Degree, Octave, Overflow |
as Note |
| Wave A / B |
sine, triangle, square, sawtooth |
| Octave A / B |
β3 to 3 |
| Semitones A / B |
β12 to 12 |
| Detune A / B |
β50 to 50 cents |
| Level A / B |
0β1 |
| Cutoff |
80β12000 Hz |
| Reso |
0.1β20 |
| Env depth |
0β4 octaves |
| Attack |
0.001β2 s |
| Decay |
0.005β2 s |
| Sustain |
0β1 |
| Release |
0.005β3 s |
| Gate |
0.02β4 beats |
| Level |
0β1 |
| Voices |
1β32, oldest stolen past the limit |
LFO
| Control |
Range |
| Shape |
Sine, Triangle, Ramp, Saw, Square, Random, Drift |
| Cycle |
1/16, 1/8, 1/4, 1/2, 1 bar, 2 bars, 4 bars, 8 bars |
| Phase |
0β100% of a turn |
| Depth |
0β100% |
| From / To |
0β127 each |
| Steps |
1β48 values per beat |
| Reset on pulse |
on / off |
| Base ch |
1β16 |
An LFO is patched into a Param node, which sends the value. Note, Voice, Logic,
Chance and Router ignore waves; Split, Key and Param act on them.
Param
| Control |
Range |
| Scope |
Node, Signal, CC |
| Target / Param |
in Node scope |
| Param |
Velocity, Transpose, Degree shift, in Signal scope |
| Controller |
0β127, in CC scope |
| Mode |
Seq, Rand, Walk, Add |
| Values |
space separated, in Seq mode |
| Amount |
in Add and Walk modes |
| Min / Max |
outside Seq mode |
Key
| Control |
Range |
| Mode |
Set, Cycle, Rand |
| Key / Scale |
in Set mode |
| Changes |
root:scale pairs, e.g. 0:minPent 5:major |
| Transpose |
β24 to 24 semitones |
| Latch |
writes the project key |
lines
A line carries state, not just a connection.
| Property |
Behaviour |
| Channels |
A set, each with its own output, transpose and velocity. One pulse fans out across all of them. |
| Channel mode |
Inherit passes on what arrived; Set here overrides from this line down. |
| Scale and key |
Same two modes. Set here retunes everything the line feeds. |
| Travel |
Read-only. The line's length in cells, and what that comes to in beats. |
| Extra delay |
0β4 beats on top of the travel time, for anything the grid cannot say. |
| Mute |
Stops passing pulses without unpatching. |
Scales: Major, Nat Minor, Harm Minor, Mel Minor, Dorian, Phrygian, Lydian,
Mixolydian, Locrian, Maj Pent, Min Pent, Blues, Whole Tone, Chromatic.
Degree overflow: Extend carries past the end of the scale into the next
octave, Fold wraps inside one octave, Clamp stops at the top.
library
Library is the second tab in the header, and it is the patch's tab as well
as the shelves': the patch's name and what can be done to it β new, demo, keep,
save, open, sounds β sit over two rows of patches. Picking one opens it like
any other patch, undo included, and the row stays where it is, so the shelf is
still there to try the next one from.
ships with is the five below, each with what it is. Copy on one puts it
on the row underneath under a name of its own β "Bossa Nova copy" β and leaves
the canvas alone, because copying is not opening.
yours is what this browser has been asked to keep. Keep puts the patch
on the grid there, under whatever it is called; keeping it again under the same
name replaces it, which is what saving twice means anywhere else. Rename
edits the name in the cell it is in, and takes the patch's own name with it;
renaming onto a name already on the shelf is refused rather than allowed to
overwrite it. Remove takes one off, and asks first, because that is the one
thing here that undo cannot reach. They live in this browser's local storage and nowhere else β no account,
no sync, gone with the site data. Save is still how a patch leaves the
machine.
Gridi opens on Bossa Nova out of that library, so a first visit arrives at a
patch doing the thing the app is for rather than at an empty grid. Demo is
the other starting point: a patch of built-in voices, which needs no SoundFont
to make a sound.
| Patch |
BPM |
What it is |
| Bossa Nova |
132 |
Comp, bass, melody and a latched key on ii-V-I. The clave is five lines of different lengths out of one Split. |
| Samba |
100 |
A bateria β surdo, caixa, ganzΓ‘, tamborim, agogΓ΄, cuΓca β with a cavaquinho chopping off the beat, a bass that pushes into the surdo, and a flute over the top. Four bars of i-iv-i-v, all minor sevenths, so one shape in the cavaquinho's hand is right in every bar. |
| House |
124 |
Four to the floor, offbeat bass and a stab. The open hat shares the kick's clock and is drawn an eighth longer. |
| Hip-Hop |
88 |
Boom-bap kit with swung hats. One clock a bar, three kicks at their own distances. |
| Ambient |
60 |
Five loops of 5, 7, 11, 13 and 16 beats, after the tape loops on Music for Airports. They come back into line once every 22 hours. |
The figures the percussion patches use, in sixteenths from the downbeat:
| Figure |
Hits |
| Bossa clave, 3-2 |
0, 6, 12, 20, 28, over two bars |
| Samba surdo |
0 muffled, 4 open, in every 2/4 bar β the stroke everything else leans on |
| Samba caixa |
sixteenths throughout, accented 0, 3, 6 of every eight: three, three, two |
| Samba tamborim |
0, 2, 3, 5, 9, 11, 12, 14, over two 2/4 bars |
| Samba cavaquinho |
3 and 6 of every 2/4 bar |
| Hip-hop kick |
0, 7, 10 |
Drum voices are pinned with a chromatic line rooted at C, so a Note node's
degree is a General MIDI note number and the key never moves them.
Which drum matters as much as which hit. General MIDI's standard kit has a mute
surdo and an open one at 86 and 87, a cuΓca at 78 and 79, a cabasa at 69 and a
pair of timbales at 65 and 66 β a samba played on a low tom and a tambourine
instead is a drum kit doing an impression of a bateria, which is what this one
used to be. The cavaquinho is General MIDI's banjo, its nearest neighbour.
what keeps them moving
Every one of these patches hangs a few LFOs off the side of itself, wired to
nothing that sounds. Each drifts one number on one node β a clock's velocity, a
Chance node's odds, the rotation of a Euclid figure β and because they are
wired to nothing, they cannot move anybody's timing.
Velocity is the one worth understanding. A SoundFont routes it to filter cutoff
as well as to loudness, so a part played at one velocity is not merely
undynamic: it is one timbre, from the first bar to the last. That is what made
these sound like a demonstration rather than like playing. A CC sweep is no
help β a controller goes out of the MIDI port and straight past the player
built into Gridi, which reads a font's velocity and key routings and nothing
else.
The shape is drift, which eases between hashed values instead of coming
round: the twentieth bar is not the fourth, and the same seed gives the same
twentieth bar every time. Rates that share no factors β one bar against two
against four β mean the parts are never in the same place twice. Rendered for a
minute and a half, every channel of every patch now plays at between twenty
and seventy velocities, and no bar is an exact repeat of an earlier one; both
are checked in tests/library.test.js.
Patches live in patches/, listed in patches/index.json. Add one by saving a
patch into that folder and adding a line to the index. The ones that ship were
built by the scripts in tools/, because an offset is a distance and solving
several of them is arithmetic:
soundfont
Gridi ships with GeneralUser GS 2.0.3 by S. Christian Collins, and fetches
it on the first press of play. 261 instruments and 13 drum kits in 32MB β too
much to spend on somebody who has not asked to hear anything yet, which is what
loading it on boot spent.
https://www.schristiancollins.com/generaluser
His licence is in soundfont/LICENSE.txt, unaltered. It permits use in
software projects and redistribution, and asks that nobody link directly to his
download files β which is why the copy is in this repository rather than
fetched from his site. If you get use out of it,
buy him a coffee.
Drop any other .sf2 on the canvas to play through that instead.
|
|
| Format |
SoundFont 2, sixteen or twenty-four bit. Compressed .sf3 is not read |
| Stereo |
A zone naming one half of a pair plays both, placed left and right. A font that names both itself keeps its own panning |
| A dropped font |
Kept in IndexedDB, so it survives a reload. forget it, in the Sounds panel, goes back to the bundled one |
| Channel 10 |
Looked up in bank 128, where a General MIDI font keeps its kits |
| Modulators |
Velocity and key number, over the two default routings. A font's own replace a default that reads and writes the same things |
| Not read |
Modulators sourced from the wheel, the pedals, aftertouch or the bender, since none of them reach the internal player. Reverb and chorus sends, which Gridi has nowhere to put. Samples that live in a synthesiser's ROM, which a file holds the header of and none of the sound |
Velocity goes through the font rather than around it: it reaches loudness and
filter cutoff by the routings the font carries, so an instrument gets darker as
well as quieter as you play softer.
Samples are decoded when a preset is first played and kept after, so a
thirty-megabyte font costs the file plus the few sounds a patch uses.
With no font at all, Note nodes audition through a single triangle oscillator.
That is a click track for building a patch, not an instrument.
sounds
Sounds, on the library tab, lists the channels a patch plays on, worked out
from the graph, and sets a General MIDI program for each. Gridi sends them as
program changes 50ms before the first note, so a receiving module is on the
right sound before it has anything to play.
|
|
| Numbers |
0β127, as they go down the wire. Every printed GM chart counts from 1 |
| Channel 10 |
Offered as kits rather than instruments |
| Leave as it is |
No entry, no program change: the device keeps whatever it had |
Bank select is not sent, so a module with more than 128 sounds needs its bank
chosen on the device.
What the library asks for:
| Patch |
ch 1 |
ch 2 |
ch 3 |
ch 4 |
ch 10 |
| Bossa Nova |
|
Acoustic Bass |
Acoustic Guitar (nylon) |
Flute |
|
| Samba |
|
Acoustic Bass |
|
|
Standard Kit |
| House |
|
Synth Bass 1 |
Electric Piano 1 |
|
Electronic Kit |
| Hip-Hop |
|
Electric Bass (finger) |
|
Vibraphone |
Standard Kit |
| Ambient |
Pad 2 (warm) |
|
|
|
|
Ambient also sends CC 74 from an LFO, which is filter cutoff by convention.
patches
A patch is one JSON file: nodes, lines, tempo, grid, key. Name it on the
library tab; Save names the file after it, and Keep puts it on the
shelf there under the same name.
| To open one |
How |
| A file |
Drop it anywhere on the canvas, or press Open |
| A SoundFont |
Drop the .sf2 on the canvas too, replacing the bundled one |
| Its text |
Paste it onto the canvas |
Opening replaces the canvas and goes on the undo stack. βZ puts back what was
there.
Files are stamped "app": "gridi". Anything that is not a patch is declined,
not opened. Files saved before the stamp are recognised by their shape and
still open.
Device bindings are not in the file. A patch names output slots AβD; each
machine binds its own devices.
MIDI
Every control below is on the MIDI in & out tab, the second of the two in
the header.
| Control |
Does |
| Enable MIDI |
Requests access. Needs a user gesture and a secure context. |
| A B C D |
Picks which output slot the device list and Clk apply to |
| Device list |
Binds a device to that slot |
| Clk AβD |
Whether that output receives clock |
| Clock |
Whether clock is sent at all |
| Panic |
All notes off on every output |
| MIDI in device |
The port clock and played notes arrive on |
| Sync |
Follow the incoming clock instead of the project tempo |
Outputs are named slots. A patch stores the slot letter; each machine binds its
own devices, remembered between sessions.
Sent: note on, note off, control change, program change, clock at 24 PPQN,
start, stop, continue, song position.
Received: clock, start, stop, continue, song position, note on.
On a single channel, note length is capped by the retrigger rate β two instances
of one pitch on one channel cannot overlap. Use different pitches or channels.
controls
| Action |
Control |
| Play / stop |
Space, or Play |
| Place a node |
Click a type in the left rail, then click the grid. Shift keeps placing. |
| Show the node names |
The chevron beside nodes. The rail keeps to its colours until then, and remembers which way it was left. |
| Fold the inspector away |
The chevron in its head. Selecting something opens it again. |
| Project settings, the library, MIDI ports |
The three tabs in the header |
| Patch two nodes |
Drag from a node's right edge onto another node |
| Select |
Click a node or a line |
| Move a node |
Drag it |
| Pan |
Drag empty grid, or alt-drag |
| Zoom |
Wheel |
| Fit to patch |
F |
| Set what a cell is worth |
Grid, in the project tab |
| Open a patch |
Drop the file on the canvas, or paste its text |
| Open a patch that ships with Gridi |
The library tab |
| Choose what each channel plays |
Sounds, on the library tab |
| Keep a patch in this browser |
Keep, on the library tab |
| Copy a patch that ships |
Copy, on its cell, which puts it under yours |
| Rename a kept patch |
Rename, on its cell |
| Take a kept patch off the shelf |
Remove, on its cell |
| Play through a SoundFont |
Drop a .sf2 on the canvas |
| Delete selection |
Del or Backspace |
| Duplicate node |
D |
| Mute selected line |
M, or double-click the line |
| Undo / redo |
βZ / β§βZ |
| Save patch |
βS, or Save |
| Cancel |
Escape |
New, Demo, Save, Open and Sounds sit with the patch name on the library tab.
The theme toggle is in the footer, with the level and the counts: what is
app-wide rather than about this patch or the machine it is played on. The
transport β play and the bar count β is the rule between the settings and the
grid, and holds nothing else. Patches autosave to local storage.
Embedded in someone else's page, every key and the wheel above waits until the
app has been clicked into, and stops again when it is clicked out of.
limits
Past these, Gridi drops what it cannot carry and says so in a banner.
| Limit |
Ceiling |
| Scheduler events |
2000 per tick, 8000 per second sustained |
| MIDI messages |
2000 per second, per device |
| Sounding voices |
64 across the patch |
| Patch size |
400 nodes, 800 lines |
Repeated overloads stop the transport. Note-offs and clock are never dropped.
task recipes
drive a DAW
- Open a virtual MIDI port: IAC Driver on macOS, loopMIDI on Windows.
- Press Enable MIDI, pick slot A, choose that port.
- Set the DAW to receive on it, and to external sync if it should follow the
tempo.
- Select the line feeding your Note nodes, set channels to Set here, pick
the channels.
drive two instruments from one pulse
- Bind slot A and slot B to different devices.
- Select the line, Set here, pick output A and its channels.
- Switch the output selector to B and pick its channels.
- Give the B channels a transpose if the second instrument sits in another
register.
send a filter sweep
- Place an LFO and a Param node, and patch LFO into Param.
- On the Param node, set Scope to CC and Controller to the number the
instrument listens on β 74 is filter cutoff by convention.
- Select the line between them, Set here, pick the output and channel.
- On the LFO, set Cycle to the sweep length and From/To to the range.
- Patch a Pulse into the LFO to restart the sweep in time.
put two parts in step
- Patch a Split into two Note nodes.
- Put both Note nodes in the same row as each other. Equal lengths, so their
pulses arrive together.
- Drag one of them a row down and play it again: the line got longer, so that
part is now late. Select the line to see by how much.
- Use the Grid setting to scale the whole patch at once β 1/16 halves every
travel time, 1/4 doubles it.
share a patch
- Give it a name on the library tab.
- Save. The file lands in your downloads, named after the patch.
- Send the file, or its text.
- They drop it on their canvas, or paste it there.
follow another sequencer
- Choose the master's port under MIDI in.
- Press Sync. The tempo readout follows the master and the project BPM is
left alone.
- Press play on the master.
play the patch from a keyboard
- Choose the keyboard's port under MIDI in.
- Place a MIDI In node and patch it into a Note node.
- Set Played note to Key to retune the patch from the pitch played,
Transpose to shift it, Gate to ignore the pitch.
- Start the transport; played notes need it running.
tech
Vanilla ES modules. No build, no dependencies. Web Audio for the built-in
voices and the SoundFont player, Web MIDI for output and input. The SoundFont
parser is src/sf2.js and touches no browser API, so it is read and checked
outside one.
npm test runs 350 tests under node --test. engine, model, music,
rhythm, voice, sync, lfo, limits, shelf and geometry have no DOM,
audio or MIDI dependencies and are tested directly; the engine runs against a fake clock
and stub outputs. The synth is checked in a browser at
tests/audio-check.html.
Web MIDI: Chrome, Edge, Opera, Firefox 108+. Not Safari and not any iOS
browser, which all run WebKit β those get the built-in voices only.