@viritura/score-engine 0.1.1-next.7 → 0.1.1-next.9

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/dist/index.js CHANGED
@@ -53,6 +53,44 @@ function plainTextContent(content) {
53
53
  return content.map((chunk) => "text" in chunk ? chunk.text : "").join("");
54
54
  }
55
55
 
56
+ // packages/core/src/model/instrumentChange.ts
57
+ init_define_import_meta_env();
58
+ var MEASURE_START = [0, 1];
59
+ function comparePositions(a, b) {
60
+ return a[0] * b[1] - b[0] * a[1];
61
+ }
62
+ function changePosition(change) {
63
+ return change.position?.fraction ?? MEASURE_START;
64
+ }
65
+ function initialInstrumentState(part) {
66
+ let ext = part._x?.viritura, instrumentKey = ext?.initialInstrument, instrument = instrumentKey !== void 0 && ext?.instruments && Object.hasOwn(ext.instruments, instrumentKey) ? ext.instruments[instrumentKey] : void 0, transposition = part.transposition ?? instrument?.transposition;
67
+ return {
68
+ ...instrumentKey !== void 0 ? { instrumentKey } : {},
69
+ ...instrument ? { instrument } : {},
70
+ ...transposition ? { transposition } : {}
71
+ };
72
+ }
73
+ function applyChange(part, state, change) {
74
+ if (change.instrument === void 0)
75
+ return { ...state, ...change.transposition ? { transposition: change.transposition } : {} };
76
+ let instruments = part._x?.viritura?.instruments, instrument = instruments && Object.hasOwn(instruments, change.instrument) ? instruments[change.instrument] : void 0, transposition = change.transposition ?? instrument?.transposition;
77
+ return {
78
+ instrumentKey: change.instrument,
79
+ ...instrument ? { instrument } : {},
80
+ ...transposition ? { transposition } : {}
81
+ };
82
+ }
83
+ function listInstrumentChanges(part) {
84
+ let located = [], state = initialInstrumentState(part);
85
+ return part.measures.forEach((measure, measureIndex) => {
86
+ let ordered = [...measure.instrumentChanges ?? []].sort(
87
+ (a, b) => comparePositions(changePosition(a), changePosition(b))
88
+ );
89
+ for (let change of ordered)
90
+ state = applyChange(part, state, change), located.push({ measureIndex, position: changePosition(change), change, state });
91
+ }), located;
92
+ }
93
+
56
94
  // packages/core/src/model/event.ts
57
95
  init_define_import_meta_env();
