polyphon 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +374 -2
- package/README.zh-TW.md +375 -0
- package/dist/engine/automation.d.ts +24 -0
- package/dist/engine/automation.d.ts.map +1 -0
- package/dist/engine/automation.js +55 -0
- package/dist/engine/automation.js.map +1 -0
- package/dist/engine/compile.d.ts +36 -0
- package/dist/engine/compile.d.ts.map +1 -0
- package/dist/engine/compile.js +103 -0
- package/dist/engine/compile.js.map +1 -0
- package/dist/engine/jukebox.d.ts +113 -0
- package/dist/engine/jukebox.d.ts.map +1 -0
- package/dist/engine/jukebox.js +287 -0
- package/dist/engine/jukebox.js.map +1 -0
- package/dist/engine/mixer.d.ts +29 -0
- package/dist/engine/mixer.d.ts.map +1 -0
- package/dist/engine/mixer.js +141 -0
- package/dist/engine/mixer.js.map +1 -0
- package/dist/engine/output.d.ts +14 -0
- package/dist/engine/output.d.ts.map +1 -0
- package/dist/engine/output.js +51 -0
- package/dist/engine/output.js.map +1 -0
- package/dist/engine/playback.d.ts +97 -0
- package/dist/engine/playback.d.ts.map +1 -0
- package/dist/engine/playback.js +311 -0
- package/dist/engine/playback.js.map +1 -0
- package/dist/engine/prepare.d.ts +6 -0
- package/dist/engine/prepare.d.ts.map +1 -0
- package/dist/engine/prepare.js +12 -0
- package/dist/engine/prepare.js.map +1 -0
- package/dist/engine/render.d.ts +14 -0
- package/dist/engine/render.d.ts.map +1 -0
- package/dist/engine/render.js +60 -0
- package/dist/engine/render.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/instruments/bowed.d.ts +357 -0
- package/dist/instruments/bowed.d.ts.map +1 -0
- package/dist/instruments/bowed.js +502 -0
- package/dist/instruments/bowed.js.map +1 -0
- package/dist/instruments/builtin.d.ts +261 -0
- package/dist/instruments/builtin.d.ts.map +1 -0
- package/dist/instruments/builtin.js +160 -0
- package/dist/instruments/builtin.js.map +1 -0
- package/dist/instruments/definition.d.ts +46 -0
- package/dist/instruments/definition.d.ts.map +1 -0
- package/dist/instruments/definition.js +5 -0
- package/dist/instruments/definition.js.map +1 -0
- package/dist/instruments/dsp/ambience.d.ts +17 -0
- package/dist/instruments/dsp/ambience.d.ts.map +1 -0
- package/dist/instruments/dsp/ambience.js +146 -0
- package/dist/instruments/dsp/ambience.js.map +1 -0
- package/dist/instruments/dsp/core.d.ts +22 -0
- package/dist/instruments/dsp/core.d.ts.map +1 -0
- package/dist/instruments/dsp/core.js +100 -0
- package/dist/instruments/dsp/core.js.map +1 -0
- package/dist/instruments/dsp/drums.d.ts +112 -0
- package/dist/instruments/dsp/drums.d.ts.map +1 -0
- package/dist/instruments/dsp/drums.js +566 -0
- package/dist/instruments/dsp/drums.js.map +1 -0
- package/dist/instruments/dsp/modal.d.ts +30 -0
- package/dist/instruments/dsp/modal.d.ts.map +1 -0
- package/dist/instruments/dsp/modal.js +54 -0
- package/dist/instruments/dsp/modal.js.map +1 -0
- package/dist/instruments/dsp/piano.d.ts +9 -0
- package/dist/instruments/dsp/piano.d.ts.map +1 -0
- package/dist/instruments/dsp/piano.js +230 -0
- package/dist/instruments/dsp/piano.js.map +1 -0
- package/dist/instruments/dsp/pluck.d.ts +53 -0
- package/dist/instruments/dsp/pluck.d.ts.map +1 -0
- package/dist/instruments/dsp/pluck.js +195 -0
- package/dist/instruments/dsp/pluck.js.map +1 -0
- package/dist/instruments/dsp/reverb.d.ts +16 -0
- package/dist/instruments/dsp/reverb.d.ts.map +1 -0
- package/dist/instruments/dsp/reverb.js +47 -0
- package/dist/instruments/dsp/reverb.js.map +1 -0
- package/dist/instruments/dsp/staccato.d.ts +18 -0
- package/dist/instruments/dsp/staccato.d.ts.map +1 -0
- package/dist/instruments/dsp/staccato.js +147 -0
- package/dist/instruments/dsp/staccato.js.map +1 -0
- package/dist/instruments/play.d.ts +21 -0
- package/dist/instruments/play.d.ts.map +1 -0
- package/dist/instruments/play.js +140 -0
- package/dist/instruments/play.js.map +1 -0
- package/dist/instruments/registry.d.ts +63 -0
- package/dist/instruments/registry.d.ts.map +1 -0
- package/dist/instruments/registry.js +50 -0
- package/dist/instruments/registry.js.map +1 -0
- package/dist/instruments/samples.d.ts +30 -0
- package/dist/instruments/samples.d.ts.map +1 -0
- package/dist/instruments/samples.js +109 -0
- package/dist/instruments/samples.js.map +1 -0
- package/dist/instruments/voices.d.ts +76 -0
- package/dist/instruments/voices.d.ts.map +1 -0
- package/dist/instruments/voices.js +1061 -0
- package/dist/instruments/voices.js.map +1 -0
- package/dist/midi.d.ts +15 -0
- package/dist/midi.d.ts.map +1 -0
- package/dist/midi.js +280 -0
- package/dist/midi.js.map +1 -0
- package/dist/notation/format.d.ts +12 -0
- package/dist/notation/format.d.ts.map +1 -0
- package/dist/notation/format.js +102 -0
- package/dist/notation/format.js.map +1 -0
- package/dist/notation/harmony.d.ts +83 -0
- package/dist/notation/harmony.d.ts.map +1 -0
- package/dist/notation/harmony.js +191 -0
- package/dist/notation/harmony.js.map +1 -0
- package/dist/notation/melody.d.ts +25 -0
- package/dist/notation/melody.d.ts.map +1 -0
- package/dist/notation/melody.js +138 -0
- package/dist/notation/melody.js.map +1 -0
- package/dist/notation/part.d.ts +34 -0
- package/dist/notation/part.d.ts.map +1 -0
- package/dist/notation/part.js +15 -0
- package/dist/notation/part.js.map +1 -0
- package/dist/notation/transform.d.ts +28 -0
- package/dist/notation/transform.d.ts.map +1 -0
- package/dist/notation/transform.js +67 -0
- package/dist/notation/transform.js.map +1 -0
- package/dist/song.d.ts +140 -0
- package/dist/song.d.ts.map +1 -0
- package/dist/song.js +45 -0
- package/dist/song.js.map +1 -0
- package/dist/tempo.d.ts +36 -0
- package/dist/tempo.d.ts.map +1 -0
- package/dist/tempo.js +81 -0
- package/dist/tempo.js.map +1 -0
- package/dist/theory.d.ts +70 -0
- package/dist/theory.d.ts.map +1 -0
- package/dist/theory.js +183 -0
- package/dist/theory.js.map +1 -0
- package/dist/transcribe.d.ts +81 -0
- package/dist/transcribe.d.ts.map +1 -0
- package/dist/transcribe.js +433 -0
- package/dist/transcribe.js.map +1 -0
- package/package.json +57 -4
- package/src/engine/automation.ts +65 -0
- package/src/engine/compile.ts +124 -0
- package/src/engine/jukebox.ts +322 -0
- package/src/engine/mixer.ts +166 -0
- package/src/engine/output.ts +59 -0
- package/src/engine/playback.ts +349 -0
- package/src/engine/prepare.ts +15 -0
- package/src/engine/render.ts +71 -0
- package/src/index.ts +28 -0
- package/src/instruments/bowed.ts +660 -0
- package/src/instruments/builtin.ts +180 -0
- package/src/instruments/definition.ts +52 -0
- package/src/instruments/dsp/ambience.ts +142 -0
- package/src/instruments/dsp/core.ts +97 -0
- package/src/instruments/dsp/drums.ts +565 -0
- package/src/instruments/dsp/modal.ts +76 -0
- package/src/instruments/dsp/piano.ts +226 -0
- package/src/instruments/dsp/pluck.ts +240 -0
- package/src/instruments/dsp/reverb.ts +56 -0
- package/src/instruments/dsp/staccato.ts +141 -0
- package/src/instruments/play.ts +151 -0
- package/src/instruments/registry.ts +100 -0
- package/src/instruments/samples.ts +119 -0
- package/src/instruments/voices.ts +1168 -0
- package/src/midi.ts +285 -0
- package/src/notation/format.ts +96 -0
- package/src/notation/harmony.ts +233 -0
- package/src/notation/melody.ts +133 -0
- package/src/notation/part.ts +42 -0
- package/src/notation/transform.ts +78 -0
- package/src/song.ts +170 -0
- package/src/tempo.ts +108 -0
- package/src/theory.ts +208 -0
- package/src/transcribe.ts +487 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yu Lun Hsu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,375 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Polyphon
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
https://github.com/user-attachments/assets/da754130-8c39-4e8d-962d-78e3fc75a40d
|
|
4
|
+
|
|
5
|
+
<p align="center"><em>Por una Cabeza</em> as electro tango. Every note is synthesized by Polyphon; nothing was recorded.</p>
|
|
6
|
+
|
|
7
|
+
**Music for the browser, written as code.** A score notation, a synthesized orchestra of 120 instruments and a scheduler, on top of the Web Audio API. There are no audio files: every note is computed in the browser.
|
|
8
|
+
|
|
9
|
+
[繁體中文](README.zh-TW.md)
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { Jukebox, bassline, chords, defineSong, melody, pad, rhythm } from 'polyphon';
|
|
13
|
+
|
|
14
|
+
const harmony = chords('C | Am | F | G');
|
|
15
|
+
|
|
16
|
+
const theme = defineSong({
|
|
17
|
+
title: 'Morning Market',
|
|
18
|
+
bpm: 112,
|
|
19
|
+
tracks: [
|
|
20
|
+
{ instrument: 'marimba', part: 'E5:2 G5:2 C6:4 B5:2 G5:2 E5:4 | A5:4 G5:2 E5:2 C5:8 | F5:2 A5:2 C6:4 A5:2 F5:2 D5:4 | G5:6 F5:2 E5:4 D5:4 |' },
|
|
21
|
+
{ instrument: 'strPad', part: pad(harmony, { center: 'C4', velocity: 0.4 }), gain: 0.6 },
|
|
22
|
+
{ instrument: 'pizzBass', part: bassline(harmony, 'x...x.x.x...x.x.', '1 5 8 5') },
|
|
23
|
+
{ instrument: 'kick', part: rhythm('X.......x.......') },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const music = new Jukebox();
|
|
28
|
+
music.play(theme); // starts on the first click or key press if the browser needs a gesture
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Polyphon grew out of the music engine of [Eldor](https://github.com/alan890104/eldor), a creature-battle RPG.
|
|
32
|
+
|
|
33
|
+
## Listen
|
|
34
|
+
|
|
35
|
+
Every example is rendered by Polyphon itself; nothing here was recorded. Six of them, playing on their music-box discs:
|
|
36
|
+
|
|
37
|
+
https://github.com/user-attachments/assets/3dd6375e-73da-4b24-bd01-fde3ada79b8c
|
|
38
|
+
|
|
39
|
+
https://github.com/user-attachments/assets/e0cf3e6e-2149-4503-a2e3-3facc73d2753
|
|
40
|
+
|
|
41
|
+
https://github.com/user-attachments/assets/7bf0cd1c-3810-4a21-96bb-052546b22d0d
|
|
42
|
+
|
|
43
|
+
https://github.com/user-attachments/assets/32e30b01-1e8a-49c2-b495-7dad8942bd93
|
|
44
|
+
|
|
45
|
+
https://github.com/user-attachments/assets/b75ce45f-5b55-4657-b7ac-4200df6921b1
|
|
46
|
+
|
|
47
|
+
https://github.com/user-attachments/assets/2b1fafc0-cbc6-424c-b731-b079f3d0606e
|
|
48
|
+
|
|
49
|
+
And every one as an MP3:
|
|
50
|
+
|
|
51
|
+
| Song | What you hear | |
|
|
52
|
+
| --- | --- | --- |
|
|
53
|
+
| Morning Market | An original market tune in C major: marimba over pizzicato bass, shaker and a soft string bed | [▶ 0:24](docs/audio/morning-market.mp3) |
|
|
54
|
+
| Night Waltz | An original waltz in A minor: piano over harp arpeggios, a string pad and cellos | [▶ 0:20](docs/audio/night-waltz.mp3) |
|
|
55
|
+
| After Midnight | An original piano ballad in B minor over a slow R&B groove, with strings and a violin | [▶ 1:42](docs/audio/after-midnight.mp3) |
|
|
56
|
+
| Neon Tide | An original progressive-house track in F minor: build, pumping supersaw drop, piano breakdown, final drop | [▶ 2:35](docs/audio/neon-tide.mp3) |
|
|
57
|
+
| Por una Cabeza (Electro Tango) | Gardel's tango remade as electro tango: his refrain on a solo violin and strings over a kick on tango's 3-3-2, a new strain in A minor for a solo cello, a build, a dead stop, the last refrains in octaves | [▶ 1:44](docs/audio/por-una-cabeza.mp3) |
|
|
58
|
+
| Moonlight Sonata | Beethoven, the opening of the first movement, on the modelled piano | [▶ 0:43](docs/audio/moonlight.mp3) |
|
|
59
|
+
| Canon in D | Pachelbel, complete: three violins in canon over a cello ground | [▶ 4:08](docs/audio/canon.mp3) |
|
|
60
|
+
| Canon in D Minor | The same canon moved into D minor, written as a transformation of the original | [▶ 4:08](docs/audio/canon-minor.mp3) |
|
|
61
|
+
| Symphony No. 5 | Beethoven, the exposition of the first movement, for full orchestra | [▶ 1:19](docs/audio/symphony5.mp3) |
|
|
62
|
+
| Carmen: Prelude | Bizet, the Prelude to Act 1, complete, for full orchestra: the procession, the Toreador's song, the fate motif | [▶ 3:36](docs/audio/carmen.mp3) |
|
|
63
|
+
| Carmen: Habanera | Bizet, the opening of Carmen's entrance aria: her line on a solo violin over the cellos' habanera in D minor, then the chorus in D major | [▶ 0:50](docs/audio/habanera.mp3) |
|
|
64
|
+
|
|
65
|
+
## What it does
|
|
66
|
+
|
|
67
|
+
- **Notation that reads like music.** Melodies are short strings (`'E5:4 D5:2 C5:2 |'`), drum patterns are grids (`'X...x...'`), harmony is chord symbols (`'Am7 | D7 | Gmaj7'`). Pads, arpeggios, bass lines and strums grow out of a progression. Everything produces plain data, so you can also write notes yourself.
|
|
68
|
+
- **An orchestra with no samples.** Pianos and plucked strings are modelled physically, bowed strings are rendered player by player, and winds, brass, choirs and synths are synthesized live. Each sampled note is computed once and cached, so playback costs almost nothing.
|
|
69
|
+
- **A jukebox for games.** Crossfades between songs, preloading, pause, resume and seek, ducking under sound effects, intros that lead into loops, and the browser's audio unlock handled for you.
|
|
70
|
+
- **Import and export.** Load Standard MIDI Files with `songFromMidi`, or write down a hummed melody with `transcribe`; render any song to a WAV with `renderSong` and `encodeWav`.
|
|
71
|
+
- **A listening room.** `npm run dev` opens a player with every example and a per-track mixer; each song is drawn from its own notes as a perforated disc, like a polyphon's, that turns as it plays.
|
|
72
|
+
|
|
73
|
+
## Install
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm install github:alan890104/polyphon
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
TypeScript types are included. Polyphon has no runtime dependencies.
|
|
80
|
+
|
|
81
|
+
## Writing music
|
|
82
|
+
|
|
83
|
+
Time is counted in **steps**: one step is a sixteenth note, so a quarter note is 4 steps and a 4/4 bar is 16.
|
|
84
|
+
|
|
85
|
+
### Melodies
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
melody('E5:4 D5:2 C5:2 | B4:8 r:8 |')
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
| Write | Means |
|
|
92
|
+
| --- | --- |
|
|
93
|
+
| `E5:4` | note E5, 4 steps long (a quarter note) |
|
|
94
|
+
| `E5 D5` | leave out the length to reuse the previous one |
|
|
95
|
+
| `r:8` or `.` | a rest |
|
|
96
|
+
| `R` | a whole bar of rest |
|
|
97
|
+
| `\|` | bar line: each bar's length is checked, and a mistake throws with the bar number |
|
|
98
|
+
| `C4+E4+G4:16` | a chord |
|
|
99
|
+
| `!E5` / `?E5` | accent / ghost note |
|
|
100
|
+
| `E5:4>` | glide into the next note (instruments that can glide) |
|
|
101
|
+
| `G4:8~ \| G4:4` | tie: one note held across the bar line |
|
|
102
|
+
| `E5:4/3` | fractional length: triplet eighths are `4/3` |
|
|
103
|
+
| `(E5:2 D5:2)*4` | repeat a group |
|
|
104
|
+
|
|
105
|
+
Pass a meter for other time signatures: `melody('G4:6 A4:2 B4:4 |', '3/4')`. A melody string can also be used directly as a track's `part`; it is then read in the song's meter.
|
|
106
|
+
|
|
107
|
+
### Rhythms
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
rhythm('X...x...X..xx...') // X accent, x normal, o soft, . or - rest
|
|
111
|
+
rhythm('x.x.x.x.', { pitch: 'G2' }) // every hit on one pitch
|
|
112
|
+
rhythm('x.x.', { step: 2 }) // each character lasts 2 steps
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Harmony
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const harmony = chords('Am | F | C G | E7'); // the chords in a bar share it equally
|
|
119
|
+
chords('Dm:12 A7:4 | %'); // explicit lengths; % holds the previous chord another bar
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Chord symbols: major, `m`, `dim`, `aug`, `sus2`, `sus4`, `5`, `6`, `m6`, `7`, `maj7`, `m7`, `mmaj7`, `m7b5`, `dim7`, `7sus4`, `add9`, `madd9`, `9`, `maj9`, `m9`, `7b9`, `7#9`, `m11`, `11`, `maj7s11`, `6/9`, `q` and `q4` (stacked fourths), and slash basses (`C/E`).
|
|
123
|
+
|
|
124
|
+
From a progression:
|
|
125
|
+
|
|
126
|
+
| Function | Makes |
|
|
127
|
+
| --- | --- |
|
|
128
|
+
| `pad(harmony, { center, size, velocity, gap })` | sustained chords, voiced to move as little as possible |
|
|
129
|
+
| `arpeggio(harmony, '0 1 2 1', { step, center, size, velocity, accent })` | broken chords: digits pick chord tones from the bottom (past the top they climb an octave), `.` rests, `-` holds |
|
|
130
|
+
| `bassline(harmony, 'x...x.x.', '1 5 8 5', { register, velocity })` | a bass line: the rhythm repeats every bar, and the line lists scale degrees (`1 3 5 8 -8 b7 7 2 4 6 b2 -5 -4`) |
|
|
131
|
+
| `roots(harmony, { register, fifth, velocity })` | one long bass note per chord, optionally with an open fifth |
|
|
132
|
+
| `strum(harmony, 'x.x-', { center, size, spread, velocity })` | strummed chords |
|
|
133
|
+
|
|
134
|
+
`center` and `register` take a note name (`'C4'`) or a MIDI number.
|
|
135
|
+
|
|
136
|
+
### Combining parts
|
|
137
|
+
|
|
138
|
+
| Function | Does |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `sequence(a, b, ...)` | one after another (strings are read as 4/4 melodies) |
|
|
141
|
+
| `stack(a, b, ...)` | at the same time |
|
|
142
|
+
| `repeat(part, n)` | n times in a row |
|
|
143
|
+
| `rest(bars, meter?)` | silence |
|
|
144
|
+
| `transpose(part, semitones)` | shift pitches |
|
|
145
|
+
| `mapPitch(part, fn)` | rewrite pitches (for example `remapToMode`) |
|
|
146
|
+
| `scaleVelocity(part, factor)` | louder or softer |
|
|
147
|
+
| `slice(part, from, to)` | cut out a stretch of steps |
|
|
148
|
+
| `pedal(part, changes)` | the sustain pedal: notes ring until the next change (every n steps, a list of steps, or each chord of a progression) or until the key is struck again |
|
|
149
|
+
|
|
150
|
+
Theory helpers: `midi('C#4')`, `frequency('A4')`, `noteName(61)`, `parseChord('F#m7b5')`, `voiceChord`, `scaleDegree('D4', 'dorian', 2)`, `remapToMode`, `romanChord('iv7', 'D4', 'aeolian')`, and `MODES` (ionian to locrian, harmonic and melodic minor, Phrygian dominant, Hungarian minor, double harmonic, pentatonics, whole tone, octatonic and more).
|
|
151
|
+
|
|
152
|
+
### Songs
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
defineSong({
|
|
156
|
+
title: 'Night Waltz',
|
|
157
|
+
description: 'A minor, 3/4',
|
|
158
|
+
bpm: 84, // the tempo it starts at
|
|
159
|
+
tempo: [{ bar: 33, bpm: 72, over: 4 }], // changes of tempo, below
|
|
160
|
+
silences: [{ bar: 32.75, beats: 1 }], // dead stops, below
|
|
161
|
+
meter: '3/4', // default '4/4'
|
|
162
|
+
swing: 0, // eighth-note swing 0-0.5; swing16 for sixteenths
|
|
163
|
+
reverb: { size: 2.6, damp: 0.5, preDelay: 0.02, early: 0.3, mix: 1 },
|
|
164
|
+
delay: { steps: 3, feedback: 0.3, tone: 2800, mix: 0.8 }, // tempo-synced ping-pong
|
|
165
|
+
gain: 1,
|
|
166
|
+
master: { warm: 2, cut: 1, trim: -1 }, // mastering EQ in dB; glue and loud below
|
|
167
|
+
intro: [/* tracks that play once */],
|
|
168
|
+
tracks: [/* tracks that loop */],
|
|
169
|
+
loop: true, // false: play once (jingles, fanfares)
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A section lasts as long as its longest track, rounded up to whole bars; shorter tracks repeat to fill it unless they set `repeat: false`.
|
|
174
|
+
|
|
175
|
+
The tempo can change along the way, at once or gradually. Bars count from 1 at the start of the song, the intro's first; a looping body plays its changes again on every pass.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
tempo: [
|
|
179
|
+
{ bar: 17, bpm: 66 }, // a new tempo from bar 17
|
|
180
|
+
{ bar: 29, bpm: 50, over: 4 }, // ritardando: down to 50 across bars 29 to 32
|
|
181
|
+
{ bar: 33, bpm: 84 }, // a tempo
|
|
182
|
+
{ bar: 40.5, bpm: 40 }, // the second half of bar 40, held back: a fermata
|
|
183
|
+
{ bar: 41, bpm: 84 },
|
|
184
|
+
]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`tempoClock(song)` turns positions in the score into seconds and back, for anything that has to keep time with the music.
|
|
188
|
+
|
|
189
|
+
A silence is a dead stop: from its bar, counted the same way, everything falls silent for its beats, the reverb and delay tails too, as if the room were switched off, and the music crashes back in at full level. Leaving the notes out makes a rest; a silence makes the beat of nothing before a drop.
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
silences: [{ bar: 32.75, beats: 1 }], // the last beat of bar 32
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
A track:
|
|
196
|
+
|
|
197
|
+
| Field | Default | |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| `instrument` | | a built-in name (autocompleted) or one you registered |
|
|
200
|
+
| `part` | | a part, or a melody string |
|
|
201
|
+
| `name` | the instrument | shown in mixers |
|
|
202
|
+
| `gain` | 1 | level |
|
|
203
|
+
| `pan` | 0 | -1 left to 1 right |
|
|
204
|
+
| `reverb` | 0.25 | send to the room |
|
|
205
|
+
| `delay` | 0 | send to the delay |
|
|
206
|
+
| `duck` | | pump under another track's hits: its name (`'Kick'`), or `{ by, depth, release }` |
|
|
207
|
+
| `lowpass` | | low-pass cutoff in Hz, or a sweep `{ step: Hz }` |
|
|
208
|
+
| `highpass` | | high-pass cutoff in Hz, or a sweep |
|
|
209
|
+
| `transpose` | 0 | semitones |
|
|
210
|
+
| `humanize` | 0.008 s (drums 0.003) | timing looseness |
|
|
211
|
+
| `params` | | instrument settings: `attack`, `release`, `bright`, `glide`, `ring`, `tune` and others |
|
|
212
|
+
| `repeat` | true | repeat to fill the section |
|
|
213
|
+
|
|
214
|
+
### Mixing electronic music
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const harmony = chords('Fm | Db | Ab | Eb');
|
|
218
|
+
|
|
219
|
+
defineSong({
|
|
220
|
+
title: 'Drop',
|
|
221
|
+
bpm: 128,
|
|
222
|
+
master: { glue: 0.5, loud: 8 },
|
|
223
|
+
tracks: [
|
|
224
|
+
{ name: 'Kick', instrument: 'bigKick', part: rhythm('X...'.repeat(64), { pitch: 'F1' }) },
|
|
225
|
+
{ name: 'Chords', instrument: 'supersaw', part: pad(harmony, { center: 'Ab4' }), duck: 'Kick' },
|
|
226
|
+
{ name: 'Sub', instrument: 'sub', part: roots(harmony, { register: 'F1' }), duck: { by: 'Kick', depth: 0.9 } },
|
|
227
|
+
{ name: 'Pluck', instrument: 'pluck', part: arpeggio(harmony, '0 1 2 3', { center: 'C5' }), lowpass: { 0: 400, 128: 400, 256: 18000 } },
|
|
228
|
+
],
|
|
229
|
+
});
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
- **Sidechain pumping.** `duck: 'Kick'` dips a track at every hit of the track called Kick in the same section, then swells it back: the pumping of house and EDM. `depth` is how far it dips (0-1, default 0.7) and `release` how many steps it takes to come back (default 4, a beat). It follows the notes, not the sound, so it stays exact through seeks and loops, and a muted or silent kick (`gain: 0`) still drives it.
|
|
233
|
+
- **Filter sweeps.** `lowpass` and `highpass` take a cutoff in Hz, or breakpoints `{ step: Hz }` counted from the start of the section. The cutoff glides between them evenly in pitch and holds before the first and after the last, so the pluck above stays dark for eight bars and opens over the next eight. Each loop starts the sweep again.
|
|
234
|
+
- **Mastering.** `glue` (0-1) is bus compression that holds the mix together. `loud` masters the song loud: that many dB pushed into a brickwall limiter that keeps the peaks under -1 dBFS, between the samples too (EDM: about 6 to 10). A song with `loud` skips the gentle limiter other songs share, so it is not compressed twice.
|
|
235
|
+
|
|
236
|
+
## Playing music
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const music = new Jukebox({
|
|
240
|
+
songs: { // optional: songs to play by name
|
|
241
|
+
town: () => import('./music/town.ts'), // loaded and prepared on first use
|
|
242
|
+
battle: battleSong,
|
|
243
|
+
},
|
|
244
|
+
volume: 0.8,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
music.play('town'); // crossfades from whatever was playing
|
|
248
|
+
music.preload('battle'); // render its samples ahead of time
|
|
249
|
+
await music.play('battle', { fadeOut: 0.5, fadeIn: 0.05 });
|
|
250
|
+
music.duck(-10); // dip under a big sound effect, recover in 0.35 s
|
|
251
|
+
music.duck(-30, { attack: 0.12, hold: 4, release: 0.6 });
|
|
252
|
+
music.pause();
|
|
253
|
+
music.resume();
|
|
254
|
+
music.seek(30);
|
|
255
|
+
music.playOnce(victoryJingle); // layered on top
|
|
256
|
+
music.stop();
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`play` resolves with the `Playback` once the song starts, or `null` if another song was asked for first (`music.playback` is the current one). A `Playback` reports `position()`, `duration`, `introDuration` and `loopDuration`; its `tracks` can be muted one by one (`track.muted = true`) and expose an `output` node for meters; `finished` resolves when a song with an end has played. Pass `{ loops: n }` to `play` to end after n passes.
|
|
260
|
+
|
|
261
|
+
The Jukebox creates its `AudioContext` on first use and starts it on the first click, tap or key press. To share a context with your sound effects, pass `context` and `destination`.
|
|
262
|
+
|
|
263
|
+
## Instruments
|
|
264
|
+
|
|
265
|
+
| Family | Instruments |
|
|
266
|
+
| --- | --- |
|
|
267
|
+
| Keys | `piano` `softPiano` `epiano` `revPiano` `organ` `organFull` `accordion` |
|
|
268
|
+
| Plucked strings | `harp` `nylon` `steel` `lute` `dulcimer` `pizz` `pizzBass` |
|
|
269
|
+
| Mallets and bells | `kalimba` `marimba` `vibes` `glock` `celesta` `musicbox` `bell` `chime` `steelpan` |
|
|
270
|
+
| Bowed strings | `strings` `strPad` `violin` `cello` `celli` `contrabass` `fiddle` `morin` `spicc` |
|
|
271
|
+
| Woodwinds | `flute` `panflute` `ocarina` `whistle` `clarinet` `oboe` `bassoon` `sax` `tenorSax` |
|
|
272
|
+
| Brass | `horn` `horns` `trumpet` `trumpets` `trombone` `tuba` `brassStab` |
|
|
273
|
+
| Voices | `choir` `choirOo` `choirO` `menChoir` `chant` `soprano` `throat` |
|
|
274
|
+
| Pads and drones | `warmPad` `glassPad` `darkPad` `airPad` `glass` `drone` |
|
|
275
|
+
| Synths and guitars | `supersaw` `synthLead` `pluck` `powerGtr` `muteGtr` |
|
|
276
|
+
| Bass | `bass` `synthBass` `reese` `acid` `bass808` `sub` `distBass` |
|
|
277
|
+
| Drums | `kick` `boom` `snare` `field` `rim` `clap` `hat` `ohat` `ride` `crash` `swell` `tom` `taiko` `timpani` `frame` `darbuka` |
|
|
278
|
+
| Electronic drums | `kick808` `kick909` `bigKick` `snare909` `clap909` `hat909` `ohat909` |
|
|
279
|
+
| Percussion | `shaker` `tamb` `woodblock` `clave` `anvil` `triangle` `gong` `chain` `snap` `brush` `drip` `thunder` `thunderclap` |
|
|
280
|
+
| Ambience and effects | `wind` `sea` `fire` `crickets` `rain` `void` `cave` `birds` `riser` `downlifter` `impact` |
|
|
281
|
+
|
|
282
|
+
How they are made:
|
|
283
|
+
|
|
284
|
+
- **Piano**: modal synthesis of every string (one to three per note, slightly detuned), with inharmonicity, stretched tuning, two-stage decay and frequency-dependent string loss; a hammer whose felt rounds off the treble; sympathetic resonance from the other strings; a soundboard, damper noise and a faint ring after release; velocity-dependent brightness; and the keyboard spread across the stereo field.
|
|
285
|
+
- **Plucked strings**: Karplus-Strong excited by a physically shaped pluck (the n-th harmonic ∝ sin(nπ·position)/n), with frequency-dependent string loss, an allpass for exact tuning and the resonances of the instrument's body (guitar, lute, harp, dulcimer, violin); bass plucks are additive.
|
|
286
|
+
- **Bowed strings**: every player in a section is rendered separately, with their own intonation, vibrato, bowing point and bow noise, through a body of a hundred-odd resonances fitted to recordings of real violins, cellos and double basses, so the vibrato makes the upper partials flicker. The bowed corner is rounded at a partial number, so low strings are darker than high ones. The sustain loops seamlessly; spiccato notes ring on freely after the bow bounces off.
|
|
287
|
+
- **Mallets and bells**: modal synthesis with the partial ratios of the real bars, tines and tubes (church bells have hum, prime, tierce, quint and nominal; a tubular bell's pitch is heard from partials near 2 : 3 : 4), and the mallet's contact time rounding off the overtones of the high notes.
|
|
288
|
+
- **Winds and brass**: the lips, the reed or the air jet are modelled as a valve (a sine through a nonlinear curve), so the tone brightens with dynamics the way the real instruments do: a soft trumpet is mellow and a loud one blazes, the clarinet keeps mostly odd harmonics in its low register, and the saxophone growls when pushed. Bore and bell are filters and formants; section players enter a few milliseconds apart and sit a few cents off.
|
|
289
|
+
- **Voices**: formant filters (choirs sing vowels), breath noise and per-note vibrato.
|
|
290
|
+
- **Drums**: drumheads as circular-membrane modes that glide as the head settles, with the stick or beater's click; timpani tuned by the air in the kettle (modes near 1 : 1.5 : 2 : 2.5 : 3); cymbal washes that bloom upward after the strike and lose their highs first; velocity-dependent brightness; and random variants, so repeated hits never sound identical.
|
|
291
|
+
- **Synthesizers**: analog subtractive synthesis. The oscillators run free (each note starts them at a random point of their cycle, a cent or two off pitch) into a four-pole low-pass (24 dB/oct, like a transistor ladder) whose cutoff follows the keyboard and whose envelope opens further the harder a note is played. `supersaw` follows Adam Szabo's measurements of the Roland JP-8000 (seven saws on the original detune and mix curves, with a high-pass at the fundamental); `synthLead` is a Minimoog-style lead, `pluck` the trance arpeggio pluck, and `warmPad` and `darkPad` Juno-style chorused pads. `acid` is a TB-303 (accent with `!`, slide with `>`), `reese` a churning detuned bass and `bass808` a gliding 808; the basses stay mono below 120 Hz. `cut`, `q`, `env`, `detune`, `attack`, `release` and `glide` shape each voice.
|
|
292
|
+
- **Electronic drums**: modelled on Roland's drum machines and fitted to recordings. The 808 kick is a bridged-T resonator whose attack briefly raises its pitch; the 909 kick is an oscillator with a fast pitch envelope and a noise click; the clap is three noise bursts about 11 ms apart and a reverberant tail; the 909 hats are 6-bit, like the machine's memory, and a closed hat cuts off a ringing open one. `bigKick` is a modern layered EDM kick: a pitch-dive punch, a clipped sub and a top click. Give a kick a pitch to tune it to the song (`rhythm('X...X...X...X...', { pitch: 'F1' })`); `riser` and `downlifter` last as long as their note, so `melody('C4:128')` is an eight-bar build.
|
|
293
|
+
- **Rooms**: a procedural convolution reverb per song.
|
|
294
|
+
|
|
295
|
+
Your own instruments:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
registerInstrument('beep', {
|
|
299
|
+
play({ context, destination, time, duration, frequency, velocity }) {
|
|
300
|
+
const osc = new OscillatorNode(context, { type: 'square', frequency });
|
|
301
|
+
const amp = new GainNode(context, { gain: velocity * 0.2 });
|
|
302
|
+
osc.connect(amp).connect(destination);
|
|
303
|
+
osc.start(time);
|
|
304
|
+
osc.stop(time + duration);
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
registerInstrument('myBell', {
|
|
309
|
+
render: (sampleRate, midi) => renderBell(sampleRate, midi), // a Float32Array, computed once per pitch
|
|
310
|
+
sustain: 'ring',
|
|
311
|
+
});
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## MIDI, audio files and singing
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
const song = songFromMidi(await file.arrayBuffer(), { title: file.name });
|
|
318
|
+
music.play(song);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
`songFromMidi` keeps the notes on the file's beat grid and its tempo changes as the song's own, and reads the time signature, the sustain pedal, volume and pan. General MIDI programs pick instruments from the orchestra, and channel 10 becomes the drum kit.
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
const buffer = await renderSong(song, { loops: 1 }); // OfflineAudioContext, faster than real time
|
|
325
|
+
const wav = encodeWav(buffer); // 16-bit PCM
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
A WAV is lossless but large, about 10 MB a minute. For an MP3 (about a seventh of that), hand the buffer to an MP3 encoder; the listening room does it with [lamejs](https://github.com/zhuker/lamejs) in a worker (`player/src/mp3.ts`), so the library itself needs none.
|
|
329
|
+
|
|
330
|
+
A melody sung or hummed one note at a time can be written down. `PitchTracker` follows the voice's pitch as audio arrives, from a microphone or a file, and `transcribe` puts what it heard on the beat grid:
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
const tracker = new PitchTracker(audio.sampleRate);
|
|
334
|
+
tracker.push(audio.getChannelData(0));
|
|
335
|
+
const { melody, bpm, key } = transcribe(tracker.frames);
|
|
336
|
+
// 'r:12 Bb4:4 | Bb4:3 C5 D5:2 D5:8 | A4:12 G4:4 | ...', 96.8, 'G minor'
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
It needs no trained model: a voice's pitch shows in the sound's own periodicity. A voice that sits sharp or flat, or drifts, is followed, and a note between two semitones takes the one in the key. Sung with a metronome (`{ bpm, start }`: its tempo and where its first bar begins), the notes fall on its beats; without one, the tempo and the downbeat are found from the notes themselves. A new note starts where the pitch moves, where the voice stops and starts again, or where the same note is struck anew, and a rest is written only for a clear silence. `formatMelody` writes any one-line part back as a melody string, spelled in its key.
|
|
340
|
+
|
|
341
|
+
## The listening room
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
npm install
|
|
345
|
+
npm run dev
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The player lists every example song (Beethoven's Moonlight Sonata and Fifth Symphony, Pachelbel's Canon in D, the same canon moved into D minor, and a few short demos), with a mixer to mute or solo tracks, MP3 and WAV download and MIDI import. Every song's cover is its score: the notes punched into a disc, angle for time and radius for pitch, the tune in brass. While a song plays its disc turns under the comb and the perforations light up as they pass.
|
|
349
|
+
|
|
350
|
+
Press **F** (or the visualizer button) for the immersive view: full screen, the controls fade away while you listen. Pick a style (by default a disc music box in 3D, whose comb is plucked by each perforation as you hear its note while a lamp inside throws the notes onto the lid; or the turning disc ringed by its spectrum, falling notes onto a piano keyboard, spectrum, waveform, ridgeline, particles), a background (ink, the song's disc, aurora, starfield or a picture of your own; the music box has a room of its own) and a theme; the choice is remembered. The music box, falling notes and the disc follow the score itself, in time with what you hear. MIDI files you import stay in your browser.
|
|
351
|
+
|
|
352
|
+
**Hum a tune** (in the sidebar) to have a melody written out. Sing or hum on "da" into the microphone, with a click to keep time or at your own pace, or open a recording. The voice is drawn as it comes in; then the notes appear on a roll, played on the instrument you pick (with your voice under them, to compare), with the score ready to copy and a button that adds the tune to Your songs beside the MIDI files you import. Nothing you sing leaves the browser.
|
|
353
|
+
|
|
354
|
+
## Development
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm test # notation, theory, MIDI import, transcription and every example song (Node, no browser)
|
|
358
|
+
npm run typecheck
|
|
359
|
+
npm run build # the library, to dist/
|
|
360
|
+
npm run build:player # the listening room, to player/dist/
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
Node 22.18 or later runs the TypeScript sources directly.
|
|
364
|
+
|
|
365
|
+
## Credits
|
|
366
|
+
|
|
367
|
+
- The Moonlight Sonata example was checked against the [Mutopia Project](https://www.mutopiaproject.org) edition (the music is public domain; the edition is licensed CC BY-SA 2.5).
|
|
368
|
+
- The Fifth Symphony example was checked against the [Mutopia Project](https://www.mutopiaproject.org) edition (the music and the edition are both public domain).
|
|
369
|
+
- The Canon in D example was checked against the [Mutopia Project](https://www.mutopiaproject.org) edition (the music is public domain; the edition is licensed CC BY 4.0).
|
|
370
|
+
- The piano's string loss follows A. Chaigne and A. Askenfelt, *Numerical simulations of piano strings* (1994).
|
|
371
|
+
- The listening room's MP3 download uses [lamejs](https://github.com/zhuker/lamejs), a JavaScript port of [LAME](https://lame.sourceforge.io) (LGPL-3.0), loaded unmodified and in its own file, only when a song is downloaded.
|
|
372
|
+
|
|
373
|
+
## License
|
|
374
|
+
|
|
375
|
+
MIT
|