pepperscore 0.3.0 → 0.4.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.
@@ -0,0 +1,2 @@
1
+ export * from './lib/instrument-model';
2
+ export * from './lib/range-fit-core';
@@ -0,0 +1,2 @@
1
+ import { _ as e, a as t, c as n, d as r, f as i, g as a, h as o, i as s, l as c, m as l, n as u, o as d, p as f, r as p, s as m, t as h, u as g, v as _ } from "./range-fit-core-B1rMWdI4.js";
2
+ export { t as EMBED_INSTRUMENTS, d as INSTRUMENTS, m as INSTRUMENT_IDS, n as LEVELS, h as applyOverrides, c as instrumentFor, g as instrumentName, u as joinRuns, r as keyAlter, i as pitchMidi, p as planShifts, f as spellMidi, l as spelledSoundingMidi, s as tieChainOf, o as transposePitch, a as transposePitchSpelled, e as writtenKeyFifths, _ as writtenPitch };
@@ -1,6 +1,6 @@
1
1
  /** Chrome presets (docs/specs/pepperscore-embeddable-component.md, "Chrome presets") and per-control overrides. No runtime imports. */
2
2
  export type ChromePreset = 'full' | 'minimal' | 'none';
3
- export type PepperScoreControls = Partial<Record<'mixer' | 'openFile' | 'drawer' | 'transport' | 'viewToggle' | 'zoom' | 'tabToggle', boolean>>;
3
+ export type PepperScoreControls = Partial<Record<'mixer' | 'openFile' | 'drawer' | 'transport' | 'viewToggle' | 'zoom' | 'tabToggle' | 'instrumentSwitch' | 'octaveTool', boolean>>;
4
4
  export type ResolvedChrome = {
5
5
  header: boolean;
6
6
  transport: boolean;
@@ -11,6 +11,10 @@ export type ResolvedChrome = {
11
11
  zoom: boolean;
12
12
  /** The Notation / TAB / Both switch (shown only for a score with a stave that can take TAB). */
13
13
  tabToggle: boolean;
14
+ /** The instrument switcher (instrument, level and fit to range per part). */
15
+ instrumentSwitch: boolean;
16
+ /** The reader's octave tool (move notes or passages by an octave). */
17
+ octaveTool: boolean;
14
18
  /** The overlaid play/stop button of `minimal`. */
15
19
  compactPlay: boolean;
16
20
  /** Playhead, note highlight and follow. */
@@ -24,6 +28,7 @@ export type ResolvedChrome = {
24
28
  * `transport` covers stop, play, BPM, transpose, key flip and bars; `viewToggle` the Vertical/Endless toggle; `openFile` the
25
29
  * Open button, file input, open dialog and reopen offer; `mixer` the mixer and the part icons / instrument picker / stave
26
30
  * eyes; `zoom` the stave-scale slider, pinch zoom and the page arrows row; `drawer` the bottom drawer and its handle;
27
- * `tabToggle` the Notation / TAB / Both switch (header and drawer).
31
+ * `tabToggle` the Notation / TAB / Both switch (header and drawer); `instrumentSwitch` the instrument switcher;
32
+ * `octaveTool` the octave tool. Each defaults to on for `full`, off otherwise.
28
33
  */
29
34
  export declare function resolveChrome(preset?: ChromePreset, controls?: PepperScoreControls): ResolvedChrome;
@@ -2,6 +2,9 @@ import { type InstrumentId, type Score } from './score';
2
2
  import { type DeviceClass } from './stave-scale';
3
3
  import { clampBpm, MAX_BPM, MIN_BPM, type AnnotationSize } from './prefs';
4
4
  import type { TabView } from './tab/tab-parts';
5
+ import type { PartSound, PartSwitch } from './instrument-view';
6
+ import type { Shift } from './range-fit-core';
7
+ import { type RangeLevel } from './instrument-model';
5
8
  export type FileSource = {
6
9
  name: string;
7
10
  size: number;
@@ -43,11 +46,16 @@ export type ScoreView = {
43
46
  zoom: number;
44
47
  zoomDevice: DeviceClass;
45
48
  };
49
+ /**
50
+ * `switch`: the reader's instrument switch of this staff part (instrument-view.ts `PartSwitch`; `octaves` by
51
+ * `ScoreNote.id`). Added after version 2 shipped: optional, and an unusable one is dropped (the part opens as written).
52
+ */
46
53
  export type PartState = {
47
54
  visible: boolean;
48
55
  instrument: InstrumentId;
49
56
  gmSoundfont?: string;
50
57
  volume?: number;
58
+ switch?: SavedSwitch;
51
59
  };
52
60
  /** Per-score record (schema: file-state.schema.json). Only settings and hashes, never score content. */
53
61
  export type FileState = {
@@ -58,6 +66,12 @@ export type FileState = {
58
66
  settings: SavedSettings;
59
67
  parts: Record<string, PartState>;
60
68
  view?: ScoreView;
69
+ /**
70
+ * The host's instrument settings when the record was saved (instrument-switch-state.ts `hostInstrumentKey`); a host
71
+ * whose settings differ on the next load applies them again over the saved switches. Added after version 2 shipped:
72
+ * optional, absent when the host gave none, dropped when unusable.
73
+ */
74
+ hostInstruments?: string;
61
75
  };
62
76
  /** The localStorage v1 record: zoom was a multiplier of the width fit and lived in settings. */
63
77
  export type FileStateV1 = {
@@ -93,6 +107,19 @@ export declare function fileSource(file: Pick<File, 'name' | 'size'>, bytes: Uin
93
107
  export declare const fileSourceFor: (name: string, size: number, sha256: string) => FileSource;
94
108
  /** Source identity for text without a file (the sample, pasted ABC, the musicXml prop): a hash of the text with a format prefix. */
95
109
  export declare function textSource(format: string, text: string, name: string): FileSource;
110
+ /** A part's switch as saved in its record (`PartState.switch`); `sound` only with an `instrument`. */
111
+ export type SavedSwitch = {
112
+ instrument?: InstrumentId;
113
+ level: RangeLevel;
114
+ autoRange: boolean;
115
+ octaves?: Record<string, Shift>;
116
+ sound?: PartSound;
117
+ };
118
+ export declare const isLevel: (value: unknown) => value is RangeLevel;
119
+ /** A switch target: any model instrument but the drum kit (unpitched). */
120
+ export declare const isSwitchTarget: (value: unknown) => value is InstrumentId;
121
+ /** A part's switch as saved, or undefined for no entry. */
122
+ export declare function savedSwitch(entry: PartSwitch | undefined): SavedSwitch | undefined;
96
123
  /** Validate a stored v1 (legacy localStorage) record for `sha256`. */
97
124
  export declare function parseLegacyFileState(raw: unknown, sha256: string): FileStateV1 | null;
98
125
  /**
@@ -105,4 +132,4 @@ export declare function migrateFileState(legacy: FileStateV1, device: DeviceClas
105
132
  export declare function parseFileState(raw: unknown, sha256: string): FileState | null;
106
133
  /** Read the legacy localStorage v1 record for a source. */
107
134
  export declare function readFileState(source: FileSource): FileStateV1 | null;
108
- export declare function buildFileState(source: FileSource, settings: FileSettings, score: Score, hiddenIds: string[], volumes?: Record<string, number>, view?: ScoreView, lastOpened?: number): FileState;
135
+ export declare function buildFileState(source: FileSource, settings: FileSettings, score: Score, hiddenIds: string[], volumes?: Record<string, number>, view?: ScoreView, lastOpened?: number, switches?: Record<string, PartSwitch>, hostInstruments?: string): FileState;
@@ -0,0 +1,80 @@
1
+ /** The instrument model shared by the reader, the embeds and the editor (`pepperscore/instruments`); framework-free. */
2
+ /** The viewer's instrument ids. */
3
+ export declare const INSTRUMENT_IDS: readonly ["piano", "guitar", "bassguitar", "double-bass", "drum-kit", "alto-saxophone", "tenor-saxophone", "bari-saxophone", "flute", "clarinet", "trumpet", "trombone", "violin", "viola", "cello", "microphone"];
4
+ export declare const LEVELS: readonly ["beginner", "intermediate", "advanced"];
5
+ export type InstrumentId = typeof INSTRUMENT_IDS[number];
6
+ export type RangeLevel = typeof LEVELS[number];
7
+ /** An instrument a pitched part can be switched to: any but the drum kit (unpitched). */
8
+ export type SwitchInstrumentId = Exclude<InstrumentId, 'drum-kit'>;
9
+ export type Transposition = {
10
+ semitones: number;
11
+ diatonic: number;
12
+ };
13
+ export type InstrumentClef = 'treble' | 'bass' | 'alto' | 'tenor' | 'percussion' | 'treble8vb';
14
+ export type SpelledPitch = {
15
+ step: 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G';
16
+ octave: number;
17
+ alter: number;
18
+ };
19
+ export type InstrumentInfo = {
20
+ id: InstrumentId;
21
+ label: string;
22
+ /** The name the editor's controls and status line use when shorter than `label` ("Alto Sax"). */
23
+ short?: string;
24
+ /** 1-based GM program (the viewer's voice program); the drum kit plays on channel 10, not a program. */
25
+ gmProgram: number;
26
+ clef: InstrumentClef;
27
+ /** Written minus sounding. */
28
+ transposition: Transposition;
29
+ /** Playable range per level in sounding MIDI, inclusive; absent for unpitched instruments. */
30
+ ranges?: Record<RangeLevel, {
31
+ low: number;
32
+ high: number;
33
+ }>;
34
+ };
35
+ /** The model's instruments by id (frozen). */
36
+ export declare const INSTRUMENTS: Readonly<Record<InstrumentId, Readonly<InstrumentInfo>>>;
37
+ /** The instrument's short name for controls and the status line (`short`, else `label`). */
38
+ export declare const instrumentName: (id: InstrumentId) => string;
39
+ /** The instrument nearest a 1-based GM program; piano when none is near. */
40
+ export declare function instrumentFor(gmProgram: number): Readonly<InstrumentInfo>;
41
+ /** The MIDI number of a pitch (C4 = 60). */
42
+ export declare const pitchMidi: (p: SpelledPitch) => number;
43
+ /** The alteration the key signature (`fifths`) gives `step`. */
44
+ export declare function keyAlter(step: SpelledPitch['step'], fifths: number): number;
45
+ /**
46
+ * The spelling of MIDI note `target`: the key signature's (`fifths`) spelling if it has one, else the
47
+ * natural, else a sharp going up (`direction` >= 0) or a flat going down, else the other single
48
+ * accidental, else a double.
49
+ */
50
+ export declare function spellMidi(target: number, fifths: number, direction: number): SpelledPitch | undefined;
51
+ /** The sounding MIDI number of a written pitch on an instrument with `transposition` (written minus sounding). */
52
+ export declare function spelledSoundingMidi(pitch: SpelledPitch, transposition: {
53
+ semitones: number;
54
+ }): number;
55
+ /**
56
+ * The written pitch of sounding MIDI `soundingMidi` for an instrument with `transposition`, spelled for the
57
+ * written key `keyFifths`: the key's own accidentals first, then naturals, then sharps for chromatic notes in
58
+ * sharp keys and C major, flats in flat keys. Only `semitones` is used: a MIDI number carries no spelling to
59
+ * move by the diatonic interval.
60
+ */
61
+ export declare function writtenPitch(soundingMidi: number, transposition: Transposition, keyFifths: number): SpelledPitch;
62
+ /**
63
+ * `pitch` moved by `t`, keeping its spelling: the step moves by `diatonic` (carrying the octave) and the alter
64
+ * is whatever makes the MIDI number move by `semitones`. When that alter would pass a double accidental the
65
+ * note is respelled with `writtenPitch` for the key `keyFifths` and `respelled` is true.
66
+ */
67
+ export declare function transposePitchSpelled(pitch: SpelledPitch, t: Transposition, keyFifths: number): {
68
+ pitch: SpelledPitch;
69
+ respelled: boolean;
70
+ };
71
+ /** `pitch` moved by `t`, keeping its spelling (see `transposePitchSpelled`; the fallback spells for C major). */
72
+ export declare const transposePitch: (pitch: SpelledPitch, t: Transposition) => SpelledPitch;
73
+ /**
74
+ * The written key (fifths) of music in concert key `concertFifths` shown with `transposition` (written minus
75
+ * sounding): the transposition as fifths, `(7 * semitones) mod 12` normalised to -6..6, added and wrapped
76
+ * back into -7..7. Alto sax (+9 semitones) is +3 fifths; a B-flat instrument +2.
77
+ */
78
+ export declare function writtenKeyFifths(concertFifths: number, transposition: Transposition): number;
79
+ /** The instruments the embeds offer, in switch order (frozen). */
80
+ export declare const EMBED_INSTRUMENTS: readonly InstrumentId[];
@@ -0,0 +1,169 @@
1
+ import { type PartState } from './file-state';
2
+ import { type InstrumentId, type RangeLevel, type SwitchInstrumentId } from './instrument-model';
3
+ import { type InstrumentViewResult, type PartSound, type PartSwitch } from './instrument-view';
4
+ import type { BaseTimeline } from './playback/timeline';
5
+ import type { Shift } from './range-fit-core';
6
+ import { type Part, type Score } from './score';
7
+ export declare const DEFAULT_RANGE_LEVEL: RangeLevel;
8
+ /** The instruments a pitched part can be switched to: the model's, in its order, without the drum kit. */
9
+ export declare const SWITCH_TARGETS: InstrumentId[];
10
+ /** The instrument keys of `PepperScoreSettings` (prefs.ts documents them). */
11
+ export type InstrumentSettings = {
12
+ instrument?: SwitchInstrumentId;
13
+ instruments?: Record<string, SwitchInstrumentId>;
14
+ rangeLevel?: RangeLevel;
15
+ autoRange?: boolean;
16
+ };
17
+ export type SwitchDefaults = {
18
+ level: RangeLevel;
19
+ autoRange: boolean;
20
+ };
21
+ type Switches = Record<string, PartSwitch>;
22
+ /** Source parts in score order, numbered from 1 by the host (TAB doublings excluded), with their staff parts. */
23
+ export declare function sourcePartsOf(score: Score): {
24
+ id: string;
25
+ parts: Part[];
26
+ switchable: boolean;
27
+ }[];
28
+ export declare const switchDefaults: (settings: InstrumentSettings) => SwitchDefaults;
29
+ /**
30
+ * The parts `onReady` lists (every staff, TAB doublings excluded), each with its 1-based source part number (the
31
+ * `settings.instruments` key; a grand staff's staves share one) and whether the switch applies to it.
32
+ */
33
+ export declare function readerPartsInfo(score: Score): {
34
+ id: string;
35
+ name: string;
36
+ sourcePart: number;
37
+ switchable: boolean;
38
+ }[];
39
+ /**
40
+ * Change one source part's switch on every staff part it has (a grand staff switches together). Choosing another
41
+ * instrument (or As written) drops a picked sound: the instrument's main sound plays again. Choosing the current one
42
+ * keeps it.
43
+ */
44
+ export declare function withSourceSwitch(score: Score, switches: Switches, sourcePart: string, patch: Partial<Omit<PartSwitch, 'octaves'>>, defaults: SwitchDefaults): Switches;
45
+ /** Set the reader's octave moves (by `ScoreNote.id`) of one staff part. */
46
+ export declare function withOctaves(switches: Switches, partId: string, octaves: Record<string, Shift>, defaults: SwitchDefaults): Switches;
47
+ /**
48
+ * Drop the reader's "keep as written" pins (an octave override of 0) that the auto-fit plan no longer overrides: after
49
+ * a level, instrument or fit change (or a host change) the plan of the view these switches give may not move that note
50
+ * any more, and a stale pin would keep its staff "non-plain" (a saved entry) and block auto-fit if the plan moves the
51
+ * note again later. A 0 on a note the plan still moves (8vb on an auto-raised phrase: as written) stays, as do 8va / 8vb
52
+ * moves. The view is the reader's: `scoreForView` at `semitones`, in the pitch view `readerPitch` gives these switches.
53
+ */
54
+ export declare function withoutStalePins(score: Score, switches: Switches, view: {
55
+ scorePitch: 'written' | 'concert';
56
+ semitones: number;
57
+ keyFlip?: boolean;
58
+ }, defaults: SwitchDefaults): Switches;
59
+ /**
60
+ * A staff part's label: its own name and, while it is switched to another instrument, that instrument's short name
61
+ * (the reader shows "Piano → Guitar"). No `switched` when as written, or switched to the instrument the part already is.
62
+ */
63
+ export declare function partLabel(part: Part, switches: Switches): {
64
+ name: string;
65
+ switched?: string;
66
+ };
67
+ /**
68
+ * The octave tool's selection on one staff part: `ids` (ScoreNote ids, score order, whole tie chains), the note the
69
+ * selection started from (`anchor`) and whether a plain click extends it to a passage (only after a single-note pick).
70
+ */
71
+ export type OctaveSelection = {
72
+ partId: string;
73
+ anchor: string;
74
+ ids: string[];
75
+ extendable: boolean;
76
+ };
77
+ /**
78
+ * A click on a note with the octave tool on. A note of a switchable part selects its tie chain; with `extend`
79
+ * (Shift-click), or a plain click after a single-note pick in the same part, it selects every note of that part whose
80
+ * (measure, start) lies between the anchor and this note, in all voices, each with its tie chain. A note the tool can't
81
+ * move (a rest, a drum or TAB staff) keeps the selection.
82
+ */
83
+ export declare function selectOctaveNote(score: Score, current: OctaveSelection | null, noteId: string, extend: boolean): OctaveSelection | null;
84
+ /** "Select phrase": the selection widened to the auto-fit phrases (`phrasesOf`) that contain any of its notes. */
85
+ export declare function selectOctavePhrase(score: Score, selection: OctaveSelection): OctaveSelection;
86
+ /**
87
+ * 8va / 8vb on the selected notes, from where each is shown (`shifts`: auto-fit and the reader's moves): the new shift
88
+ * (kept within ±12) is stored as the note's override, 0 included (a note auto-fit moved goes back as written); a
89
+ * shift auto-fit alone gives (`planned`) needs no override and drops it.
90
+ */
91
+ export declare function steppedOctaves(octaves: Record<string, Shift>, ids: string[], step: 12 | -12, shifts?: Record<string, Shift>, planned?: Record<string, Shift>): Record<string, Shift>;
92
+ /**
93
+ * A Sound pick on a staff part: while the part is switched it only overrides the instrument's main sound (the switch
94
+ * itself is kept); undefined when the part is not switched (the pick then changes the part's own sound).
95
+ */
96
+ export declare function withSound(switches: Switches, partId: string, sound: PartSound): Switches | undefined;
97
+ /** The host's settings as switches (a score without a saved record). */
98
+ export declare function switchesFromSettings(score: Score, settings: InstrumentSettings): Switches;
99
+ /** A host settings change: only the changed keys are applied again (the reader's other choices stay). */
100
+ export declare function applyHostSwitchChange(score: Score, current: Switches, previous: InstrumentSettings, next: InstrumentSettings): Switches;
101
+ /**
102
+ * A fingerprint of the host's instrument settings, saved with the record (`FileState.hostInstruments`): only the usable
103
+ * keys, `instruments` as [key, id] pairs sorted by key. Undefined when the host gives none.
104
+ */
105
+ export declare function hostInstrumentKey(settings: InstrumentSettings): string | undefined;
106
+ /**
107
+ * The host's instrument settings applied again over restored switches: every switchable source part takes the host's
108
+ * instrument (or As written), level and fit. A part whose instrument changes loses its octave moves (and its picked
109
+ * sound); a part whose instrument stays keeps them.
110
+ */
111
+ export declare function withHostSwitches(score: Score, switches: Switches, settings: InstrumentSettings): Switches;
112
+ /**
113
+ * The switches a score opens with. Without a saved record, the host's settings. With one, its switches, unless the
114
+ * host's instrument settings changed since it was saved (`hostInstrumentKey` differs from `hostInstruments`): then the
115
+ * host's are applied again over it (`withHostSwitches`), so a teacher's new instrument reaches returning readers. A host
116
+ * that gives no instrument settings, or the same ones, leaves the reader's saved choices.
117
+ */
118
+ export declare function switchesOnLoad(score: Score, saved: {
119
+ parts: Record<string, PartState>;
120
+ hostInstruments?: string;
121
+ } | null, settings: InstrumentSettings): Switches;
122
+ /**
123
+ * Switches restored from a saved record's parts (already validated by file-state.ts). The staves of a source part
124
+ * switch together, so they are normalised to its first staff with a saved switch (instrument, level, fit): a staff
125
+ * whose switch differs or was dropped takes it, keeping its own octave moves (and its sound when the instrument matches).
126
+ * A staff left as written with the defaults and no octave moves gets no entry, as `withSourceSwitch` would leave it.
127
+ */
128
+ export declare function switchesFromSaved(score: Score, parts: Record<string, PartState>, defaults?: SwitchDefaults): Switches;
129
+ /**
130
+ * The saved pitch view to restore. A concert-basis score used to be forced to (and saved as) concert pitch while no
131
+ * part was switched to a transposing instrument, so a saved 'concert' there was never the reader's choice: it opens as
132
+ * 'written', and a later switch to a B♭/E♭ instrument shows its written pitch.
133
+ */
134
+ export declare function restoredScorePitch(score: Score, switches: Switches, saved: 'written' | 'concert'): 'written' | 'concert';
135
+ /**
136
+ * The pitch view the reader shows. Verovio can only move written pitch to sounding, so a concert-basis score is shown
137
+ * at concert pitch, unless a part is switched to a transposing instrument (it shows its written pitch by default).
138
+ */
139
+ export declare function readerPitch(score: Score, switches: Switches, preferred: 'written' | 'concert'): {
140
+ scorePitch: "concert" | "written";
141
+ concertBasis: boolean;
142
+ forcedConcert: boolean;
143
+ switchedTransposing: boolean;
144
+ hasTransposingParts: boolean;
145
+ };
146
+ /**
147
+ * The Pitch panel's hint for the pitch view `readerPitch` gives. In a concert-basis score the Transposing view moves
148
+ * only the parts switched to a transposing instrument: every other part (a transposing one included) stays at concert pitch.
149
+ */
150
+ export declare function pitchHint(view: ReturnType<typeof readerPitch>): string;
151
+ /** The switched view of `scoreForView`'s result (the switch runs last); with no switches, that result itself. */
152
+ export declare function switchedView(viewed: Score, switches: Switches, scorePitch: 'written' | 'concert'): InstrumentViewResult;
153
+ /** Internal highlights of a switched view, applied before the host's. */
154
+ export declare function switchHighlights(view: InstrumentViewResult): {
155
+ ids: string[];
156
+ className: string;
157
+ }[];
158
+ /**
159
+ * The performance with the switched (or octave-moved) parts' events at the view's sounding pitch. The base timeline
160
+ * is the source score's, at 0 semitones (playback adds `semitones`), so every other part keeps its events as before;
161
+ * the view's pitches already include `semitones`, so they are taken back out here.
162
+ */
163
+ export declare function switchedTimeline(base: BaseTimeline, view: InstrumentViewResult, semitones: number): BaseTimeline;
164
+ /**
165
+ * Parts to play with: a part switched to an instrument plays the target's sound (the view part's), or the sound the
166
+ * reader picked for it while switched (`PartSwitch.sound`).
167
+ */
168
+ export declare function switchedSoundParts(parts: Part[], view: InstrumentViewResult, switches: Switches): Part[];
169
+ export {};
@@ -0,0 +1,92 @@
1
+ import { type InstrumentClef, type InstrumentId, type RangeLevel } from './instrument-model';
2
+ import { type Shift } from './range-fit-core';
3
+ import { type KeyMode, type Measure, type Part, type Score, type ScoreNote } from './score';
4
+ /** The part all staves of one source part share: the MusicXML <score-part id>, or the part's own id for ABC/MIDI. */
5
+ export declare const sourcePartOf: (part: Part) => string;
6
+ /**
7
+ * The playback sound a reader picked for a switched part (the mixer's Sound grid, like `Part.instrument` and
8
+ * `gmSoundfont`: the GM sound's icon id and FluidR3 name; no `gmSoundfont` with `instrument: 'drum-kit'` = the kit).
9
+ */
10
+ export type PartSound = {
11
+ instrument: InstrumentId;
12
+ gmSoundfont?: string;
13
+ };
14
+ /**
15
+ * A reader's switch for one staff part: `instrument` undefined = as written; `octaves` keyed by `ScoreNote.id` (a note's
16
+ * shift from its pitch in the view, overriding auto-fit along its tie chain; 0 keeps a note auto-fit would move);
17
+ * `sound` overrides the target's main sound (only while switched; cleared when the instrument changes).
18
+ */
19
+ export type PartSwitch = {
20
+ instrument?: InstrumentId;
21
+ level: RangeLevel;
22
+ autoRange: boolean;
23
+ octaves: Record<string, Shift>;
24
+ sound?: PartSound;
25
+ };
26
+ /**
27
+ * The MEI pass's view of one switched (or octave-moved) staff: `shift` = [diatonic, semitones] from the baseline MEI
28
+ * (the source's written pitch, after the global interval) to the pitch shown; `octaves` keyed by MEI note id.
29
+ */
30
+ export type StaffSwitch = {
31
+ shift: [number, number];
32
+ clef?: InstrumentClef;
33
+ octaves: Record<string, 12 | -12>;
34
+ };
35
+ /**
36
+ * What the MEI pass needs of one staff of a switched view (carried on `Score.instrumentView`): its `StaffSwitch` and,
37
+ * for a switched part, its keys [measure index, fifths, mode] before the global transposition (the source's written
38
+ * keys moved by `shift`), as `SpellingSpec` staves take them.
39
+ */
40
+ export type EngravedSwitch = StaffSwitch & {
41
+ keys?: [number, number, KeyMode][];
42
+ };
43
+ export type InstrumentViewResult = {
44
+ score: Score;
45
+ /** `ScoreNote.id`s moved by an octave (auto-fit or the reader's). */
46
+ moved: string[];
47
+ /** `ScoreNote.id`s still outside the level's range (switched parts), or moved outside it (unswitched parts). */
48
+ outOfRange: string[];
49
+ /** Each moved note's octave shift (auto-fit and the reader's), and what auto-fit alone would shift (nonzero only). */
50
+ shifts: Record<string, Shift>;
51
+ planned: Record<string, Shift>;
52
+ /** Keyed by part id. */
53
+ staves: Record<string, StaffSwitch>;
54
+ /**
55
+ * TAB parts doubling a switched or octave-moved part (their frets are stale). The renderer reads the same from
56
+ * `score.instrumentView` (tab/tab-parts.ts `planTab`): regenerated while the part is a guitar, else hidden.
57
+ */
58
+ hiddenTab: string[];
59
+ };
60
+ /**
61
+ * Whether the switch is offered for a part: never for the drum kit or percussion, nor for TAB staves. Judged on the
62
+ * part as imported: a Sound pick (`withPickedSound`) changes `instrument` but never what can be switched.
63
+ */
64
+ export declare const switchablePart: (part: Part) => boolean;
65
+ /**
66
+ * A part playing a picked sound (the mixer's Sound grid or a saved record): its icon id, GM program and FluidR3 name,
67
+ * with the instrument it was imported as kept in `importedInstrument`.
68
+ */
69
+ export declare const withPickedSound: (part: Part, sound: {
70
+ instrument: InstrumentId;
71
+ gmProgram?: number;
72
+ gmSoundfont?: string;
73
+ }) => Part;
74
+ export declare const pitched: (note: ScoreNote) => boolean;
75
+ export declare const byStart: (measure: Measure) => ScoreNote[];
76
+ /** Each pitched note's tie chain in a part, in score order (a note alone when untied). */
77
+ export declare function tieChainsOf(part: Part): (id: string) => string[];
78
+ /** Runs of pitched notes per voice per measure, broken at rests and after a slur end, joined across ties. */
79
+ export declare function phrasesOf(part: Part): string[][];
80
+ /**
81
+ * Switches parts of a Score view (`scoreForView`'s result) to other instruments and applies the reader's octave
82
+ * moves. Each part is treated on its own (a grand staff's callers set one `PartSwitch` on every staff). For every
83
+ * note of every part, `soundingMidi(outPart, measure, pitch, 0)` is its judged pitch (sounding, after the view's
84
+ * transposition) plus its octave shift: a concert view's transposing parts (already at sounding pitch) lose their
85
+ * transposition, switched or not. A switched part is written for the target instrument in the written view, at
86
+ * sounding pitch in the concert view. A target without a pitched clef or ranges (the drum kit) is treated as written.
87
+ *
88
+ * The view is the Score's own (`score.pitchView.view`); `scorePitch` only applies to a Score no view was made of.
89
+ * Throws when `score` already carries an instrument view: apply it once, to `scoreForView`'s result (a second pass
90
+ * would stack the shifts).
91
+ */
92
+ export declare function applyInstrumentView(score: Score, switches: Record<string, PartSwitch>, scorePitch: 'written' | 'concert'): InstrumentViewResult;
@@ -1,4 +1,4 @@
1
- import type { Part, Score } from '../score';
1
+ import type { Part, Score, ScoreNote } from '../score';
2
2
  import { type FeatureInventory, type SourceDiagnostic } from './musicxml-source';
3
3
  export type { SourceDiagnostic };
4
4
  export type SourceFormat = 'musicxml' | 'mxl' | 'mei' | 'abc' | 'midi' | 'sample';
@@ -32,6 +32,16 @@ export type SourceDocument = Readonly<{
32
32
  identity: SourceIdentity;
33
33
  readOriginalBytes: () => Uint8Array;
34
34
  }>;
35
+ /**
36
+ * The MEI xml:id `scoreToMei` gives pitch `index` of a note (ABC, MIDI and demo scores, whose pitches carry no
37
+ * `sourceId`). Its continuations into later measures and the pieces of a split duration are separate MEI notes with
38
+ * `_cN` / `_sN` appended; `generatedPitchIdOf` maps those back to this id.
39
+ */
40
+ export declare const generatedPitchId: (note: Pick<ScoreNote, "id" | "pitches">, index: number) => string;
41
+ /** The `generatedPitchId` an MEI note of `scoreToMei` was drawn for (its `_cN` / `_sN` suffixes removed), or null. */
42
+ export declare const generatedPitchIdOf: (meiId: string) => string | null;
43
+ /** Whether a document's engraving MEI was generated by `scoreToMei` from the Score (not an imported MEI or MusicXML). */
44
+ export declare const generatedEngraving: (doc: Pick<SourceDocument, "format" | "engravingFormat"> | undefined) => boolean;
35
45
  /** Deterministic MEI staffDef ID for a Score part, so views can hide staves by identity. */
36
46
  export declare const staffDefXmlId: (partId: string) => string;
37
47
  /** Source feature inventory of an XML source (MusicXML, MXL member or MEI), read with the streaming tokenizer. */
@@ -9,13 +9,17 @@
9
9
  * degrees, Fb/Cb only as passing notes in keys of 6+ flats, flats-only/sharps-only per key, no double accidentals;
10
10
  * - accidentals are re-shown or cancelled within each measure to match the new spellings (@accid / @accid.ges);
11
11
  * - chord symbols (`<harm>`) are respelled for their staff's key;
12
- * - percussion staves never get a key signature or key change (in every view, transposed or not).
12
+ * - percussion staves never get a key signature or key change (in every view, transposed or not);
13
+ * - an instrument-switched staff (instrument-view.ts) moves by its own `shift`, takes the target's clef (the source's
14
+ * clef changes are dropped), and notes an octave move names (`octaves`) move by an octave, before accidentals are
15
+ * re-evaluated.
13
16
  *
14
17
  * Keys are rewritten from a per-staff timeline (the spec's Score keys, else the MEI's): every key signature is removed
15
18
  * from the score definitions and each staff's keys are written where they start or change.
16
19
  *
17
20
  * String-level like mei-view.ts (the worker has no DOMParser); the input is Verovio's own `getMEI` output.
18
21
  */
22
+ import type { InstrumentClef } from '../instrument-model';
19
23
  import { type KeyMode } from '../spelling';
20
24
  import { type StaffSpelling } from './respell';
21
25
  export type SpellingSpec = {
@@ -44,6 +48,10 @@ export type StaffSpec = StaffSpelling & {
44
48
  * spellings, e.g. a B♭ trumpet in Ab sounding in F# with C double sharps), the way the Score view does.
45
49
  */
46
50
  shift?: [number, number];
51
+ /** Octave moves of notes (instrument switch, range fit or the reader's): semitones by MEI note xml:id. */
52
+ octaves?: Record<string, 12 | -12>;
53
+ /** The staff's clef replaced by an instrument switch: set on its staff definitions, its clef changes removed. */
54
+ clef?: InstrumentClef;
47
55
  };
48
56
  /** Does this spec change anything for this MEI (a transposition, a flip, or a percussion staff with keys to strip)? */
49
57
  export declare function spellingNeeded(mei: string, spec: SpellingSpec | undefined): boolean;
@@ -33,11 +33,18 @@ export type TabSpec = {
33
33
  capo: number;
34
34
  /** Use `tab.course`/`tab.fret` already on notation notes as fixed fingerings (false when the view moved pitches). */
35
35
  fixed: boolean;
36
+ /** Generated staves whose `tab.course`/`tab.fret` do not apply even when `fixed` (an instrument view moved their notes). */
37
+ unfixed?: string[];
36
38
  /**
37
39
  * Generated staves whose notes are already at sounding pitch (the concert view of a transposing part): their
38
40
  * `trans.semi` was applied by spelling and must not be applied again.
39
41
  */
40
42
  sounding?: string[];
43
+ /**
44
+ * Generated staves of a part switched to another instrument (instrument-view.ts), by `@n`: semitones from their
45
+ * spelled (shown) pitch to sounding pitch, which replace `trans.semi` (a guitar in the written view: -12).
46
+ */
47
+ switched?: Record<string, number>;
41
48
  /** Imported TAB staves (no notation partner) to re-finger, by staff `@n`: the semitones their pitches move. */
42
49
  refret?: Record<string, number>;
43
50
  };
@@ -1,5 +1,6 @@
1
1
  import type { FileSettings } from './file-state';
2
2
  import type { TabView } from './tab/tab-parts';
3
+ import type { RangeLevel, SwitchInstrumentId } from './instrument-model';
3
4
  export declare const MIN_BPM = 30, MAX_BPM = 240;
4
5
  /** The reader's BPM control range; saved and source tempos outside it are clamped. */
5
6
  export declare const clampBpm: (bpm: number) => number;
@@ -77,6 +78,22 @@ export type PepperScoreSettings = Partial<{
77
78
  bpm: number;
78
79
  /** Spell a transposed view in the enharmonic key (F# major shown as Gb major). */
79
80
  keyFlip: boolean;
81
+ /**
82
+ * Show the first switchable part (every staff of a grand staff) as this instrument: its sound, clef, written pitch
83
+ * and range. Applied when the score has no saved record, like the other settings.
84
+ */
85
+ instrument: SwitchInstrumentId;
86
+ /**
87
+ * Instruments per part; wins over `instrument`. A key is either the 1-based source part number as a string
88
+ * (`{ '2': 'trumpet' }`: score parts in order, a grand staff counted once, a drum kit counted too; `onReady`'s
89
+ * `parts[].sourcePart`) or an `onReady` part id (`parts[].id`; a staff of a grand staff switches its whole part).
90
+ * Keys of parts that cannot be switched (`parts[].switchable` false) are ignored.
91
+ */
92
+ instruments: Record<string, SwitchInstrumentId>;
93
+ /** Range level the switched parts are fitted to: `'beginner'`, `'intermediate'` (default) or `'advanced'`. */
94
+ rangeLevel: RangeLevel;
95
+ /** Move phrases outside the instrument's range by an octave (default `true`). */
96
+ autoRange: boolean;
80
97
  }>;
81
98
  export declare function settingsToPrefs(settings?: PepperScoreSettings): Partial<Prefs>;
82
99
  export declare const viewToMode: (view: "vertical" | "endless") => Prefs["mode"];
@@ -0,0 +1,26 @@
1
+ export type Shift = 0 | 12 | -12;
2
+ /** One event (note or chord): its notes' judged MIDI (sounding plus interval). */
3
+ export type FitEvent = {
4
+ id: string;
5
+ midi: number[];
6
+ };
7
+ /** Merges runs of event ids that a tie links (`[from, to]`), keeping the first run's position. */
8
+ export declare function joinRuns(runs: string[][], ties: [string, string][]): string[][];
9
+ /** The ordered chain of events the ties link around `id` (itself alone when untied), in `order` (event order) when given, else by first appearance in the ties. */
10
+ export declare function tieChainOf(ties: [string, string][], order?: string[]): (id: string) => string[];
11
+ export type ShiftPlan = {
12
+ shifts: Record<string, Shift>;
13
+ bothWays: string[][];
14
+ stillOut: string[][];
15
+ };
16
+ /**
17
+ * Plans an octave per phrase: any note below the range moves every event up, above moves it down, both
18
+ * leaves the phrase (listed in `bothWays`); a phrase still out after its move keeps it (listed in
19
+ * `stillOut`). A chord is out when any of its notes is. No range or `autoRange` off plans no shifts.
20
+ */
21
+ export declare function planShifts(phrases: FitEvent[][], range: {
22
+ low: number;
23
+ high: number;
24
+ } | undefined, autoRange: boolean): ShiftPlan;
25
+ /** Sets single events from the user's overrides, each extended along its tie chain (the first override on a chain wins); overrides beat planned shifts. */
26
+ export declare function applyOverrides(shifts: Record<string, Shift>, overrides: Record<string, Shift>, chainOf: (id: string) => string[]): Record<string, Shift>;
@@ -1,4 +1,5 @@
1
1
  import type { SourceDocument } from './notation/document';
2
+ import type { InstrumentId as ModelInstrumentId } from './instrument-model';
2
3
  export type Pitch = {
3
4
  step: string;
4
5
  octave: number;
@@ -112,6 +113,8 @@ export type Measure = {
112
113
  };
113
114
  export type Part = {
114
115
  id: string;
116
+ /** The MusicXML <score-part id> every staff of one part shares; absent for ABC/MIDI single parts (use `id`). */
117
+ sourcePartId?: string;
115
118
  name: string;
116
119
  clef: string;
117
120
  key?: number;
@@ -124,6 +127,11 @@ export type Part = {
124
127
  measures: Measure[];
125
128
  /** A tablature staff (MusicXML clef sign TAB), imported by Verovio as a `tab.guitar` staff. */
126
129
  tab?: boolean;
130
+ /**
131
+ * The instrument the part was imported as, kept when a reader's Sound pick (or a saved one) replaces `instrument`
132
+ * with the sound's icon id. What the instrument switch offers keys on it (instrument-view.ts `switchablePart`).
133
+ */
134
+ importedInstrument?: InstrumentId;
127
135
  /**
128
136
  * The part this TAB staff doubles (its notation staff). A doubling TAB part is engraved but never performed, has no
129
137
  * annotation lanes and is not listed in part controls, so the instrument shows and plays once (lib/tab/tab-parts.ts).
@@ -144,9 +152,16 @@ export type Score = {
144
152
  parts: Part[];
145
153
  /** Set on a transposed or flipped view of a score (transpose.ts `scoreForView`). */
146
154
  pitchView?: import('./transpose').PitchView;
155
+ /**
156
+ * Set on an instrument-switched view (instrument-view.ts `applyInstrumentView`): what the MEI pass needs of its
157
+ * switched and octave-moved staves, by part id. Dropped by `scoreForView` whenever it moves pitches.
158
+ */
159
+ instrumentView?: {
160
+ staves: Record<string, import('./instrument-view').EngravedSwitch>;
161
+ };
147
162
  document?: SourceDocument;
148
163
  };
149
- export type InstrumentId = 'piano' | 'guitar' | 'bassguitar' | 'double-bass' | 'drum-kit' | 'alto-saxophone' | 'tenor-saxophone' | 'bari-saxophone' | 'flute' | 'clarinet' | 'trumpet' | 'trombone' | 'violin' | 'viola' | 'cello' | 'microphone';
164
+ export type InstrumentId = ModelInstrumentId;
150
165
  export declare const GM_FAMILIES: readonly ["Keyboard", "Guitar", "Voice", "Percussion", "Drums", "Woodwinds", "Brass", "Strings", "FX / Other"];
151
166
  export type GmFamily = typeof GM_FAMILIES[number];
152
167
  export type GmSound = {