58
96
  function isRest(event) {
@@ -1870,9 +1908,9 @@ function interpolateBeatAnchors(bounds, beat, endX) {
1870
1908
 
1871
1909
  // packages/score-engine/src/buildInfo.ts
1872
1910
  init_define_import_meta_env();
1873
- var PACKAGE_VERSION = "0.1.1-next.7", BUNDLE_WORKER_FILE = "score-engine.worker.js";
1911
+ var PACKAGE_VERSION = "0.1.1-next.9", BUNDLE_WORKER_FILE = "score-engine.worker.js";
1874
1912
  function buildCommit() {
1875
- return "5f67e97a568c1617adbc7221871d70f7cb1fa821";
1913
+ return "8f2a6533eef0ec77252c76925031cfbc732e3271";
1876
1914
  }
1877
1915
  function bundledAssetBase() {
1878
1916
  let moduleUrl = import.meta.url;
@@ -2764,26 +2802,36 @@ function parsePart(raw) {
2764
2802
  name: raw.name ?? "",
2765
2803
  measures: parsePartMeasures(raw.measures)
2766
2804
  };
2767
- if (raw.id && (part.id = raw.id), raw.shortName !== void 0 && (part.shortName = raw.shortName), raw.staves !== void 0 && (part.staves = raw.staves), raw.transposition) {
2768
- let t = raw.transposition;
2769
- part.transposition = {
2770
- interval: {
2771
- halfSteps: t.interval.halfSteps,
2772
- staffDistance: t.interval.staffDistance
2773
- }
2774
- }, t.keyFifthsFlipAt !== void 0 && (part.transposition.keyFifthsFlipAt = t.keyFifthsFlipAt), t.prefersWrittenPitches !== void 0 && (part.transposition.prefersWrittenPitches = t.prefersWrittenPitches);
2775
- }
2776
- raw.kit && (part.kit = parseKit(raw.kit));
2805
+ raw.id && (part.id = raw.id), raw.shortName !== void 0 && (part.shortName = raw.shortName), raw.staves !== void 0 && (part.staves = raw.staves), raw.transposition && (part.transposition = parseTransposition(raw.transposition)), raw.kit && (part.kit = parseKit(raw.kit));
2777
2806
  let viritura = raw._x?.viritura;
2778
2807
  if (viritura) {
2779
2808
  viritura.chordSymbolVisibility !== void 0 && (part.chordSymbolVisibility = viritura.chordSymbolVisibility);
2780
2809
  let ext = {};
2781
2810
  typeof viritura.instrumentId == "string" && (ext.instrumentId = viritura.instrumentId), typeof viritura.midiProgram == "number" && (ext.midiProgram = viritura.midiProgram), typeof viritura.family == "string" && (ext.family = viritura.family);
2782
2811
  let sp = viritura.spatial;
2783
- sp && typeof sp.x == "number" && typeof sp.y == "number" && (ext.spatial = { x: sp.x, y: sp.y }), Object.keys(ext).length > 0 && (part._x = { viritura: ext });
2812
+ sp && typeof sp.x == "number" && typeof sp.y == "number" && (ext.spatial = { x: sp.x, y: sp.y }), viritura.instruments && (ext.instruments = Object.fromEntries(
2813
+ Object.entries(viritura.instruments).map(([key, instrument]) => [key, parseInstrumentDefinition(instrument)])
2814
+ )), viritura.initialInstrument !== void 0 && (ext.initialInstrument = viritura.initialInstrument), Object.keys(ext).length > 0 && (part._x = { viritura: ext });
2784
2815
  }
2785
2816
  return part;
2786
2817
  }
2818
+ function parseTransposition(t) {
2819
+ let transposition = {
2820
+ interval: {
2821
+ halfSteps: t.interval.halfSteps,
2822
+ staffDistance: t.interval.staffDistance
2823
+ }
2824
+ };
2825
+ return t.keyFifthsFlipAt !== void 0 && (transposition.keyFifthsFlipAt = t.keyFifthsFlipAt), t.prefersWrittenPitches !== void 0 && (transposition.prefersWrittenPitches = t.prefersWrittenPitches), transposition;
2826
+ }
2827
+ function parseInstrumentDefinition(raw) {
2828
+ let instrument = { instrumentId: raw.instrumentId };
2829
+ return raw.name !== void 0 && (instrument.name = raw.name), raw.shortName !== void 0 && (instrument.shortName = raw.shortName), raw.transposition && (instrument.transposition = parseTransposition(raw.transposition)), raw.midiProgram !== void 0 && (instrument.midiProgram = raw.midiProgram), instrument;
2830
+ }
2831
+ function parseInstrumentChange(raw) {
2832
+ let change = {};
2833
+ return raw.position && (change.position = parseRhythmicPosition(raw.position)), raw.instrument !== void 0 && (change.instrument = raw.instrument), raw.transposition && (change.transposition = parseTransposition(raw.transposition)), raw.instruction && (change.instruction = {}, raw.instruction.text !== void 0 && (change.instruction.text = raw.instruction.text), raw.instruction.hidden !== void 0 && (change.instruction.hidden = raw.instruction.hidden)), raw.reminder && (change.reminder = {}, raw.reminder.text !== void 0 && (change.reminder.text = raw.reminder.text), raw.reminder.hidden !== void 0 && (change.reminder.hidden = raw.reminder.hidden)), change;
2834
+ }
2787
2835
  function parseKit(raw) {
2788
2836
  let result = {};
2789
2837
  for (let [id, value] of Object.entries(raw))
@@ -2812,7 +2860,7 @@ function parsePartMeasure(raw) {
2812
2860
  return viritura && (viritura.pedals && (pm.pedals = viritura.pedals.map(parsePedal)), viritura.expressions && (pm.expressions = viritura.expressions.map(parseTextExpression)), viritura.condensingOverride && (pm.condensingOverride = viritura.condensingOverride), viritura.groupingDisplayOverrides && viritura.groupingDisplayOverrides.length > 0 && (pm.groupingDisplayOverrides = viritura.groupingDisplayOverrides.map((override) => ({
2813
2861
  staff: override.staff,
2814
2862
  groupingDisplay: override.groupingDisplay
2815
- }))), viritura.staffMeters && viritura.staffMeters.length > 0 && (pm.staffMeters = viritura.staffMeters.map(parseStaffMeterChange))), pm;
2863
+ }))), viritura.staffMeters && viritura.staffMeters.length > 0 && (pm.staffMeters = viritura.staffMeters.map(parseStaffMeterChange)), viritura.instrumentChanges && viritura.instrumentChanges.length > 0 && (pm.instrumentChanges = viritura.instrumentChanges.map(parseInstrumentChange))), pm;
2816
2864
  }
