@hraness/dawg 0.6.0 → 0.7.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/CHANGELOG.md +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- package/core/style-provenance.ts +80 -0
- package/core/styles/africa-mena-southasia.ts +2893 -0
- package/core/styles/americas.ts +3810 -0
- package/core/styles/art.ts +4993 -0
- package/core/styles/base.ts +123 -0
- package/core/styles/cycles.ts +106 -0
- package/core/styles/electronic.ts +2723 -0
- package/core/styles/europe-asia-pacific.ts +2838 -0
- package/core/styles/excerpt.ts +29 -0
- package/core/styles/gamelan.ts +283 -0
- package/core/styles/generate.ts +2199 -0
- package/core/styles/index.ts +515 -0
- package/core/styles/parts.ts +106 -0
- package/core/styles/pop.ts +3189 -0
- package/core/styles/rock.ts +2993 -0
- package/core/styles/roots.ts +4175 -0
- package/core/styles/schema.ts +429 -0
- package/core/styles/taxonomy.ts +940 -0
- package/core/styles/validate.ts +528 -0
- package/core/tempo.ts +32 -2
- package/core/tuning.ts +19 -3
- package/core/vocoder.ts +524 -0
- package/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- package/guides/faders.md +20 -16
- package/guides/files.md +13 -9
- package/guides/getting-started.md +15 -11
- package/guides/keys.md +19 -15
- package/guides/media.md +17 -12
- package/guides/mix.md +12 -6
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/performance.md +15 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- package/guides/rhythm.md +17 -13
- package/guides/sessions.md +17 -7
- package/guides/show-me.md +31 -0
- package/guides/sound.md +25 -9
- package/guides/sounds.md +15 -12
- package/guides/styles.md +31 -0
- package/guides/tempo.md +15 -10
- package/guides/tracks.md +15 -10
- package/guides/tuning.md +32 -0
- package/guides/voice.md +31 -0
- package/guides/web-search.md +18 -8
- package/native/prebuilt/darwin-arm64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/darwin-x64/libdawg_sink.dylib +0 -0
- package/native/prebuilt/linux-arm64/libdawg_sink.so +0 -0
- package/native/prebuilt/linux-x64/libdawg_sink.so +0 -0
- package/native/prebuilt/manifest.json +21 -0
- package/package.json +5 -2
- package/src/agent/agent.ts +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +369 -0
- package/src/agent/drum-tools.ts +2 -2
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +13 -0
- package/src/agent/portable-schema.ts +80 -0
- package/src/agent/preview-tool.ts +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/rhythm-tools.ts +1 -1
- package/src/agent/section-tools.ts +1 -1
- package/src/agent/show-me.ts +497 -0
- package/src/agent/steer.ts +15 -0
- package/src/agent/style-tools.ts +217 -0
- package/src/agent/tool-error.ts +12 -0
- package/src/agent/tools.ts +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -3
- package/src/audio/autotune-engine.ts +101 -0
- package/src/audio/autotune.ts +640 -0
- package/src/audio/clips.ts +240 -0
- package/src/audio/doctor.ts +86 -0
- package/src/audio/dsp/bandbank.ts +138 -0
- package/src/audio/dsp/envelope.ts +171 -0
- package/src/audio/dsp/follow.ts +120 -0
- package/src/audio/dsp/formant.ts +427 -0
- package/src/audio/dsp/glottal.ts +243 -0
- package/src/audio/dsp/interp.ts +7 -2
- package/src/audio/dsp/lpc.ts +50 -0
- package/src/audio/dsp/periodicity.ts +59 -0
- package/src/audio/dsp/pitch.ts +995 -0
- package/src/audio/dsp/psola.ts +199 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/synth/oscillators.ts +31 -21
- package/src/audio/synth/voice.ts +34 -1
- package/src/audio/vocoder/bank.ts +314 -0
- package/src/audio/vocoder/carrier.ts +165 -0
- package/src/audio/vocoder/control.ts +68 -0
- package/src/audio/vocoder/detect.ts +50 -0
- package/src/audio/vocoder/index.ts +304 -0
- package/src/audio/vocoder/talkbox.ts +143 -0
- package/src/audio/wav.ts +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- package/src/auth/cli.ts +38 -36
- package/src/auth/credentials.ts +30 -1
- package/src/auth/login.ts +15 -9
- package/src/auth/tui.ts +19 -10
- package/src/commands/arrange.ts +44 -29
- package/src/commands/autotune.ts +421 -0
- package/src/commands/calibration.ts +74 -0
- package/src/commands/clips.ts +887 -0
- package/src/commands/drums.ts +3 -2
- package/src/commands/edit.ts +11 -4
- package/src/commands/expression.ts +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- package/src/commands/music.ts +1 -1
- package/src/commands/nearest.ts +53 -0
- package/src/commands/pack.ts +9 -2
- package/src/commands/param-range.ts +56 -0
- package/src/commands/parses.ts +130 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- package/src/commands/style.ts +415 -0
- package/src/commands/time.ts +6 -3
- package/src/commands/tuning.ts +3 -3
- package/src/commands/vocal-pitch.ts +616 -0
- package/src/commands/vocal.ts +147 -0
- package/src/commands/vocoder.ts +627 -0
- package/src/commands/wind.ts +244 -0
- package/src/fs/durable.ts +50 -0
- package/src/lang/glossary.ts +493 -0
- package/src/launch-args.ts +163 -0
- package/src/main.ts +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- package/src/project/clip-pins.ts +72 -0
- package/src/project/init.ts +23 -8
- package/src/project/sync.ts +418 -86
- package/src/render.ts +36 -1
- package/src/session/daemon.ts +3 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +154 -0
- package/src/session/port.ts +22 -4
- package/src/session/presence.ts +50 -7
- package/src/session/protocol.ts +5 -1
- package/src/session/rebase.ts +18 -5
- package/src/session/receipt.ts +258 -0
- package/src/session/store.ts +65 -43
- package/src/tui/arrange-menu.ts +65 -31
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +18 -13
- package/src/tui/fader.ts +228 -41
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/menu-clips.ts +297 -0
- package/src/tui/menu-time.ts +20 -13
- package/src/tui/menu-voice.ts +405 -0
- package/src/tui/menu.ts +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/vocoder-menu.ts +244 -0
- package/src/tui/wind-menu.ts +144 -0
- package/src/version.ts +8 -0
- package/src/web/fetch.ts +115 -29
- package/tui/activity.ts +180 -9
- package/tui/app.ts +274 -40
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +144 -0
- package/tui/drawer.ts +70 -22
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +112 -63
- package/tui/guide.ts +42 -4
- package/tui/highway.ts +269 -25
- package/tui/hints.ts +192 -0
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/play-strip.ts +68 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +144 -14
- package/tui/theme.ts +27 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Numeric pattern checks on a generated style (quality-08). Each check
|
|
3
|
+
* measures the score the generator produced against the card that drove
|
|
4
|
+
* it: meter and tempo, onsets on the swung grid (within the groove's
|
|
5
|
+
* microtiming and humanise tolerance), the measured swing ratio, scale
|
|
6
|
+
* and tuning membership, acceptance by the card's harmony grammar,
|
|
7
|
+
* melody range and interval statistics, and that every instrument and
|
|
8
|
+
* kit resolves. Family lanes rely on `validateGenerated` through
|
|
9
|
+
* `cards.test.ts`; `/style info --check` and the agent show the same
|
|
10
|
+
* report.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createScore } from "../score.ts";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
generateStyle,
|
|
17
|
+
harmonyGrammar,
|
|
18
|
+
modeOfScale as modeOfScaleName,
|
|
19
|
+
onsetTolerance,
|
|
20
|
+
stripSeventh,
|
|
21
|
+
swungStepTick,
|
|
22
|
+
type GeneratedStyle,
|
|
23
|
+
type PlannedChord,
|
|
24
|
+
type StylePlan,
|
|
25
|
+
} from "./generate.ts";
|
|
26
|
+
import { resolveInstrumentWord } from "../instruments.ts";
|
|
27
|
+
import { SYNTH_KIT_NAMES } from "../kits.ts";
|
|
28
|
+
import { roleGrid } from "./index.ts";
|
|
29
|
+
import {
|
|
30
|
+
KIT_ROLES,
|
|
31
|
+
PERC_ROLES,
|
|
32
|
+
PITCHED_ROLES,
|
|
33
|
+
type RoleName,
|
|
34
|
+
type StyleId,
|
|
35
|
+
type StyleOptions,
|
|
36
|
+
} from "./schema.ts";
|
|
37
|
+
|
|
38
|
+
export type StyleCheck = Readonly<{
|
|
39
|
+
name: string;
|
|
40
|
+
ok: boolean;
|
|
41
|
+
/** The measured value, when the check measures one. */
|
|
42
|
+
value?: number;
|
|
43
|
+
detail: string;
|
|
44
|
+
}>;
|
|
45
|
+
|
|
46
|
+
export type StyleReport = Readonly<{
|
|
47
|
+
id: StyleId;
|
|
48
|
+
seed: number;
|
|
49
|
+
bars: number;
|
|
50
|
+
ok: boolean;
|
|
51
|
+
checks: readonly StyleCheck[];
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
/** Fraction of pitched notes that may sit outside scale and chord. */
|
|
55
|
+
export const VALIDATE_LIMITS = Object.freeze({
|
|
56
|
+
/** Lead and counter notes in the scale or the sounding chord. */
|
|
57
|
+
melodyMembership: 0.9,
|
|
58
|
+
/** Absolute tolerance on the measured swing ratio. */
|
|
59
|
+
swingTolerance: 0.2,
|
|
60
|
+
/** Odd-step kit notes needed before swing is measured. */
|
|
61
|
+
swingSamples: 6,
|
|
62
|
+
/** Largest melodic leap inside a phrase (an octave). */
|
|
63
|
+
maxLeap: 12.5,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const KIT = new Set<string>(KIT_ROLES);
|
|
67
|
+
const PERC = new Set<string>(PERC_ROLES);
|
|
68
|
+
const PITCHED = new Set<string>(PITCHED_ROLES);
|
|
69
|
+
type CheckedNote = Readonly<{
|
|
70
|
+
id: string;
|
|
71
|
+
trackId: string;
|
|
72
|
+
startTick: number;
|
|
73
|
+
durationTicks: number;
|
|
74
|
+
pitch: number;
|
|
75
|
+
velocity: number;
|
|
76
|
+
cents?: number;
|
|
77
|
+
}>;
|
|
78
|
+
|
|
79
|
+
const NOTES = new WeakMap<object, readonly CheckedNote[]>();
|
|
80
|
+
|
|
81
|
+
/** The generated notes with their tick fields required (the generator always writes ticks). */
|
|
82
|
+
function notesOf(data: GeneratedStyle["data"]): readonly CheckedNote[] {
|
|
83
|
+
const cached = NOTES.get(data);
|
|
84
|
+
if (cached) return cached;
|
|
85
|
+
const notes = (data.notes ?? []).map((note) => ({
|
|
86
|
+
id: note.id,
|
|
87
|
+
trackId: note.trackId,
|
|
88
|
+
startTick: note.startTick ?? NaN,
|
|
89
|
+
durationTicks: note.durationTicks ?? NaN,
|
|
90
|
+
pitch: note.pitch,
|
|
91
|
+
velocity: note.velocity,
|
|
92
|
+
...(note.cents !== undefined ? { cents: note.cents } : {}),
|
|
93
|
+
}));
|
|
94
|
+
NOTES.set(data, notes);
|
|
95
|
+
return notes;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const mod = (n: number, m: number) => ((n % m) + m) % m;
|
|
99
|
+
|
|
100
|
+
function check(
|
|
101
|
+
name: string,
|
|
102
|
+
ok: boolean,
|
|
103
|
+
detail: string,
|
|
104
|
+
value?: number,
|
|
105
|
+
): StyleCheck {
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
name,
|
|
108
|
+
ok,
|
|
109
|
+
detail,
|
|
110
|
+
...(value !== undefined ? { value: Math.round(value * 1000) / 1000 } : {}),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Generate `id` and validate the result. */
|
|
115
|
+
export function validateStyle(
|
|
116
|
+
id: StyleId,
|
|
117
|
+
options: StyleOptions = {},
|
|
118
|
+
): StyleReport {
|
|
119
|
+
return validateGenerated(generateStyle(id, options));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Every pattern check on one generated style. */
|
|
123
|
+
export function validateGenerated(generated: GeneratedStyle): StyleReport {
|
|
124
|
+
const { plan, data } = generated;
|
|
125
|
+
const checks: StyleCheck[] = [
|
|
126
|
+
checkStructure(generated),
|
|
127
|
+
checkMeter(plan, data),
|
|
128
|
+
checkTempo(plan, data.tempoBpm ?? NaN),
|
|
129
|
+
...checkOnsets(generated),
|
|
130
|
+
checkTuning(generated),
|
|
131
|
+
...checkPitch(generated),
|
|
132
|
+
checkGrammar(plan),
|
|
133
|
+
...checkMelody(generated),
|
|
134
|
+
checkInstruments(generated),
|
|
135
|
+
];
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
id: plan.style.id,
|
|
138
|
+
seed: plan.seed,
|
|
139
|
+
bars: plan.bars,
|
|
140
|
+
ok: checks.every((entry) => entry.ok),
|
|
141
|
+
checks: Object.freeze(checks),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
function checkStructure({ plan, data }: GeneratedStyle): StyleCheck {
|
|
148
|
+
const loop = plan.bars * plan.barTicks;
|
|
149
|
+
const bad = notesOf(data).filter(
|
|
150
|
+
(note) =>
|
|
151
|
+
!Number.isInteger(note.startTick) ||
|
|
152
|
+
note.startTick < 0 ||
|
|
153
|
+
note.startTick >= loop ||
|
|
154
|
+
!(note.durationTicks >= 1) ||
|
|
155
|
+
note.startTick + note.durationTicks > loop ||
|
|
156
|
+
!(note.velocity > 0 && note.velocity <= 1) ||
|
|
157
|
+
note.pitch < 0 ||
|
|
158
|
+
note.pitch > 127,
|
|
159
|
+
);
|
|
160
|
+
const tracks = new Set((data.tracks ?? []).map((track) => track.id));
|
|
161
|
+
const orphan = notesOf(data).filter((note) => !tracks.has(note.trackId));
|
|
162
|
+
let parsed = true;
|
|
163
|
+
try {
|
|
164
|
+
createScore(data);
|
|
165
|
+
} catch {
|
|
166
|
+
parsed = false;
|
|
167
|
+
}
|
|
168
|
+
const ok =
|
|
169
|
+
notesOf(data).length > 0 &&
|
|
170
|
+
bad.length === 0 &&
|
|
171
|
+
orphan.length === 0 &&
|
|
172
|
+
parsed &&
|
|
173
|
+
data.bars === plan.bars;
|
|
174
|
+
return check(
|
|
175
|
+
"structure",
|
|
176
|
+
ok,
|
|
177
|
+
`${notesOf(data).length} notes, ${bad.length} out of bounds, ${orphan.length} orphaned, score ${parsed ? "loads" : "rejected"}`,
|
|
178
|
+
notesOf(data).length,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function checkMeter(plan: StylePlan, data: GeneratedStyle["data"]): StyleCheck {
|
|
183
|
+
const allowed = plan.style.meter.signatures.map(([sig]) => sig);
|
|
184
|
+
const meter = data.time?.meter?.[0];
|
|
185
|
+
const unit = meter ? meter.beatUnit : 4;
|
|
186
|
+
const beats = meter ? meter.beatsPerBar : data.beatsPerBar;
|
|
187
|
+
const ok =
|
|
188
|
+
allowed.includes(plan.signature) &&
|
|
189
|
+
beats === plan.beatsPerBar &&
|
|
190
|
+
unit === plan.beatUnit &&
|
|
191
|
+
data.beatsPerBar === plan.beatsPerBar;
|
|
192
|
+
return check(
|
|
193
|
+
"meter",
|
|
194
|
+
ok,
|
|
195
|
+
`${beats}/${unit}; card allows ${allowed.join(", ")}`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function checkTempo(plan: StylePlan, bpm: number): StyleCheck {
|
|
200
|
+
const [low, high] = plan.style.tempo.bpm;
|
|
201
|
+
const ok =
|
|
202
|
+
Number.isFinite(bpm) &&
|
|
203
|
+
bpm >= Math.floor(low) &&
|
|
204
|
+
bpm <= Math.ceil(high) &&
|
|
205
|
+
bpm >= 20 &&
|
|
206
|
+
bpm <= 300;
|
|
207
|
+
return check("tempo", ok, `${bpm} bpm in ${low}-${high}`, bpm);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Steps of the last beat of each fill bar. */
|
|
211
|
+
function fillSteps(plan: StylePlan): Set<number> {
|
|
212
|
+
const out = new Set<number>();
|
|
213
|
+
const fills = plan.style.rhythm.fills;
|
|
214
|
+
if (!fills) return out;
|
|
215
|
+
const beat = Math.max(1, Math.round(plan.stepsPerBar / plan.beatsPerBar));
|
|
216
|
+
for (let bar = fills.every - 1; bar < plan.bars; bar += fills.every)
|
|
217
|
+
for (
|
|
218
|
+
let s = (bar + 1) * plan.stepsPerBar - beat;
|
|
219
|
+
s < (bar + 1) * plan.stepsPerBar;
|
|
220
|
+
s += 1
|
|
221
|
+
)
|
|
222
|
+
out.add(s);
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function checkOnsets({ plan, data }: GeneratedStyle): StyleCheck[] {
|
|
227
|
+
const style = plan.style;
|
|
228
|
+
const fills = fillSteps(plan);
|
|
229
|
+
const cycle = style.meter.cycle;
|
|
230
|
+
const pulse = (480 * 4) / plan.beatUnit;
|
|
231
|
+
let total = 0;
|
|
232
|
+
let off = 0;
|
|
233
|
+
let offGrid = 0;
|
|
234
|
+
let worst = 0;
|
|
235
|
+
const swingShifts: number[] = [];
|
|
236
|
+
for (const note of notesOf(data)) {
|
|
237
|
+
const role = plan.noteRoles.get(note.id) as RoleName | undefined;
|
|
238
|
+
if (!role || !(KIT.has(role) || PERC.has(role))) continue;
|
|
239
|
+
total += 1;
|
|
240
|
+
if (role === "perc" && cycle) {
|
|
241
|
+
const i = Math.round(note.startTick / pulse);
|
|
242
|
+
const error = Math.abs(note.startTick - i * pulse);
|
|
243
|
+
worst = Math.max(worst, error);
|
|
244
|
+
if (error > onsetTolerance(plan, role)) off += 1;
|
|
245
|
+
if (cycle.strokes[mod(i, cycle.beats)] === ".") offGrid += 1;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// Nearest grid step under the swung grid.
|
|
249
|
+
const raw = Math.round(note.startTick / plan.stepTicks);
|
|
250
|
+
let step = raw;
|
|
251
|
+
let error = Infinity;
|
|
252
|
+
for (const s of [raw - 1, raw, raw + 1]) {
|
|
253
|
+
if (s < 0) continue;
|
|
254
|
+
const e = Math.abs(note.startTick - swungStepTick(plan, s));
|
|
255
|
+
if (e < error) {
|
|
256
|
+
error = e;
|
|
257
|
+
step = s;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
worst = Math.max(worst, error);
|
|
261
|
+
if (error > onsetTolerance(plan, role)) off += 1;
|
|
262
|
+
const grid = roleGrid(style.rhythm.onsets[role], role, plan);
|
|
263
|
+
const inGrid = grid ? grid[mod(step, plan.stepsPerBar)]! > 0 : false;
|
|
264
|
+
const isFill = fills.has(step) && (role === "snare" || role === "tom");
|
|
265
|
+
if (!inGrid && !isFill) offGrid += 1;
|
|
266
|
+
if (
|
|
267
|
+
step % 2 === 1 &&
|
|
268
|
+
plan.subdivision % 2 === 0 &&
|
|
269
|
+
!style.groove.microtiming?.length &&
|
|
270
|
+
!style.groove.roleOffset?.[role]
|
|
271
|
+
)
|
|
272
|
+
swingShifts.push(note.startTick - step * plan.stepTicks);
|
|
273
|
+
}
|
|
274
|
+
const out = [
|
|
275
|
+
check(
|
|
276
|
+
"onset-grid",
|
|
277
|
+
off === 0 && offGrid === 0,
|
|
278
|
+
`${total} kit/perc onsets: ${off} off the swung grid (worst ${Math.round(worst)} ticks), ${offGrid} on steps the card's grid never plays`,
|
|
279
|
+
total ? (total - off - offGrid) / total : 1,
|
|
280
|
+
),
|
|
281
|
+
];
|
|
282
|
+
// Swing: the median shift of odd steps gives the long:short ratio.
|
|
283
|
+
if (
|
|
284
|
+
swingShifts.length >= VALIDATE_LIMITS.swingSamples &&
|
|
285
|
+
plan.subdivision % 2 === 0
|
|
286
|
+
) {
|
|
287
|
+
const sorted = swingShifts.sort((a, b) => a - b);
|
|
288
|
+
const shift = sorted[Math.floor(sorted.length / 2)]! / plan.stepTicks;
|
|
289
|
+
const ratio = (1 + shift) / Math.max(1e-6, 1 - shift);
|
|
290
|
+
const [low, high] = style.groove.swingRatio;
|
|
291
|
+
const tolerance = VALIDATE_LIMITS.swingTolerance;
|
|
292
|
+
out.push(
|
|
293
|
+
check(
|
|
294
|
+
"swing",
|
|
295
|
+
ratio >= low - tolerance &&
|
|
296
|
+
ratio <= high + tolerance &&
|
|
297
|
+
Math.abs(ratio - plan.swing) <= tolerance,
|
|
298
|
+
`measured ${ratio.toFixed(2)}:1 over ${swingShifts.length} off-steps; card ${low}-${high}, drawn ${plan.swing.toFixed(2)}`,
|
|
299
|
+
ratio,
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function checkTuning({ plan, data }: GeneratedStyle): StyleCheck {
|
|
307
|
+
const wanted = plan.style.pitch.tuning;
|
|
308
|
+
if (!wanted) {
|
|
309
|
+
const ok = !data.tuning;
|
|
310
|
+
return check("tuning", ok, ok ? "12-TET" : "unexpected tuning");
|
|
311
|
+
}
|
|
312
|
+
const ok =
|
|
313
|
+
!!data.tuning &&
|
|
314
|
+
data.tuning.name === plan.tuning?.name &&
|
|
315
|
+
data.tuning.root === plan.rootKey &&
|
|
316
|
+
notesOf(data).every((note) => !note.cents);
|
|
317
|
+
return check(
|
|
318
|
+
"tuning",
|
|
319
|
+
ok,
|
|
320
|
+
`${data.tuning?.name ?? "none"} rooted on ${data.tuning?.root ?? "-"}; card wants ${wanted}`,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function chordAtTick(plan: StylePlan, tick: number): PlannedChord | undefined {
|
|
325
|
+
const bar = tick / plan.barTicks;
|
|
326
|
+
for (let i = plan.chords.length - 1; i >= 0; i -= 1)
|
|
327
|
+
if (bar + 1e-9 >= plan.chords[i]!.bar) return plan.chords[i];
|
|
328
|
+
return plan.chords[0];
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Sounding semitone class above the tonic for a key (tuning-aware). */
|
|
332
|
+
function pitchClassOf(plan: StylePlan, key: number, cents: number): number {
|
|
333
|
+
const scale = plan.scale;
|
|
334
|
+
if (scale.period !== 12) {
|
|
335
|
+
const offset = key - plan.rootKey;
|
|
336
|
+
const index = mod(offset, scale.period);
|
|
337
|
+
const tone = scale.tones.find((t) => t.key === index);
|
|
338
|
+
return tone ? mod(tone.semis, scale.periodSemis) : NaN;
|
|
339
|
+
}
|
|
340
|
+
if (plan.tuning) {
|
|
341
|
+
// A 12-key tuning table: the key's degree carries the table's cents.
|
|
342
|
+
const step = mod(key - plan.rootKey, 12);
|
|
343
|
+
const tone = scale.tones.find((t) => t.key === step);
|
|
344
|
+
return tone ? mod(tone.semis, 12) : step;
|
|
345
|
+
}
|
|
346
|
+
return mod(key - plan.rootKey + cents / 100, 12);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function checkPitch(generated: GeneratedStyle): StyleCheck[] {
|
|
350
|
+
const { plan, data } = generated;
|
|
351
|
+
const scale = plan.scale;
|
|
352
|
+
const inScale = (pc: number) =>
|
|
353
|
+
scale.tones.some(
|
|
354
|
+
(t) =>
|
|
355
|
+
Math.abs(mod(t.semis, scale.periodSemis) - pc) < 0.02 ||
|
|
356
|
+
Math.abs(mod(t.semis, scale.periodSemis) - pc - scale.periodSemis) <
|
|
357
|
+
0.02 ||
|
|
358
|
+
Math.abs(mod(t.semis, scale.periodSemis) - pc + scale.periodSemis) <
|
|
359
|
+
0.02,
|
|
360
|
+
);
|
|
361
|
+
// Power chords are root and perfect fifth whatever the triad's fifth.
|
|
362
|
+
const power = plan.style.harmony.voicing.types.some(([t]) => t === "power");
|
|
363
|
+
const walking = plan.style.bass.behaviour.some(([b]) => b === "walking");
|
|
364
|
+
let melodyTotal = 0;
|
|
365
|
+
let melodyIn = 0;
|
|
366
|
+
let strictTotal = 0;
|
|
367
|
+
let strictOut = 0;
|
|
368
|
+
const strays: string[] = [];
|
|
369
|
+
for (const note of notesOf(data)) {
|
|
370
|
+
const role = plan.noteRoles.get(note.id) as RoleName | undefined;
|
|
371
|
+
if (!role || !PITCHED.has(role)) continue;
|
|
372
|
+
const pc = pitchClassOf(plan, note.pitch, note.cents ?? 0);
|
|
373
|
+
// The chord sounding at the note, or the one it anticipates by up to
|
|
374
|
+
// half a step (humanised and pushed notes land just before the change).
|
|
375
|
+
const inChord =
|
|
376
|
+
scale.period === 12 &&
|
|
377
|
+
[note.startTick, note.startTick + plan.stepTicks / 2].some((tick) => {
|
|
378
|
+
const chord = chordAtTick(plan, tick);
|
|
379
|
+
return (
|
|
380
|
+
!!chord &&
|
|
381
|
+
!chord.degrees &&
|
|
382
|
+
[...chord.pcs, ...(power ? [chord.rootPc + 7] : [])].some(
|
|
383
|
+
(p) => Math.abs(mod(p - plan.tonic, 12) - pc) < 0.02,
|
|
384
|
+
)
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
const ok = Number.isFinite(pc) && (inScale(pc) || inChord);
|
|
388
|
+
if (role === "lead" || role === "counter") {
|
|
389
|
+
melodyTotal += 1;
|
|
390
|
+
if (ok) melodyIn += 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (role === "bass" && walking) {
|
|
394
|
+
// Walking bass: chromatic approach tones are idiomatic.
|
|
395
|
+
if (!ok && !Number.isInteger(Math.round(pc * 1000) / 1000)) {
|
|
396
|
+
strictTotal += 1;
|
|
397
|
+
strictOut += 1;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
strictTotal += 1;
|
|
402
|
+
if (!ok) {
|
|
403
|
+
strictOut += 1;
|
|
404
|
+
if (strays.length < 4)
|
|
405
|
+
strays.push(`${role}@${note.startTick}:${pc.toFixed(2)}`);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const membership = melodyTotal ? melodyIn / melodyTotal : 1;
|
|
409
|
+
return [
|
|
410
|
+
check(
|
|
411
|
+
"scale",
|
|
412
|
+
strictOut === 0,
|
|
413
|
+
`${strictTotal} harmony/bass notes, ${strictOut} outside ${scale.name} and the sounding chord${strays.length ? ` (${strays.join(" ")})` : ""}`,
|
|
414
|
+
strictTotal ? 1 - strictOut / strictTotal : 1,
|
|
415
|
+
),
|
|
416
|
+
check(
|
|
417
|
+
"melody-scale",
|
|
418
|
+
membership >= VALIDATE_LIMITS.melodyMembership,
|
|
419
|
+
`${melodyIn}/${melodyTotal} melody notes in scale or chord`,
|
|
420
|
+
membership,
|
|
421
|
+
),
|
|
422
|
+
];
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function checkGrammar(plan: StylePlan): StyleCheck {
|
|
426
|
+
if (plan.harmonyModel !== "functional" || plan.scale.period !== 12)
|
|
427
|
+
return check("grammar", true, `${plan.harmonyModel} harmony: no grammar`);
|
|
428
|
+
const grammar = harmonyGrammar(plan.style, modeOfScaleName(plan.scaleName));
|
|
429
|
+
const numerals = plan.chords.map((chord) => stripSeventh(chord.numeral));
|
|
430
|
+
const unknown = numerals.filter((n) => !grammar.numerals.has(n));
|
|
431
|
+
const badEdges: string[] = [];
|
|
432
|
+
for (let i = 0; i + 1 < numerals.length; i += 1) {
|
|
433
|
+
const edge = `${numerals[i]}>${numerals[i + 1]}`;
|
|
434
|
+
if (!grammar.edges.has(edge)) badEdges.push(edge);
|
|
435
|
+
}
|
|
436
|
+
const ok = unknown.length === 0 && badEdges.length === 0;
|
|
437
|
+
return check(
|
|
438
|
+
"grammar",
|
|
439
|
+
ok,
|
|
440
|
+
ok
|
|
441
|
+
? `${numerals.length} chords accepted (${plan.harmonySource})`
|
|
442
|
+
: `rejected: ${[...new Set([...unknown, ...badEdges])].slice(0, 5).join(" ")} (${plan.harmonySource})`,
|
|
443
|
+
numerals.length
|
|
444
|
+
? 1 - (unknown.length + badEdges.length) / numerals.length
|
|
445
|
+
: 1,
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function checkMelody({ plan, data }: GeneratedStyle): StyleCheck[] {
|
|
450
|
+
const melody = plan.style.melody;
|
|
451
|
+
const lead = notesOf(data)
|
|
452
|
+
.filter((note) => plan.noteRoles.get(note.id) === "lead")
|
|
453
|
+
.sort((a, b) => a.startTick - b.startTick || a.pitch - b.pitch);
|
|
454
|
+
if (lead.length < 2) return [];
|
|
455
|
+
const sounding = (note: (typeof lead)[number]) =>
|
|
456
|
+
note.pitch + (note.cents ?? 0) / 100;
|
|
457
|
+
const [low, high] = melody.range;
|
|
458
|
+
const outside = lead.filter(
|
|
459
|
+
(note) =>
|
|
460
|
+
note.pitch < Math.round(low) - 1 || note.pitch > Math.round(high) + 1,
|
|
461
|
+
);
|
|
462
|
+
const intervals: number[] = [];
|
|
463
|
+
// Intervals inside a phrase; the jump between phrases is a new start.
|
|
464
|
+
const phraseOf = (tick: number) => {
|
|
465
|
+
const bar = tick / plan.barTicks;
|
|
466
|
+
let index = 0;
|
|
467
|
+
plan.phrases.forEach((phrase, i) => {
|
|
468
|
+
if (bar + 1e-9 >= phrase.startBar) index = i;
|
|
469
|
+
});
|
|
470
|
+
return index;
|
|
471
|
+
};
|
|
472
|
+
for (let i = 1; i < lead.length; i += 1)
|
|
473
|
+
if (phraseOf(lead[i]!.startTick) === phraseOf(lead[i - 1]!.startTick))
|
|
474
|
+
intervals.push(sounding(lead[i]!) - sounding(lead[i - 1]!));
|
|
475
|
+
if (intervals.length === 0) intervals.push(0);
|
|
476
|
+
const abs = intervals.map(Math.abs);
|
|
477
|
+
const mean = abs.reduce((a, b) => a + b, 0) / abs.length;
|
|
478
|
+
const maxLeap = Math.max(...abs);
|
|
479
|
+
// Expected mean |interval| from the card's weights (-12..12).
|
|
480
|
+
const weights = melody.intervals;
|
|
481
|
+
let wsum = 0;
|
|
482
|
+
let expect = 0;
|
|
483
|
+
weights.forEach((w, i) => {
|
|
484
|
+
wsum += w;
|
|
485
|
+
expect += w * Math.abs(i - 12);
|
|
486
|
+
});
|
|
487
|
+
expect = wsum > 0 ? expect / wsum : 2;
|
|
488
|
+
const stepScale =
|
|
489
|
+
plan.scale.period === 12
|
|
490
|
+
? 1
|
|
491
|
+
: plan.scale.periodSemis / plan.scale.tones.length;
|
|
492
|
+
return [
|
|
493
|
+
check(
|
|
494
|
+
"melody-range",
|
|
495
|
+
outside.length === 0,
|
|
496
|
+
`${lead.length} lead notes in ${low}-${high}: ${outside.length} outside`,
|
|
497
|
+
outside.length,
|
|
498
|
+
),
|
|
499
|
+
check(
|
|
500
|
+
"melody-intervals",
|
|
501
|
+
maxLeap <= VALIDATE_LIMITS.maxLeap &&
|
|
502
|
+
mean <= Math.max(expect * 2.2, 3 * stepScale) + 1,
|
|
503
|
+
`mean |interval| ${mean.toFixed(2)} (card ~${expect.toFixed(2)}), largest ${maxLeap.toFixed(1)}`,
|
|
504
|
+
mean,
|
|
505
|
+
),
|
|
506
|
+
];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function checkInstruments({ plan, data }: GeneratedStyle): StyleCheck {
|
|
510
|
+
const bad: string[] = [];
|
|
511
|
+
for (const track of plan.tracks) {
|
|
512
|
+
if (track.instrument === "drums") {
|
|
513
|
+
const kit = track.voice.kit;
|
|
514
|
+
if (kit && !SYNTH_KIT_NAMES.includes(kit)) bad.push(`kit ${kit}`);
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (!resolveInstrumentWord(track.instrument)) bad.push(track.instrument);
|
|
518
|
+
}
|
|
519
|
+
if ((data.tracks ?? []).length !== plan.tracks.length)
|
|
520
|
+
bad.push("track count");
|
|
521
|
+
return check(
|
|
522
|
+
"instruments",
|
|
523
|
+
bad.length === 0,
|
|
524
|
+
bad.length
|
|
525
|
+
? `unresolved: ${bad.join(", ")}`
|
|
526
|
+
: `${plan.tracks.map((t) => t.voice.kit ?? t.instrument).join(", ")}`,
|
|
527
|
+
);
|
|
528
|
+
}
|
package/core/tempo.ts
CHANGED
|
@@ -107,8 +107,8 @@ export const TIME_LIMITS = Object.freeze({
|
|
|
107
107
|
/** Same as `SCORE_LIMITS.minTempoBpm` / `maxTempoBpm`. */
|
|
108
108
|
minBpm: 20,
|
|
109
109
|
maxBpm: 300,
|
|
110
|
-
/** Same as `SCORE_LIMITS.maxTick
|
|
111
|
-
maxTick:
|
|
110
|
+
/** Same as `SCORE_LIMITS.maxTick` (2^26). */
|
|
111
|
+
maxTick: 67_108_864,
|
|
112
112
|
/** Same as `SCORE_LIMITS.maxBeatsPerBar`. */
|
|
113
113
|
maxBeatsPerBar: 16,
|
|
114
114
|
/** Same as `SCORE_LIMITS.maxBars`. */
|
|
@@ -1098,6 +1098,36 @@ export function withoutFermatas(
|
|
|
1098
1098
|
});
|
|
1099
1099
|
}
|
|
1100
1100
|
|
|
1101
|
+
/**
|
|
1102
|
+
* The song time as the SDK can write it: tempo events and fermatas at or
|
|
1103
|
+
* past the final barline and meter changes from bar `bars` on are dropped
|
|
1104
|
+
* (never heard; the SDK refuses them), and a ramp landing on the final
|
|
1105
|
+
* barline moves to the last tick, as the SDK's `ramp()` does. A shorter
|
|
1106
|
+
* song applies this, and the printer applies it to older scores.
|
|
1107
|
+
*/
|
|
1108
|
+
export function timeWithinSong(score: TimeScore): SongTime | undefined {
|
|
1109
|
+
const time = score.time;
|
|
1110
|
+
if (!time) return time;
|
|
1111
|
+
const end = loopTicksOf(score);
|
|
1112
|
+
const tempo: TempoEvent[] = [];
|
|
1113
|
+
for (const event of time.tempo ?? []) {
|
|
1114
|
+
const previous = tempo[tempo.length - 1]?.tick ?? 0;
|
|
1115
|
+
if (event.tick < end) tempo.push(event);
|
|
1116
|
+
else if (event.tick === end && event.ramp && end - 1 > previous)
|
|
1117
|
+
tempo.push({ ...event, tick: end - 1 });
|
|
1118
|
+
}
|
|
1119
|
+
const meter = (time.meter ?? []).filter((m) => m.bar < score.bars);
|
|
1120
|
+
const fermatas = (time.fermatas ?? []).filter((f) => f.tick < end);
|
|
1121
|
+
if (
|
|
1122
|
+
tempo.every((e, i) => e === time.tempo?.[i]) &&
|
|
1123
|
+
tempo.length === (time.tempo ?? []).length &&
|
|
1124
|
+
meter.length === (time.meter ?? []).length &&
|
|
1125
|
+
fermatas.length === (time.fermatas ?? []).length
|
|
1126
|
+
)
|
|
1127
|
+
return time;
|
|
1128
|
+
return rebuild(time, { tempo, meter, fermatas });
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1101
1131
|
/** Sets the meter from bar `bar` (0-based) on; null removes that change. */
|
|
1102
1132
|
export function withMeterChange(
|
|
1103
1133
|
time: SongTime | undefined,
|
package/core/tuning.ts
CHANGED
|
@@ -264,7 +264,7 @@ const FIXED_PRESETS: readonly TuningPreset[] = [
|
|
|
264
264
|
{
|
|
265
265
|
name: "12-tet",
|
|
266
266
|
family: "equal",
|
|
267
|
-
about: "twelve
|
|
267
|
+
about: "twelve equal steps per octave (12-TET), the default",
|
|
268
268
|
cents: edoCents(12),
|
|
269
269
|
aliases: ["12edo", "12-edo", "12tet", "12-et", "equal", "et", "standard"],
|
|
270
270
|
},
|
|
@@ -339,6 +339,15 @@ const FIXED_PRESETS: readonly TuningPreset[] = [
|
|
|
339
339
|
cents: [231, 474, 717, 955, 1200],
|
|
340
340
|
approximate: true,
|
|
341
341
|
},
|
|
342
|
+
{
|
|
343
|
+
name: "thai",
|
|
344
|
+
family: "equal",
|
|
345
|
+
about:
|
|
346
|
+
"Thai and Khmer seven-tone equidistant tuning, about 171 cents a step (Morton's theoretical norm; every ensemble differs)",
|
|
347
|
+
cents: edoCents(7),
|
|
348
|
+
approximate: true,
|
|
349
|
+
aliases: ["7-edo", "7edo", "7-tet", "khmer"],
|
|
350
|
+
},
|
|
342
351
|
{
|
|
343
352
|
name: "nyamaropa",
|
|
344
353
|
family: "african",
|
|
@@ -375,6 +384,9 @@ const FIXED_PRESETS: readonly TuningPreset[] = [
|
|
|
375
384
|
function scalePreset(name: string, info: ScaleInfo): TuningPreset | undefined {
|
|
376
385
|
const quarter = info.steps.some((step) => !Number.isInteger(step));
|
|
377
386
|
if (!quarter && !info.intonation) return undefined;
|
|
387
|
+
// Quarter-tone art scales keep both a key and its shadow: note cents
|
|
388
|
+
// carry them, so no twelve-key table can.
|
|
389
|
+
if (info.family === "quarter-tone") return undefined;
|
|
378
390
|
const raga = info.family === "raga";
|
|
379
391
|
const table = raga ? fromRatios(HINDUSTANI) : edoCents(12);
|
|
380
392
|
const keys = [0, ...table.slice(0, 11)];
|
|
@@ -390,10 +402,14 @@ function scalePreset(name: string, info: ScaleInfo): TuningPreset | undefined {
|
|
|
390
402
|
? "raga"
|
|
391
403
|
: info.family === "dastgah"
|
|
392
404
|
? "dastgah"
|
|
393
|
-
: "
|
|
405
|
+
: info.family === "overtone"
|
|
406
|
+
? "just"
|
|
407
|
+
: "maqam",
|
|
394
408
|
about: raga
|
|
395
409
|
? `raga ${name} with its shruti intonation over the Hindustani just table`
|
|
396
|
-
:
|
|
410
|
+
: info.family === "overtone"
|
|
411
|
+
? `partials 8 to 15 of the harmonic series over the tonic, in just cents`
|
|
412
|
+
: `${info.family} ${name} quarter tones on the twelve keys (24-tone convention)`,
|
|
397
413
|
cents: [...keys.slice(1), 1200],
|
|
398
414
|
approximate: true,
|
|
399
415
|
};
|