strudel-scalenav
v0.6.1
Published
Connect Strudel live-coding patterns to Scale Navigator ensembles. Sign in, join a live room, and sample the host's current scale, chord, and BPM as Strudel signals.
Downloads
2,132
Maintainers
Readme
strudel-scalenav
Connect your Strudel live-coding patterns to Scale Navigator ensembles. Sign in, join a live room, and receive the host's current scale, chord, and BPM as live values you can sample in your Strudel patterns — so your algorithmic music stays harmonically locked to whoever's driving the ensemble.
- Read-only client. Strudel users consume harmonic state; the host still drives.
- Live. Updates stream via Firestore
onSnapshot, so pattern output follows the host in real time. - Signed in. Google or email/password — your display name shows up in the ensemble alongside Dashboard users.
- MIT-licensed, one-line import from the Strudel REPL.
Quick start
Paste this into strudel.cc:
const { signInWithGoogle, joinEnsemble, getCurrentUser } = await import('https://cdn.jsdelivr.net/npm/[email protected]/dist/strudel-scalenav.js')
if (!getCurrentUser()) await signInWithGoogle()
const ens = await joinEnsemble('your-room-id')
n("0 2 4 <[6,8] [7,9]>")
.scale(ens.strudelScale)
.sound("piano")Whenever the Scale Navigator host changes the scale or chord, your pattern reharmonizes on the next cycle — no re-evaluation needed.
Installation
In the Strudel REPL (the normal case)
const sn = await import('https://cdn.jsdelivr.net/npm/[email protected]/dist/strudel-scalenav.js')In a Vite / bundler project using @strudel/core
npm install strudel-scalenav firebaseimport { joinEnsemble, signInWithGoogle } from 'strudel-scalenav'You must have @strudel/core's signal available in global scope (the REPL does this for you; in a bundler project, expose it yourself: globalThis.signal = signal).
Signing in
Sign-in is required. Everyone who joins an ensemble shows up to the host by name. Pick one:
await signInWithGoogle()
// or
await signUpWithEmail('[email protected]', 'your-password', 'Your Display Name')
// or (for returning users)
await signInWithEmail('[email protected]', 'your-password')Check if you're already signed in:
const user = getCurrentUser()
if (!user) await signInWithGoogle()Joining an ensemble
You need a room ID. The host gets this when they create an ensemble in the Scale Navigator Dashboard or mobile app.
const ens = await joinEnsemble('nathan-jam-session')Leave gracefully:
await ens.leave()The ens API
Hierarchical API (recommended)
The API is organized into clear namespaces: ens.scale.* and ens.chord.*.
ens.scale.*
| Method | Description |
|---|---|
| ens.scale.pitch(i) | i-th scale note in octave 4 |
| ens.scale.arp(n) | Arpeggiate scale notes at n notes per cycle |
| ens.scale.block() | All scale notes sounded together |
ens.scale.thirds.*
Stack thirds from the scale root (index 0 of the scale). Uses the current scale root, not the chord root.
| Method | Description |
|---|---|
| ens.scale.thirds.pitch(i) | i-th stacked third from scale root |
| ens.scale.thirds.arp(n) | Arpeggiate thirds from scale root |
| ens.scale.thirds.block(count) | Block chord of count stacked thirds |
ens.chord.voicing.*
Access the host's original chord voicing (preserves octave placement).
| Method | Description |
|---|---|
| ens.chord.voicing.pitch(i) | i-th voicing note (original octaves) |
| ens.chord.voicing.arp(n) | Arpeggiate voicing at n notes per cycle |
| ens.chord.voicing.block() | All voicing notes as block chord |
ens.chord.closed.*
Close-position chord (all pitch classes in octave 4).
| Method | Description |
|---|---|
| ens.chord.closed.pitch(i) | i-th chord note in close position |
| ens.chord.closed.arp(n) | Arpeggiate closed position |
| ens.chord.closed.block() | All closed position notes as block chord |
ens.chord.thirds.*
Stack thirds from the chord root using the current scale. Continues up in octaves (9th is above 7th, not wrapped back down).
| Method | Description |
|---|---|
| ens.chord.thirds.pitch(i) | i-th stacked third from chord root |
| ens.chord.thirds.arp(n) | Arpeggiate thirds from chord root |
| ens.chord.thirds.block(count) | Block chord of count stacked thirds |
Stacked thirds index reference:
| Index | Interval | Example (C major scale, chord root = C) | |---|---|---| | 0 | root | C4 | | 1 | 3rd | E4 | | 2 | 5th | G4 | | 3 | 7th | B4 | | 4 | 9th | D5 | | 5 | 11th | F5 | | 6 | 13th | A5 | | 7 | root (2 oct) | C6 |
All index methods support negative indices: -1 = last, -2 = second to last, etc.
Flat API (aliases)
For backwards compatibility and quick access, flat methods are also available. Prefer the hierarchical API for new code.
| Flat Method | Equivalent |
|---|---|
| ens.scalePitch(i) | ens.scale.pitch(i) |
| ens.chordPitch(i) | ens.chord.voicing.pitch(i) |
| ens.chordVoicingPitch(i) | ens.chord.voicing.pitch(i) |
| ens.closedPitch(i) | ens.chord.closed.pitch(i) |
| ens.chordClosedPitch(i) | ens.chord.closed.pitch(i) |
| ens.stackThird(i) | ens.chord.thirds.pitch(i) |
| ens.arpScale(n) | ens.scale.arp(n) |
| ens.arp(n) | ens.chord.voicing.arp(n) |
| ens.arpVoicing(n) | ens.chord.voicing.arp(n) |
| ens.arpClosed(n) | ens.chord.closed.arp(n) |
| ens.arpThirds(n) | ens.chord.thirds.arp(n) |
| ens.block() | ens.chord.voicing.block() |
| ens.blockVoicing() | ens.chord.voicing.block() |
| ens.blockClosed() | ens.chord.closed.block() |
| ens.blockThirds(count) | ens.chord.thirds.block(count) |
Scale data
| Property | Type | Description |
|---|---|---|
| ens.scaleRoot | signal (0–11) | Pitch class of scale root |
| ens.scaleRootNote | signal (MIDI) | Scale root in octave 4 (playable) |
| ens.scalePitchClasses | getter number[] | Pitch classes 0–11 |
| ens.scaleNotes | getter number[] | Scale notes in octave 4 (playable) |
| ens.strudelScale | signal string | For .scale(), e.g. "C:major" |
| ens.scaleName | signal string | Raw ID, e.g. "c_diatonic" |
| ens.scaleClass | signal string | e.g. "diatonic", "harmonic_minor" |
| ens.scaleRootName | signal string | e.g. "C", "F#" |
| ens.scalePretty | getter string | Pretty display: "E♭ Diatonic" or "[0,2] Whole Tone" |
Chord data
| Property | Type | Description |
|---|---|---|
| ens.chordRoot | signal (0–11) | Pitch class of chord root |
| ens.chordRootNote | signal (MIDI) | Chord root in octave 2 (bass) |
| ens.chordVoicing | getter number[] | Original voicing (MIDI notes) |
| ens.chordPitchClasses | getter number[] | Pitch classes 0–11 |
| ens.chordClosed | getter number[] | Close position in octave 4 (playable) |
| ens.chordRootName | signal string | e.g. "A", "Db" |
| ens.chordType | signal string | e.g. "M7", "_13#9-110" |
| ens.chordNoteNames | getter string[] | e.g. ["G3", "Db4", "F#4"] |
| ens.chordPretty | getter string | Pretty display: "A♭ m7", "F♯ 13♯9" |
Other
| Property | Type | Description |
|---|---|---|
| ens.bpm | signal | Host's BPM |
| ens.hostName | getter | Who's hosting |
| ens.roomName | getter | Room display name |
| ens.roomId | string | Room ID you joined |
| ens.state | getter | Full internal state object |
| ens.leave() | async | Leave the ensemble gracefully |
Visual helpers
| Property/Method | Type | Description |
|---|---|---|
| ens.scaleColor | getter string | Current scale color (hex or rgb) |
| ens.showBadge(options?) | function | Display scale badge in REPL header |
Options for showBadge: x (default 480), y (default 38), size (default 26), showText (default true)
note(ens.chord.voicing.arp(4)).s("piano") // 4 notes per cycle
note(ens.chord.voicing.block()).s("piano").slow(2) // block chordAuth functions
| Function | Description |
|---|---|
| signInWithGoogle() | Google popup sign-in |
| signInWithEmail(email, password) | Email sign-in (returning users) |
| signUpWithEmail(email, password, displayName) | Create new account |
| signOut() | Sign out |
| getCurrentUser() | Get current user or null |
| onAuthChange(callback) | Listen for auth state changes |
| setDisplayName(name) | Update your display name |
Pattern recipes
Play in the host's scale
n("0 2 4 <[6,8] [7,9]>")
.scale(ens.strudelScale)
.sound("piano")Arpeggiate the chord voicing
note(ens.chord.voicing.arp(4)).sound("piano")Arpeggiate stacked thirds (1-3-5-7...)
note(ens.chord.thirds.arp(4)).sound("piano")Chord with bass note
stack(
note(ens.chordRootNote).slow(2), // bass
note(ens.chord.voicing.arp(4)) // arpeggio
).sound("piano")Block chords
note(ens.chord.voicing.block())
.struct("x ~ x ~ x x ~ x")
.sound("piano")Block chord of stacked thirds (7th chord)
note(ens.chord.thirds.block(4)) // root, 3rd, 5th, 7th
.struct("x ~ x ~")
.sound("piano")Generative melody
n(irand(8).segment(8))
.scale(ens.strudelScale)
.sound("piano")Arpeggiate the scale itself
note(ens.scale.arp(8)).sound("piano")Sync to host's BPM
note(ens.chord.voicing.arp(4))
.sound("piano")
.cpm(ens.bpm.div(4))Display scale badge
ens.showBadge() // Shows polygon + scale/chord names in header
n("0 2 4 6")
.scale(ens.strudelScale)
.sound("piano")The badge automatically positions to the right of "REPL (warm)" and updates when scale/chord changes.
Caveats
- Scale → Strudel mapping. All 7 pressing scale classes map to Strudel equivalents:
diatonic→major,harmonic_minor→harmonic minor,harmonic_major→harmonic major,acoustic→lydian dominant,whole_tone→whole tone,octatonic→diminished,hexatonic→augmented. - Read-only. This package never writes changes back to the room.
- Auth required. Anonymous access isn't enabled.
Development
git clone https://github.com/nathanturczan/strudel-scalenav
cd strudel-scalenav
npm install
npm run buildPublishing
npm run build
npm publishRelated
- Scale Navigator Dashboard — the host app
- Strudel — browser-based live coding
- TidalCycles — the pattern language Strudel ports to JS
License
MIT © Nathan Turczan. See LICENSE.