2817
2865
  function parseStaffMeterChange(raw) {
2818
2866
  if ("useGlobal" in raw)
@@ -2876,6 +2924,17 @@ function parsePositionedStaffConfig(raw) {
2876
2924
  return raw.id !== void 0 && (positioned.id = raw.id), raw._c !== void 0 && (positioned._c = raw._c), raw._x !== void 0 && (positioned._x = raw._x), raw.position && (positioned.position = parseRhythmicPosition(raw.position)), raw.staff !== void 0 && (positioned.staff = raw.staff), positioned;
2877
2925
  }
2878
2926
 
2927
+ // packages/format/src/mnx/parseInstrumentChangeStyle.ts
2928
+ init_define_import_meta_env();
2929
+ function parseRootInstrumentChangeStyle(rootX) {
2930
+ let raw = rootX?.viritura?.instrumentChangeStyle;
2931
+ if (raw)
2932
+ return {
2933
+ ...typeof raw.showChangeLabel == "boolean" ? { showChangeLabel: raw.showChangeLabel } : {},
2934
+ ...typeof raw.showAdvanceReminder == "boolean" ? { showAdvanceReminder: raw.showAdvanceReminder } : {}
2935
+ };
2936
+ }
2937
+
2879
2938
  // packages/format/src/mnx/parseLayout.ts
2880
2939
  init_define_import_meta_env();
2881
2940
  function parseLayoutDefinition(raw) {
@@ -3184,6 +3243,8 @@ function applyRootExtensions(score, rootX) {
3184
3243
  timeSignatures && (score.timeSignatures = timeSignatures);
3185
3244
  let chordSymbolStyle = parseRootChordSymbolStyle(rootX);
3186
3245
  chordSymbolStyle && (score.chordSymbolStyle = chordSymbolStyle);
3246
+ let instrumentChangeStyle = parseRootInstrumentChangeStyle(rootX);
3247
+ instrumentChangeStyle && (score.instrumentChangeStyle = instrumentChangeStyle);
3187
3248
  let soundProfile = parseRootSoundProfile(rootX);
3188
3249
  soundProfile && (score.soundProfile = soundProfile);
3189
3250
  let videoSync = parseRootVideoSync(rootX);
@@ -4250,11 +4311,13 @@ function applyMeasureTechniques(expressions, ctx, state, caps, ratioForStaff = (
4250
4311
  let marks = expressions.map((e) => ({
4251
4312
  action: classifyTechniqueText(plainTextContent(e.text)),
4252
4313
  beat: fractionToBeats(e.position.fraction) * ratioForStaff(e.staff)
4253
- })).filter((m) => m.action !== null).sort((a, b) => a.beat - b.beat), { program, muted } = state;
4314
+ })).filter((m) => m.action !== null).sort((a, b) => a.beat - b.beat), { program, muted } = state, instrumentKey = state.instrumentKey ?? ctx.instrumentAtBeatOffset?.(0).key;
4254
4315
  for (let mark of marks) {
4255
- let time = ctx.model.timeAtBeat(ctx.measureStartBeat + mark.beat);
4256
- if (mark.action.kind === "bow" && caps.bow) {
4257
- let target = mark.action.pizz ? GM_PIZZICATO_STRINGS : ctx.gmProgram;
4316
+ let instrument = ctx.instrumentAtBeatOffset?.(mark.beat), baselineProgram = instrument?.program ?? ctx.gmProgram;
4317
+ instrument && instrument.key !== instrumentKey && (program = baselineProgram, muted = !1, instrumentKey = instrument.key);
4318
+ let bowCapable = instrument ? ARCO_CAPABLE_PROGRAMS.has(baselineProgram) : caps.bow, muteFamily = instrument ? muteFamilyForProgram(baselineProgram) : caps.mute, time = ctx.model.timeAtBeat(ctx.measureStartBeat + mark.beat);
4319
+ if (mark.action.kind === "bow" && bowCapable) {
4320
+ let target = mark.action.pizz ? GM_PIZZICATO_STRINGS : baselineProgram;
4258
4321
  if (target === program) continue;
4259
4322
  program = target, ctx.out.push({
4260
4323
  type: "programChange",
@@ -4265,14 +4328,136 @@ function applyMeasureTechniques(expressions, ctx, state, caps, ratioForStaff = (
4265
4328
  channel: ctx.channel,
4266
4329
  program: target
4267
4330
  });
4268
- } else if (mark.action.kind === "mute" && caps.mute) {
4331
+ } else if (mark.action.kind === "mute" && muteFamily) {
4269
4332
  if (mark.action.muted === muted) continue;
4270
4333
  muted = mark.action.muted;
4271
- let curve = muted ? MUTE_CURVES[caps.mute] : OPEN_CURVE;
4334
+ let curve = muted ? MUTE_CURVES[muteFamily] : OPEN_CURVE;
4272
4335
  emitCc(ctx, time, CC_BRIGHTNESS, curve.brightness), emitCc(ctx, time, CC_RESONANCE, curve.resonance);
4273
4336
  }
4274
4337
  }
4275
- return { program, muted };
4338
+ return { program, muted, ...instrumentKey === void 0 ? {} : { instrumentKey } };
4339
+ }
4340
+
4341
+ // packages/midi/src/instrumentPlayback/index.ts
4342
+ init_define_import_meta_env();
4343
+ function instrumentPart(part, instrument) {
4344
+ return instrument ? {
4345
+ ...part,
4346
+ name: instrument.name ?? part.name,
4347
+ _x: {
4348
+ ...part._x,
4349
+ viritura: {
4350
+ ...part._x?.viritura,
4351
+ instrumentId: instrument.instrumentId,
4352
+ midiProgram: instrument.midiProgram,
4353
+ initialInstrument: void 0,
4354
+ instruments: void 0
4355
+ }
4356
+ }
4357
+ } : part;
4358
+ }
4359
+ function playbackInstruments(part) {
4360
+ let initial = initialInstrumentState(part);
4361
+ if (initial.instrumentKey !== void 0 && !initial.instrument)
4362
+ throw new Error(`Unknown initial instrument "${initial.instrumentKey}" in "${part.name}".`);
4363
+ let result = [{ key: "initial", part: instrumentPart(part, initial.instrument) }], seen = /* @__PURE__ */ new Set();
4364
+ for (let located of listInstrumentChanges(part)) {
4365
+ let [numerator, denominator] = located.position;
4366
+ if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
4367
+ let key = located.change.instrument;
4368
+ if (!(key === void 0 || seen.has(key))) {
4369
+ if (!located.state.instrument) throw new Error(`Unknown instrument "${key}" in "${part.name}".`);
4370
+ seen.add(key), result.push({ key: `instrument:${key}`, part: instrumentPart(part, located.state.instrument) });
4371
+ }
4372
+ }
4373
+ return result;
4374
+ }
4375
+ function playbackInstrumentKeyAt(part, measureIndex, beatOffset = 0) {
4376
+ let key = "initial";
4377
+ for (let located of listInstrumentChanges(part)) {
4378
+ if (located.measureIndex > measureIndex || located.measureIndex === measureIndex && located.position[0] / located.position[1] * 4 > beatOffset) break;
4379
+ located.change.instrument !== void 0 && (key = `instrument:${located.change.instrument}`);
4380
+ }
4381
+ return key;
4382
+ }
4383
+ function boundariesForPart(part, timing, programs, initialProgram) {
4384
+ let instruments = playbackInstruments(part), definitions = new Map(instruments.map((instrument) => [instrument.key, instrument.part])), changes = listInstrumentChanges(part).filter((located) => located.change.instrument !== void 0), boundaries = [], add = (beat, key) => {
4385
+ let program = programs?.get(key) ?? (key === "initial" ? initialProgram : void 0) ?? definitions.get(key)?._x?.viritura?.midiProgram;
4386
+ boundaries.push({
4387
+ beat,
4388
+ time: timing.model.timeAtBeat(beat),
4389
+ key,
4390
+ program: typeof program == "number" && Number.isInteger(program) && program >= 0 && program <= 127 ? program : void 0
4391
+ });
4392
+ };
4393
+ return timing.measureOrder.forEach((measureIndex, expandedIndex) => {
4394
+ let start = timing.measureStartBeats[expandedIndex];
4395
+ add(start, playbackInstrumentKeyAt(part, measureIndex));
4396
+ for (let located of changes) {
4397
+ if (located.measureIndex !== measureIndex || located.position[0] === 0) continue;
4398
+ let [numerator, denominator] = located.position;
4399
+ if (denominator <= 0 || numerator < 0) throw new Error(`Invalid instrument-change position in "${part.name}".`);
4400
+ add(start + numerator / denominator * 4, `instrument:${located.change.instrument}`);
4401
+ }
4402
+ }), boundaries;
4403
+ }
4404
+ function applyInstrumentPlayback(score, events, timing, programs, initialPrograms) {
4405
+ let diagnostics = [];
4406
+ return score.parts.forEach((part, partIndex) => {
4407
+ if (playbackInstruments(part).length === 1) return;
4408
+ let boundaries = boundariesForPart(part, timing, programs?.get(partIndex), initialPrograms?.[partIndex]), partEvents = events.filter((event) => event.partIndex === partIndex), lanes = new Set(partEvents.flatMap((event) => event.playbackLaneId ? [event.playbackLaneId] : []));
4409
+ lanes.size === 0 && lanes.add(`part:${partIndex}`);
4410
+ let missingPrograms = [
4411
+ ...new Set(boundaries.filter((boundary) => boundary.program === void 0).map((boundary) => boundary.key))
4412
+ ];
4413
+ missingPrograms.length > 0 && diagnostics.push({
4414
+ code: "instrument-program-unavailable",
4415
+ message: `Physical MIDI cannot select ${missingPrograms.join(", ")} in "${part.name}"; provide resolved instrumentPrograms or a MIDI program fallback.`,
4416
+ playbackLaneIds: [...lanes]
4417
+ });
4418
+ for (let event of [...partEvents]) {
4419
+ if (event.playbackLaneId) continue;
4420
+ let [first, ...others] = lanes;
4421
+ event.playbackLaneId = first;
4422
+ for (let lane of others) {
4423
+ let scoped = { ...event, playbackLaneId: lane };
4424
+ events.push(scoped), partEvents.push(scoped);
4425
+ }
4426
+ }
4427
+ let attackDelays = /* @__PURE__ */ new Map(), attackKey = (event) => JSON.stringify([event.playbackLaneId, event.channel, event.midiNote, event.scoreBeat, event.scoreEventId]);
4428
+ for (let event of partEvents) {
4429
+ if (event.type === "controlChange" && event.cc !== 74 && event.cc !== 71) continue;
4430
+ let beat = event.scoreBeat ?? timing.model.beatAtTime(event.time), instrument = "initial", instrumentStart = 0;
4431
+ for (let boundary of boundaries) {
4432
+ if (boundary.beat > beat + 1e-9) break;
4433
+ instrument = boundary.key, instrumentStart = boundary.time;
4434
+ }
4435
+ if (event.type === "noteOn") {
4436
+ let delay = Math.max(0, instrumentStart - event.time);
4437
+ attackDelays.set(attackKey(event), delay), event.time = Math.max(event.time, instrumentStart);
4438
+ }
4439
+ event.playbackInstrumentKey = instrument;
4440
+ }
4441
+ for (let event of partEvents)
4442
+ event.type === "noteOff" && (event.time += attackDelays.get(attackKey(event)) ?? 0);
4443
+ for (let lane of lanes) {
4444
+ let previous;
4445
+ for (let boundary of boundaries)
4446
+ boundary.key !== previous && (previous = boundary.key, events.push({
4447
+ type: "programChange",
4448
+ time: boundary.time,
4449
+ scoreBeat: boundary.beat,
4450
+ midiNote: 0,
4451
+ velocity: 0,
4452
+ partIndex,
4453
+ playbackLaneId: lane,
4454
+ channel: partEvents.find((event) => event.playbackLaneId === lane)?.channel ?? 0,
4455
+ playbackInstrumentKey: boundary.key,
4456
+ instrumentChange: !0,
4457
+ program: boundary.program
4458
+ }));
4459
+ }
4460
+ }), diagnostics;
4276
4461
  }
4277
4462
 
4278
4463
  // packages/midi/src/tempoMap.ts
@@ -5058,12 +5243,20 @@ function generateTimeline(inputScore, options) {
5058
5243
  );
5059
5244
  if (chords)
5060
5245
  for (let event of chordMidiEvents(chords, measureOrder, tempoBuild)) allEvents.push(event);
5061
- let diagnostics = assignPhysicalMidiChannels(allEvents, score), EVENT_TYPE_ORDER = { noteOff: 0, programChange: 1, controlChange: 1, noteOn: 2 };
5062
- allEvents.sort((a, b) => {
5246
+ let EVENT_TYPE_ORDER = { noteOff: 0, programChange: 1, controlChange: 1, noteOn: 2 }, compareEvents = (a, b) => {
5063
5247
  if (Math.abs(a.time - b.time) > 1e-9) return a.time - b.time;
5064
5248
  let aOrder = EVENT_TYPE_ORDER[a.type] ?? 1, bOrder = EVENT_TYPE_ORDER[b.type] ?? 1;
5065
- return aOrder !== bOrder ? aOrder - bOrder : a.midiNote - b.midiNote;
5066
- });
5249
+ return aOrder !== bOrder ? aOrder - bOrder : a.instrumentChange !== b.instrumentChange ? a.instrumentChange ? -1 : 1 : a.midiNote - b.midiNote;
5250
+ };
5251
+ allEvents.sort(compareEvents);
5252
+ let instrumentDiagnostics = applyInstrumentPlayback(
5253
+ score,
5254
+ allEvents,
5255
+ { model, measureStartBeats, measureOrder },
5256
+ options?.instrumentPrograms,
5257
+ options?.partPrograms
5258
+ ), diagnostics = [...assignPhysicalMidiChannels(allEvents, score), ...instrumentDiagnostics];
5259
+ allEvents.sort(compareEvents);
5067
5260
  let duration = model.timeAtBeat(tempoBuild.totalBeats), measureTimeSignatures = measureOrder.map((origIdx) => {
5068
5261
  let ts = resolveActiveTime(globalMeasures, origIdx);
5069
5262
  return { count: ts.count, unit: ts.unit };
@@ -5179,11 +5372,13 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
5179
5372
  );
5180
5373
  for (let lane of dynamicProgram.lanes.values())
5181
5374
  emitDynamicsCc11(lane.envelope, partIdx, channel, allEvents, lane.id);
5182
- let bowCapable = ARCO_CAPABLE_PROGRAMS.has(gmProgram), muteFamily = muteFamilyForProgram(gmProgram), techniqueEnabled = bowCapable || muteFamily !== null, techniqueState = { program: gmProgram, muted: !1 };
5375
+ let techniqueState = { program: gmProgram, muted: !1, instrumentKey: "initial" }, instruments = new Map(playbackInstruments(part).map(({ key, part: instrumentPart2 }) => [key, instrumentPart2])), programForInstrument = (key) => key === "initial" ? gmProgram : options?.instrumentPrograms?.get(partIdx)?.get(key) ?? instruments.get(key)?._x?.viritura?.midiProgram ?? -1;
5183
5376
  for (let expandedIdx = 0; expandedIdx < measureOrder.length; expandedIdx++) {
5184
5377
  let origMeasureIdx = measureOrder[expandedIdx], partMeasure = part.measures[origMeasureIdx];
5185
5378
  if (!partMeasure) continue;
5186
- let measureStartTime = measureStartTimes[expandedIdx], measureStartBeat = measureStartBeats[expandedIdx], activeTime = resolveActiveTime(globalMeasures, origMeasureIdx), activeKeyFifths = resolveActiveKeyFifths(globalMeasures, origMeasureIdx), measureHolds = holdSchedule[expandedIdx] ?? [], measureSpq = model.spbAtBeat(measureStartBeat), fermataGroups = measureHolds.filter((h) => h.kind === "fermata").map((h) => ({
5379
+ let measureStartTime = measureStartTimes[expandedIdx], measureStartBeat = measureStartBeats[expandedIdx], instrumentKey = playbackInstrumentKeyAt(part, origMeasureIdx), activeProgram = programForInstrument(instrumentKey), bowCapable = ARCO_CAPABLE_PROGRAMS.has(activeProgram), muteFamily = muteFamilyForProgram(activeProgram), techniqueEnabled = bowCapable || muteFamily !== null;
5380
+ instrumentKey !== techniqueState.instrumentKey && (techniqueState = { program: activeProgram, muted: !1, instrumentKey });
5381
+ let activeTime = resolveActiveTime(globalMeasures, origMeasureIdx), activeKeyFifths = resolveActiveKeyFifths(globalMeasures, origMeasureIdx), measureHolds = holdSchedule[expandedIdx] ?? [], measureSpq = model.spbAtBeat(measureStartBeat), fermataGroups = measureHolds.filter((h) => h.kind === "fermata").map((h) => ({
5187
5382
  startBeat: h.startBeat ?? h.atBeat,
5188
5383
  spanEndBeat: h.spanEndBeat ?? h.atBeat,
5189
5384
  insertBeat: h.atBeat
@@ -5199,7 +5394,11 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
5199
5394
  pendingLegato,
5200
5395
  voiceKey: "",
5201
5396
  out: allEvents,
5202
- gmProgram,
5397
+ gmProgram: activeProgram,
5398
+ instrumentAtBeatOffset: instruments.size > 1 ? (offset) => {
5399
+ let key = playbackInstrumentKeyAt(part, origMeasureIdx, offset);
5400
+ return { key, program: programForInstrument(key) };
5401
+ } : void 0,
5203
5402
  timeSig: activeTime,
5204
5403
  staffMeterRatio: 1,
5205
5404
  keyFifths: activeKeyFifths,
@@ -5213,7 +5412,7 @@ function processPart(score, partIdx, options, partChannels, timing, measureOrder
5213
5412
  measureSpq,
5214
5413
  fermataGroups
5215
5414
  };
5216
- techniqueEnabled && (techniqueState = applyMeasureTechniques(
5415
+ (techniqueEnabled || instruments.size > 1) && (techniqueState = applyMeasureTechniques(
5217
5416
  partMeasure.expressions,
5218
5417
  ctx,
5219
5418
  techniqueState,
@@ -5313,6 +5512,7 @@ function tryProcessSpecialEvent(ctx, event, measureStartTime, beatOffset, baseVe
5313
5512
  return tremoloMarks && tremoloMarks >= 1 && tremoloMarks <= 3 ? processSingleNoteTremolo(ctx, event, measureStartTime, beatOffset, baseVelocity, beats, tremoloMarks) : event.markings?.trill && tryProcessTrill(ctx, event, measureStartTime, beatOffset, baseVelocity, beats) ? beats : null;
5314
5513
  }
5315
5514
  function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity, tupletRatio) {
5515
+ ctx.gmProgram = ctx.instrumentAtBeatOffset?.(beatOffset * ctx.staffMeterRatio).program ?? ctx.gmProgram;
5316
5516
  let { partIndex, channel, tieTargets, out, timeSig, kitMidiMap, kitAltProgramMap } = ctx, beats = durationBeats3(event.duration) * tupletRatio;
5317
5517
  if (isRest(event))
5318
5518
  return flushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, ctx.out), beats;
@@ -5386,7 +5586,8 @@ function processNoteEvent(ctx, event, measureStartTime, beatOffset, baseVelocity
5386
5586
  velocity: 0,
5387
5587
  partIndex,
5388
5588
  channel,
5389
- drumKitProgram
5589
+ drumKitProgram,
5590
+ ...ctx.instrumentAtBeatOffset ? { scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio } : {}
5390
5591
  });
5391
5592
  }
5392
5593
  }
@@ -5421,7 +5622,8 @@ function emitFreshNote(ctx, midiNote, startTime, timingOffset, durationSec, dura
5421
5622
  midiNote,
5422
5623
  velocity: 0,
5423
5624
  partIndex,
5424
- channel
5625
+ channel,
5626
+ ...ctx.instrumentAtBeatOffset ? metadata : {}
5425
5627
  });
5426
5628
  } else {
5427
5629
  let off = {
@@ -5430,7 +5632,8 @@ function emitFreshNote(ctx, midiNote, startTime, timingOffset, durationSec, dura
5430
5632
  midiNote,
5431
5633
  velocity: 0,
5432
5634
  partIndex,
5433
- channel
5635
+ channel,
5636
+ ...ctx.instrumentAtBeatOffset ? metadata : {}
5434
5637
  };
5435
5638
  legatoOut ? pushVoiceLegato(ctx.pendingLegato, ctx.voiceKey, off) : out.push(off);
5436
5639
  }
@@ -5478,7 +5681,8 @@ function processKitRoll(ctx, event, measureStartTime, beatOffset, baseVelocity,
5478
5681
  velocity: 0,
5479
5682
  partIndex,
5480
5683
  channel,
5481
- drumKitProgram
5684
+ drumKitProgram,
5685
+ ...ctx.instrumentAtBeatOffset ? { scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio } : {}
5482
5686
  });
5483
5687
  }
5484
5688
  return beats;
@@ -5515,7 +5719,15 @@ function processSingleNoteTremolo(ctx, event, measureStartTime, beatOffset, base
5515
5719
  scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio,
5516
5720
  scoreDurationBeats: totalBeats * ctx.staffMeterRatio,
5517
5721
  scoreEventId: event.id
5518
- }), out.push({ type: "noteOff", time: jitteredTremStart + durationSec, midiNote, velocity: 0, partIndex, channel }));
5722
+ }), out.push({
5723
+ type: "noteOff",
5724
+ time: jitteredTremStart + durationSec,
5725
+ midiNote,
5726
+ velocity: 0,
5727
+ partIndex,
5728
+ channel,
5729
+ ...ctx.instrumentAtBeatOffset ? { scoreBeat: ctx.measureStartBeat + beatOffset * ctx.staffMeterRatio } : {}
5730
+ }));
5519
5731
  }
5520
5732
  return out.push({
5521
5733
  type: "programChange",
@@ -5548,7 +5760,8 @@ function processSingleNoteTremolo(ctx, event, measureStartTime, beatOffset, base
5548
5760
  midiNote,
5549
5761
  velocity: 0,
5550
5762
  partIndex,
5551
- channel
5763
+ channel,
5764
+ ...ctx.instrumentAtBeatOffset ? { scoreBeat: ctx.measureStartBeat + subBeatOffset * ctx.staffMeterRatio } : {}
5552
5765
  }));
5553
5766
  }
5554
5767
  }
@@ -5599,7 +5812,8 @@ function tryProcessTrill(ctx, event, measureStartTime, beatOffset, baseVelocity,
5599
5812
  midiNote,
5600
5813
  velocity: 0,
5601
5814
  partIndex,
5602
- channel
5815
+ channel,
5816
+ ...ctx.instrumentAtBeatOffset ? { scoreBeat: ctx.measureStartBeat + subBeatOffset * ctx.staffMeterRatio } : {}
5603
5817
  });
5604
5818
  }
5605
5819
  }