A chord chart that plays along with your DAW.
You type shorthand chords into one enormous text field. The DAW's MIDI clock drives everything else — tempo, transport, song position — and jamin highlights the chord that is sounding and sends it out as MIDI, so a room full of people can see and hear where the tune is right now.
It is not an arpeggiator and it is not a sequencer. It plays the chord. That's the point: everyone keeps track of what the hell is going on.
The whole setup is two things: bind your MIDI ports, and type. If it ever asks for more than that, it has failed.
| In a browser | tonygermaneri.github.io/jamin | needs Chrome, Edge or Opera for Web MIDI |
| Jamin.vst3 | macOS, universal | use this one in Ableton Live — the AU standard has no MIDI out |
| Jamin.component | macOS, universal | the instrument, for Logic and Reaper |
| Jamin MIDI FX.component | macOS, universal | Logic's MIDI FX slot, where nothing needs routing |
| Jamin.app | macOS, universal | the standalone. No DAW needed. |
| Jamin.vst3 | Windows x64 | and a standalone beside it |
Every macOS build is signed with a Developer ID and notarised, so they open with no warning and no right-click dance. They are on the releases page.
Windows and macOS do the same things, including sharing a chart between machines: one set of sources, with Winsock and BSD sockets behind the same few functions, proved by running the suite on both. The engine that works out what to play is carried with us rather than borrowed from the system, so a Windows plugin plays exactly what a macOS one does.
Needs Node 20.19+ or 22.12+ (Vite 7's floor).
npm install
npm run devWith no Node yet, npm run serve starts a static server that runs the app
without a build step (see below) and prints an address to open.
Note that index.html is the build entry: it imports vue by name, which no
browser can resolve on its own, so opening it without a bundler gets you a blank
page and a module-resolution error in the console. It now says so on the page
instead, and the no-build server serves the working entry at its root.
Open the printed URL in Chrome, Edge or Opera — Web MIDI is not available in Safari or Firefox. Grant the MIDI prompt and the ports bind themselves; the gear icon is there if you want different ones.
npm run build # production bundle in dist/, plus the plugin's compiler
npm test # pure-logic test suites
npm run chords # regenerate the chord dictionary dataThe same application, inside your DAW, reading the host's own playhead instead of a MIDI clock. The editor is this web app — there is no second implementation of anything, and no music theory in the C++ at all.
Building it needs CMake 3.22+, Ninja, and Xcode. JUCE is fetched by
the build; nothing else to install. With no Homebrew on the machine, all three
of Node, CMake and Ninja install into ~/.local without admin rights:
curl -fsSL https://nodejs.org/dist/v22.20.0/node-v22.20.0-darwin-arm64.tar.xz | tar xJ -C ~/.local/opt
curl -fsSL https://github.com/Kitware/CMake/releases/download/v3.31.6/cmake-3.31.6-macos-universal.tar.gz | tar xz -C ~/.local/opt
curl -fsSLo /tmp/ninja.zip https://github.com/ninja-build/ninja/releases/download/v1.12.1/ninja-mac.zip && unzip -o /tmp/ninja.zip -d ~/.local/binThen, from the repository root:
npm install && npm run build # the page the plugin will show
cmake -B native/build -G Ninja -S native
cmake --build native/build
ctest --test-dir native/build --output-on-failureThe web build comes first. The page is copied into each bundle at build
time, so there has to be a dist/ for it to read; CMake says so plainly if
there is not. cmake --build native/build --target web runs Vite for you.
The standalone is the quickest way to see it, and the only one that needs no DAW at all:
STANDALONE=native/build/plugin/JaminInstrument_artefacts/RelWithDebInfo/Standalone/Jamin.app
open "$STANDALONE"It opens the full application in its own window, with the host's transport replaced by the standalone's own. Audio and MIDI devices are chosen from its own options; no MIDI output is selected by default, which is deliberate — a development build should not start playing into whatever hardware happens to be switched on.
While iterating on the page, there is no need to rebuild the plugin at all.
JAMIN_WEB_DIR makes it serve the repository's dist/ instead of its own copy,
so npm run build and reopening the window is the whole loop:
open --env JAMIN_WEB_DIR="$PWD/dist" "$STANDALONE"--env is needed because open hands the app to LaunchServices, which does not
inherit the shell's environment — a plain VAR=... open sets nothing. Running
the executable inside the bundle directly works too, and keeps its output in the
terminal, which is where you want it when something has gone wrong:
JAMIN_WEB_DIR="$PWD/dist" "$STANDALONE/Contents/MacOS/Jamin"Two workflows, and each has its own paths filter so a stylesheet does not spend twenty minutes building four bundles.
.github/workflows/deploy.yml |
the page, to GitHub Pages, on every push to main |
.github/workflows/native.yml |
the plugins: macOS universal, Windows, and a release on a v* tag |
The plugin workflow runs the music suites first — a minute, no compiler — then
builds universal on macOS, checks every binary really is universal (a host under
Rosetta does not refuse an arm64-only plugin, it never lists it), signs,
validates with auval and pluginval, runs the host-cycle gates, and notarises
on a tag.
Signing is a capability the run either has or does not, never a step that fails for lacking a secret — so a pull request from a fork still builds and validates. The secrets it looks for:
MACOS_CERTIFICATE_P12, MACOS_CERTIFICATE_PASSWORD |
the Developer ID Application certificate, base64 |
MACOS_SIGN_IDENTITY |
which identity, when more than one is installed |
NOTARY_KEY_P8, NOTARY_KEY_ID, NOTARY_ISSUER_ID |
an App Store Connect API key — preferred |
NOTARY_APPLE_ID, NOTARY_PASSWORD, NOTARY_TEAM_ID |
an Apple ID and app-specific password — the fallback |
MACOS_INSTALLER_IDENTITY |
only for a signed .pkg; a different certificate from the one above |
Both signing and notarising also run by hand, with the same scripts CI uses — a signing path that only exists inside a workflow is one you cannot debug:
native/tools/macos-sign.sh native/build/plugin
VERSION=v0.1.0 native/tools/macos-notarize.sh native/build/plugin distcp -R native/build/plugin/JaminInstrument_artefacts/RelWithDebInfo/AU/Jamin.component ~/Library/Audio/Plug-Ins/Components/
cp -R "native/build/plugin/JaminMidiFx_artefacts/RelWithDebInfo/AU/Jamin MIDI FX.component" ~/Library/Audio/Plug-Ins/Components/
cp -R native/build/plugin/JaminInstrument_artefacts/RelWithDebInfo/VST3/Jamin.vst3 ~/Library/Audio/Plug-Ins/VST3/
killall -9 AudioComponentRegistrar # macOS caches the component registry
auval -v aumu Jam1 WvCt # the instrument
auval -v aumi JamF WvCt # the MIDI effectThe builds are universal — arm64 and x86_64 in one binary. That is not
a release nicety: an Apple-silicon host can still be launched under Rosetta, and
a host running as x86_64 does not merely refuse an arm64-only plugin, it
never lists it at all. Nothing appears in any log, and the result is
indistinguishable from a plugin that failed to build.
If Live does not list it, read its scanner log rather than guessing — it says why, in as many words:
grep -i -A3 jamin ~/Library/Preferences/Ableton/Live\ */PluginScanner.txtFailed to load plugin: a sealed resource is missing or invalid means the
bundle's code signature was broken after signing (the build re-seals it; a
hand-copied file would do it again). Otherwise the things to check, in order:
Preferences ▸ Plug-Ins ▸ Use VST3 Plug-In System Folders is on, then
Rescan; and if
your other plugins live in /Library/Audio/Plug-Ins/VST3 rather than your home
folder, put it there too — that needs sudo, so it is yours to run:
sudo cp -R native/build/plugin/JaminInstrument_artefacts/RelWithDebInfo/VST3/Jamin.vst3 /Library/Audio/Plug-Ins/VST3/That killall is not optional the first time a plugin code changes. Until the
registry is rebuilt, auval reports didn't find the component for a plugin
that is installed and entirely correct, which reads exactly like a build
failure and is not one.
What jamin is, is a MIDI effect: it makes no sound, it emits notes, and it belongs above the instrument it is playing. But the AU standard has no MIDI output, and that is not a JUCE limitation or a jamin one — it is Ableton's own statement about the format:
The Audio Unit (AU) plug-in standard does not support a direct MIDI out. […] To route MIDI from a plug-in, you should use the VST version.
So the format decides the host, and there is no arguing with it:
| Host | Use | Why |
|---|---|---|
| Ableton Live | VST3 | The only format in Live that can send MIDI to another track. Live has accepted note output from VST3 since Live 10, and every CC since Live 11. |
| Logic | Jamin MIDI FX (AU) | Logic's MIDI FX slot is the one place this needs no routing at all. |
| Bitwig, Cubase, Reaper | VST3 or the AU instrument | Both work; VST3 is the one that is tested. |
The AU instrument is still built and still validates, and it is still the right thing in Logic and Reaper — but in Live it cannot do the one job it exists for, so do not reach for it there.
In Live: put Jamin (VST3) on a MIDI track; that track now makes silence. On the track holding the sound you want, set MIDI From to the Jamin track, pick Jamin in the chooser just below it, and set Monitor to In. Repeat per track: one chart, one instance per track, a different phrase on each.
In Logic: Jamin MIDI FX in the MIDI FX slot above your instrument. Done.
Both are published by WaveContour, under the manufacturer code WvCt, so a
host lists them beside Waveshape rather than as a stranger's work.
A web view inside a plugin is not a browser tab, and two things it cannot do are worth knowing before they waste your time.
It cannot download a file. There is no download handling in the web view at all, so a link that saves something does nothing and does it silently. Anything that would have downloaded opens in your real browser instead.
It can read a file you choose. The file picker works, so the Chordonomicon import is: press Download it in your browser, download it there, come back and choose it. The database is shared by every instance in the process, so that is once per machine rather than once per track.
The plan and what it rests on · the drums · the native build
There are two ways to write a chart, and which you get depends on whether you use bar lines.
With bar lines it reads the way every fake book, lead sheet and iReal Pro chart reads, so a chart pasted from anywhere behaves as expected.
| You type | It means |
|---|---|
| Dm7 G7 | Cmaj7 | |
two chords splitting a bar, then a bar of Cmaj7 |
| C / Am / | |
each symbol is a beat, so two beats each |
| C | % | |
% repeats the bar before it |
| C | F | x | |
x repeats the two bars before it |
|: Am7 | Bbmaj7 :|16 |
sixteen times through |
Without bar lines you get a shorthand that is quicker to type, where a space is a bar.
| You type | It means |
|---|---|
C F G |
three bars |
C C F |
one chord lasting two bars, then F — not two attacks |
F,F- C |
half a bar of F, half of F minor, then a bar of C |
C / F |
/ holds C for another bar |
:Am7 Am7 Bbmaj7 Bbmaj7:16 |
sixteen times through |
The whole chart reads one way or the other rather than flipping halfway down:
a bar line anywhere switches it. [Verse 1] is a label for the reader either
way, and takes no time.
Quality. A- Am Ami Amin Aminor are all the same chord, and so are
A AM Ama Amaj Amajor. Also dim ° o, ø halfdim, aug +, alt,
mmaj minmaj -maj. Suspensions say what they mean: sus is sus4, sus2 is
sus2, sus4 is sus4.
Accidentals. Sharps may be written #, ♯ or s; flats b or ♭. So
Fs7 is F♯7, A/Cs is A/C♯, and As5 is A♯ with no third. The only place s
could be misread is sus, so an s is a sharp unless sus starts there:
Fsus4 is F suspended, and F♯sus4 is F#sus4 or Fssus4.
Harte notation works too, for importing corpora: C:maj7, C:min7,
C:hdim7, C:minmaj7, C:1, C:sus4(b7), and degree basses like C:maj/5.
Inside Harte form parentheses add a degree, so C:maj(9) is a triad plus a
ninth rather than a major ninth — while a plain C(9) keeps meaning what it
always did here.
Nothing at all. N, NC or N.C. is a bar with no chord in it. It still
takes up its time.
Sections. [Intro], [Verse 1], [Chorus] on their own mark the parts of
the song. A section runs from its label to the next one, so it is a span rather
than a caption — which is what lets a drum groove be bound to "the chorus"
rather than to a bar number that moves the moment you edit anything above it.
Two sections with the same name are two sections; a song has two choruses.
Drums. [d:name] plays that groove from there on, [d:nofill] stops the
fill into the next section, [d:none] takes the drums out. Grooves are usually
bound to sections in the drum book rather than written into the chart — the
chart says what the parts are, the book says what they sound like — and a
[d:...] overrides the binding from where it appears, the way a pedal mark
overrides the pedal switch.
A fill goes in the bar before every section change. That is what a drum chart has meant since long before there were corpora to draw on, and it is the same rule Band-in-a-Box has used for thirty years: the fill belongs to the boundary, not to the section, because "the end of the verse" is really "the fill into the chorus".
The sustain pedal. [p] holds it from there on, [np] lifts it again —
[n.p] and [n.p.] mean the same, because that is how a pianist writes it and
the dots are a nuisance to type. It goes down as each chord starts and comes up
on the change, so a chord rings for its length without smearing into the next,
and it follows whatever is actually sounding: the chord channel, and the
accompaniment channel too when a phrase is playing.
A mark takes effect from where it appears and holds until another one changes it, so "pedal from the bridge" is written once, at the bridge. Hold pedal for chord in the phrase book is what applies before the first mark and to a chart with no marks at all; a mark always beats the switch. Put marks between bars rather than inside one — like a section label, a mark inside a bar takes effect from the start of that bar.
Each is decided, and each has a test pinning it down.
- An accidental always belongs to the root, so
Bb5is a B-flat power chord. WriteB(b5)for B with a flattened fifth. sis a sharp unlesssusstarts there, soFsus4is never F♯ followed by nonsense. Nothing in any corpus examined spells a sharp suspension without the guard being decidable.- Degree basses (
/5,/b7) are read only in Harte form, because outside itC6/9is the six-nine chord and not C6 over a ninth.
One wart remains: Cmi is C minor, not C major in first inversion. Write C-i
or Cmini for the inversion.
Numbers. C5 is the triad without the 3rd; C3 is the triad without the
5th. Anything above 5 stacks diatonically: C7 C9 C11 C13. Colour tones work
the way you'd expect: C7b9 C7#9 C7#11 C7b13 C6/9 Cadd9 Cmaj7 C7alt.
Inversions. A roman-numeral suffix: Di first, Dii second, C7iii third.
The one ambiguity is Cmi, which stays C minor — write C-i or Cmini if you
want C minor in first inversion.
Slash bass. C/E. An accidental glued to the root always belongs to the
root, so Bb5 is a B-flat power chord; write B(b5) for a flattened fifth.
Phrases. A leading dot marks a phrase change: .C7{walkup}. See below.
MIDI clock carries tempo, start/stop and song position, but there is no standard MIDI message for time signature — no DAW can send it. Set beats per bar once in the Transport tab and forget about it.
The bookshelf icon. A progression is just a snippet of chart text with a name -- the same notation you type -- so anything in the library drops straight into a chart and any part of a chart can be saved back. Fifteen starters ship with it: ii–V–I, both blues, rhythm changes, Giant Steps, the Andalusian cadence, and so on.
The one thing done to a progression on the way in is transposition. Pick a key
and it moves, spelled the way that key is normally written -- D-7 G7 Cmaj7 into
F gives F-7 Bb7 Ebmaj7, not F-7 A#7 D#maj7. Only roots and slash basses are
rewritten; suffixes, inversions, commas, bar lines, phrase dots and bindings stay
exactly where they were. Insert at the cursor, on a new line, or over the whole
chart.
Import and export are JSON. The importer is deliberately forgiving about shape --
our own export, a bare array, {progressions: [...]}, entries using
title/chords instead of name/text, and Hugging Face's {rows: [...]}
envelope -- so a collection found elsewhere usually just goes in.
Chordonomicon is 679,807 progressions in a 252MB CSV. That is too much to ask a server for on your behalf, and far too much for a browser's ordinary storage, so the library links to the file and you hand it back: it is read as a stream, decoded and written to IndexedDB a few thousand rows at a time, and never held in memory. Rows are kept in the dialect they arrive in and converted only when one is looked at -- converting all of them on the way in would mean running the chord converter over seventy million words to produce something nobody has asked to see.
The library is paged for the same reason. The list shows a name and a length;
whatever is selected is shown in full beside it. Searching the imported set is a
scan, so it stops at the first few hundred matches and says so rather than
freezing. Its dialect differs from ours in exactly three ways, each confirmed
against the data rather than assumed: sharps are written s (Fs7 is F#7);
except when that s begins sus (Fsus4 is F sus4, and nothing in the corpus
contains ss, so F#sus4 never arises); and no3d means "no 3rd". Section tags
become [verse 1] labels on their own lines. Every chord symbol observed in the
corpus is covered by the parser, bar one that is corrupt at source -- that one
stays visibly unreadable in the chart rather than being quietly invented.
The data is CC-BY-NC-4.0, so jamin ships the converter and not the collection. The import tab has the URL for a ready-made slice.
It listens. Play, and the chord under your fingers is worked out while you are still holding it, then played back to you through a phrase -- so the answer is in the style of the song rather than in the style of a chord generator.
Turn it on with the ear icon, or in the phrase book under Playback. Two ways to sit with the chart, and it is a real choice rather than a default with an escape hatch:
- Play over the chart. The chart keeps its own chords and you play over the top, which is what a second player in the room is.
- My chords replace the chart's. While you are holding something, the chart's harmony gives way. The drums and the pedal still follow the song -- they follow the song, not your hands.
A written chord knows how long it lasts because the bar says so. A held one lasts until your hands move, so it is given a length and comes round again until you let go.
Nothing is recorded and nothing is kept. What you hear is what is being held.
What it hears is decided by a table, not by statistics, so a wrong answer
can be looked up rather than guessed at. Two rules in it are choices worth
knowing: a note the chord has no room for counts against it harder than a
missing one -- playing a note is evidence, leaving one out is only absence --
and the root is always a note you are actually playing. An E, a B♭ and a D is an
E half-diminished, which somebody really played, rather than a rootless C7 they
might have meant. @see src/core/chordDetect.js
A phrase is stored rooted on C -- that is, as degrees measured from the chord it was played over, not as the notes that were happened to be played. A phrase taken over Fm7 is filed as root, ♭3, 5, ♭7; play it back over Dm7 and you get D, F, A, C. The key it was born in is remembered but never used at playback. Phrases saved before this are migrated on load.
Getting it over a chord happens in that order, and the order matters:
-
Root first. Transpose so the phrase's root lands on the new chord's root. That is what keeps the degrees intact.
-
Shape second. Only if the new chord is a different shape does the minimal-movement map get involved -- and by then both chords share a root, so the root stays the root. Over Dmaj7 the ♭3 becomes a 3 and the ♭7 a 7; over Ddim7 the 5 becomes a ♭5. Notes that were never chord tones move with whichever chord tone they were leaning on, so approach notes stay approach notes.
Snap to chord notes then moves anything still outside the chord onto the nearest note that is in it. On by default: it guarantees every note fits. Turn it off and a passing tone stays where the harmony put it, which is more faithful to the phrase and less certain to fit under it.
-
Register last. The octave is chosen to sit closest to where the phrase was in the previous chord, so a figure repeating through a progression walks rather than leaps.
The phrase's rhythm is never touched. It runs at the rate it was played and
keeps time with the chart, and a chord simply decides the harmony for the stretch
of time it occupies. Under | Fm7 Gm7 | a one-bar pattern does not get rushed
through twice; the first half of it is heard as F minor and the second half as G
minor. A chord longer than the phrase hears the phrase more than once, still at
its own speed.
There are two other settings for this if you want them -- restarting the pattern on every chord, or stretching it to fill the chord exactly. Stretching is a tempo change by definition, which is why it is not the default, and why settings saved before it stopped being the default are corrected on load: a stored value beats a new default, so changing a default is not on its own enough to reach anyone who has run the app before.
Doing step 2 before step 1 -- which is what "minimal movement" means if you forget about the root -- silently rotates the degrees. From Fm7 to Dm7 the cheapest mapping leaves F where it is, and a lick that outlined the root comes out outlining the third. There is a test for exactly that.
A phrase applies from the chord it is bound to until the next chord wearing a
dot. Bindings live in the chart text — the dot you see above a chord is literally
the . you typed — so they survive copy, paste and reload.
src/core/chordParser.js shorthand -> root + interval stack
src/core/score.js text -> timeline of events, with source character ranges
src/core/voiceLeading.js minimal-movement chord mapping and phrase re-pointing
src/core/voicing.js interval stack -> actual MIDI notes
src/core/progressions.js the progression library, and transposition
src/core/vocParser.js reads Impro-Visor vocabulary files in the browser
src/core/licks.js the lick catalogue, built at run time
src/core/parts.js the two-handed parts catalogue
src/core/midiFile.js a small Standard MIDI File reader
src/core/midiPhrases.js cuts a performance into one-chord phrases
src/core/csvImport.js streams Chordonomicon's CSV in without loading it
src/core/progressionStore.js IndexedDB, so the whole collection fits
src/core/key.js key detection, Krumhansl-Schmuckler
src/core/midi.js Web MIDI: ports, clock, note IO
src/core/player.js clock in, chords and phrases out
src/canvas/layout.js fitting text to the width, caret and hit testing
src/canvas/textRenderer.js the 2D text layer
src/canvas/glRenderer.js the WebGL effects layer
Why a canvas. Text is fitted to the width — by default every line at the
size the longest one needs, so the chart reads as an even column, or with
dynamic line size each line scaled on its own so a single chord fills the
screen — and the highlight has to land on the exact word the user typed rather
than on a re-rendered copy of it. No DOM text control does either. So the chart is drawn
on canvas, with a fully transparent <textarea> on top: invisible, but a real
text control, so typing, IME, clipboard and the browser's own undo stack all
still work. Hit testing and vertical caret movement are ours, because the
browser's would use its own uniform layout instead of what's on screen.
The effects layer is one fragment shader behind the text. It is told where the interesting words are — playing, next, just finished — as rectangles, and lights them: a halo that breathes on the beat and a sweep tracking the bar, a charge building under the next chord, embers off the last one. Nothing paints opaquely over a glyph, and every term has a slider that goes to zero. Eight themes ship with matching shader presets.
Timing is in MIDI pulses (24 per quarter note) end to end, so nothing below the MIDI layer has to know about tempo.
Pinned to Vuetify 3 rather than 4 on purpose: the UI was written and reviewed
against the 3.x component API, and there was no way to run a build here to check
a major-version jump. npm install will pick up the latest 3.x.
The chord readout names what it thinks you typed using
ChordDictionary/SetTheory —
roughly 2000 pitch-class sets generated from Pascal's triangle and named by hand.
The parser itself is rule-based; the dictionary is for naming and for searching
(Settings → Notation). Regenerate src/data/chordSets.json with npm run chords.
GPL-3.0-or-later. See LICENSE.
Version 3 specifically, rather than 2: Impro-Visor's lick vocabulary is
"GPL v2 or later", so v3 is open to us, and our @mdi/font dependency is
Apache-2.0 — which is compatible with GPLv3 but not with GPLv2. Vue and Vuetify
are MIT, which is fine either way.
Licence before download, every time.
-
Impro-Visor (
src/data/My.voc, ~530KB of licks, cells and idioms) is GPL-2.0-or-later, so it is bundled here verbatim assrc/data/My.voc, with attribution. It is the closest thing to a sibling project: its vocabulary auto-transposes to the chord of the moment, which is what Mr. Accompany Me does. Shipping it unmodified also keeps the licence question simple -- there is no derived artifact to account for, becausesrc/core/vocParser.jsreads the original at run time. -
Chordonomicon is CC-BY-NC-4.0 on Hugging Face — non-commercial, which is an added restriction the GPL does not permit, so it cannot be bundled or redistributed here. (The GitHub repo's Apache-2.0 covers the code, not the data.) jamin therefore ships the converter, not the data: download it yourself and paste it into the importer. Your own use stays within CC-BY-NC.
-
The Groove MIDI Dataset (
src/data/grooveDrums.json, 6.5MB of drum grooves) is CC-BY-4.0, which is one-way compatible with GPLv3 — so it is the one corpus here that can be bundled outright, and it is. What ships is 1,150 whole human performances — 391 songs, 647 fills, 112 beats, from one bar to 639 — with nothing cut and nothing thrown away. The only change is the timing, rounded to jamin's 24 pulses per quarter. It cost 5MB of bundle to keep them whole and it was the right trade: these are takes, and a take cut at an arbitrary bar line is not a shorter take, it is a broken one. Attribution and method are insrc/data/grooveDrums.LICENSE, andscripts/extract_groove.mjsrebuilds it from the original. -
Your own drum library is imported, never bundled. Commercial MIDI packs and internet scrapes are both squarely in this category — a library you bought is yours to use and not ours to ship.
In the browser, pointing at a folder copies what is in it into that browser's own database. In the plugin it does not copy at all: an index is built and the files play from where they already live. Point at one pack and it becomes one library; point at a folder of fifty packs and each becomes its own, because a note map belongs to a vendor rather than to a collection. A collection of 774,000 files across 25,000 folders indexes to about 160MB and takes a while, so it is a batch job: stopping leaves the libraries that finished alone, and starting again finishes the one it stopped in. Nothing is copied into the project and nothing leaves the machine.
The tree is walked a rung at a time rather than asked for in one answer. JUCE returns a native function's result by inlining it into a JavaScript source string for
evaluateJavaScript, so a whole tree — twenty-five thousand paths — is a megabyte and a half of source for a single call, and when that fails the page sees an empty list, which is indistinguishable from a folder with no drums in it. One folder at a time is at most a couple of hundred names. @seewalkLibrary, and the fake filesystem it is tested against. -
Several jazz corpora that fit the notation almost perfectly state no licence at all and are transcriptions of copyrighted songs. Import-only, never bundled.
The built-in progressions are generic idioms written by hand for this project, and carry no third-party claim.
python3 scripts/import_check.py /path/to/a/drum/library
Not part of npm test, because it needs a library and nobody's is the same. It
is what you run after touching anything on the import path, and it exists
because that path broke four times in a row with every unit test passing:
a whole tree asked for in one bridge call and failing as silence; bytes handed
over in an encoding the page could not decode, so every file was read
successfully and thrown away; the genre read from a path with the library's own
name already stripped off it; and a kit verdict of "General MIDI" given to packs
where General MIDI can read none of the notes.
None of those is visible in a unit test with a hand-written fixture, and all four are obvious the moment the real path meets a real library.
Its assertions are about invariants rather than counts, which took two attempts.
"Three or more distinct genres" passed while every single-level pack came back
blank, because the inner folders still said GM - Blues. "Two or more different
kit verdicts" passed while whole percussion packs were labelled General MIDI
with nothing General MIDI could play. What catches those is the share of rows
carrying a genre, and the coverage of each verdict.
The pure-logic suites run through macOS JavaScriptCore (scripts/jsrun.py)
rather than Node. They started that way because there was no Node on the
machine this was written on, and they have stayed that way for a better reason:
running them outside a browser and outside Node is a standing proof that the
chord parser, the timeline and the voice leading have nothing browser-shaped in
them — which is exactly what the plugin depends on. They cover the chord parser,
the timeline, voice leading, voicings, playback, the host bridge and text
layout; everything except the DOM, Web MIDI and WebGL, which need a browser.