@modernized/osmd-audio-player 1.0.1

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.
Files changed (50) hide show
  1. package/.circleci/config.yml +18 -0
  2. package/.gitattributes +12 -0
  3. package/.github/workflows/ci.yml +23 -0
  4. package/.prettierrc +4 -0
  5. package/.prettierrc.js +4 -0
  6. package/CLAUDE.md +96 -0
  7. package/LICENSE +21 -0
  8. package/README.md +79 -0
  9. package/dist/PlaybackEngine.d.ts +55 -0
  10. package/dist/PlaybackEngine.js +211 -0
  11. package/dist/PlaybackScheduler.d.ts +34 -0
  12. package/dist/PlaybackScheduler.js +105 -0
  13. package/dist/index.d.ts +2 -0
  14. package/dist/index.js +2 -0
  15. package/dist/internals/EventEmitter.d.ts +7 -0
  16. package/dist/internals/EventEmitter.js +17 -0
  17. package/dist/internals/StepQueue.d.ts +16 -0
  18. package/dist/internals/StepQueue.js +32 -0
  19. package/dist/internals/noteHelpers.d.ts +5 -0
  20. package/dist/internals/noteHelpers.js +24 -0
  21. package/dist/midi/midiInstruments.d.ts +1 -0
  22. package/dist/midi/midiInstruments.js +130 -0
  23. package/dist/players/InstrumentPlayer.d.ts +15 -0
  24. package/dist/players/InstrumentPlayer.js +1 -0
  25. package/dist/players/NotePlaybackOptions.d.ts +13 -0
  26. package/dist/players/NotePlaybackOptions.js +6 -0
  27. package/dist/players/SoundfontPlayer.d.ts +17 -0
  28. package/dist/players/SoundfontPlayer.js +54 -0
  29. package/dist/players/musyngkiteInstruments.d.ts +2 -0
  30. package/dist/players/musyngkiteInstruments.js +130 -0
  31. package/eslint.config.js +12 -0
  32. package/package.json +66 -0
  33. package/rollup.umd.config.js +23 -0
  34. package/src/PlaybackEngine.test.ts +99 -0
  35. package/src/PlaybackEngine.ts +294 -0
  36. package/src/PlaybackScheduler.ts +146 -0
  37. package/src/index.ts +3 -0
  38. package/src/internals/EventEmitter.test.ts +57 -0
  39. package/src/internals/EventEmitter.ts +19 -0
  40. package/src/internals/StepQueue.ts +45 -0
  41. package/src/internals/noteHelpers.ts +26 -0
  42. package/src/midi/midiInstruments.ts +130 -0
  43. package/src/players/InstrumentPlayer.ts +17 -0
  44. package/src/players/NotePlaybackOptions.ts +15 -0
  45. package/src/players/SoundfontPlayer.ts +76 -0
  46. package/src/players/musyngkiteInstruments.ts +130 -0
  47. package/tsconfig.build.json +7 -0
  48. package/tsconfig.json +14 -0
  49. package/umd/OsmdAudioPlayer.min.js +15 -0
  50. package/vitest.config.ts +7 -0
@@ -0,0 +1,294 @@
1
+ import PlaybackScheduler from "./PlaybackScheduler";
2
+ import {
3
+ Cursor,
4
+ OpenSheetMusicDisplay,
5
+ MusicSheet,
6
+ Note,
7
+ Instrument,
8
+ Voice,
9
+ } from "opensheetmusicdisplay";
10
+ import { SoundfontPlayer } from "./players/SoundfontPlayer";
11
+ import { InstrumentPlayer, PlaybackInstrument } from "./players/InstrumentPlayer";
12
+ import { NotePlaybackInstruction } from "./players/NotePlaybackOptions";
13
+ import { getNoteDuration, getNoteVolume, getNoteArticulationStyle } from "./internals/noteHelpers";
14
+ import { EventEmitter } from "./internals/EventEmitter";
15
+ import { AudioContext, IAudioContext } from "standardized-audio-context";
16
+
17
+ interface VoiceWithMidi extends Voice {
18
+ midiInstrumentId: number;
19
+ }
20
+
21
+ export enum PlaybackState {
22
+ INIT = "INIT",
23
+ PLAYING = "PLAYING",
24
+ STOPPED = "STOPPED",
25
+ PAUSED = "PAUSED",
26
+ }
27
+
28
+ export enum PlaybackEvent {
29
+ STATE_CHANGE = "state-change",
30
+ ITERATION = "iteration",
31
+ }
32
+
33
+ interface PlaybackSettings {
34
+ bpm: number;
35
+ masterVolume: number;
36
+ }
37
+
38
+ export default class PlaybackEngine {
39
+ private ac: IAudioContext;
40
+ private defaultBpm: number = 100;
41
+ private cursor: Cursor;
42
+ private sheet: MusicSheet;
43
+ private scheduler: PlaybackScheduler;
44
+ private instrumentPlayer: InstrumentPlayer;
45
+ private events: EventEmitter<PlaybackEvent>;
46
+
47
+ private iterationSteps: number;
48
+ private currentIterationStep: number;
49
+
50
+ private timeoutHandles: number[];
51
+
52
+ public playbackSettings: PlaybackSettings;
53
+ public state: PlaybackState;
54
+ public availableInstruments: PlaybackInstrument[];
55
+ public scoreInstruments: Instrument[] = [];
56
+ public ready: boolean = false;
57
+
58
+ constructor(
59
+ context: IAudioContext = new AudioContext(),
60
+ instrumentPlayer: InstrumentPlayer = new SoundfontPlayer(),
61
+ ) {
62
+ this.ac = context;
63
+ this.ac.suspend();
64
+
65
+ this.instrumentPlayer = instrumentPlayer;
66
+ this.instrumentPlayer.init(this.ac);
67
+
68
+ this.availableInstruments = this.instrumentPlayer.instruments;
69
+
70
+ this.events = new EventEmitter();
71
+
72
+ this.cursor = null;
73
+ this.sheet = null;
74
+
75
+ this.scheduler = null;
76
+
77
+ this.iterationSteps = 0;
78
+ this.currentIterationStep = 0;
79
+
80
+ this.timeoutHandles = [];
81
+
82
+ this.playbackSettings = {
83
+ bpm: this.defaultBpm,
84
+ masterVolume: 1,
85
+ };
86
+
87
+ this.setState(PlaybackState.INIT);
88
+ }
89
+
90
+ get wholeNoteLength(): number {
91
+ return Math.round((60 / this.playbackSettings.bpm) * 4000);
92
+ }
93
+
94
+ public getPlaybackInstrument(voiceId: number): PlaybackInstrument {
95
+ if (!this.sheet) return null;
96
+ const voice = this.sheet.Instruments.flatMap((i) => i.Voices).find(
97
+ (v) => v.VoiceId === voiceId,
98
+ );
99
+ return this.availableInstruments.find((i) => i.midiId === (voice as VoiceWithMidi).midiInstrumentId);
100
+ }
101
+
102
+ public async setInstrument(voice: Voice, midiInstrumentId: number): Promise<void> {
103
+ await this.instrumentPlayer.load(midiInstrumentId);
104
+ (voice as VoiceWithMidi).midiInstrumentId = midiInstrumentId;
105
+ }
106
+
107
+ async loadScore(osmd: OpenSheetMusicDisplay): Promise<void> {
108
+ this.ready = false;
109
+ this.sheet = osmd.Sheet;
110
+ this.scoreInstruments = this.sheet.Instruments;
111
+ this.cursor = osmd.cursor;
112
+ if (this.sheet.HasBPMInfo) {
113
+ this.setBpm(this.sheet.DefaultStartTempoInBpm);
114
+ }
115
+
116
+ await this.loadInstruments();
117
+ this.initInstruments();
118
+
119
+ this.scheduler = new PlaybackScheduler(this.wholeNoteLength, this.ac, (delay, notes) =>
120
+ this.notePlaybackCallback(delay, notes),
121
+ );
122
+
123
+ this.countAndSetIterationSteps();
124
+ this.ready = true;
125
+ this.setState(PlaybackState.STOPPED);
126
+ }
127
+
128
+ private initInstruments() {
129
+ for (const i of this.sheet.Instruments) {
130
+ for (const v of i.Voices) {
131
+ (v as VoiceWithMidi).midiInstrumentId = i.MidiInstrumentId;
132
+ }
133
+ }
134
+ }
135
+
136
+ private async loadInstruments() {
137
+ const playerPromises: Promise<void>[] = [];
138
+ for (const i of this.sheet.Instruments) {
139
+ const pbInstrument = this.availableInstruments.find(
140
+ (pbi) => pbi.midiId === i.MidiInstrumentId,
141
+ );
142
+ if (pbInstrument == null) {
143
+ this.fallbackToPiano(i);
144
+ }
145
+ playerPromises.push(this.instrumentPlayer.load(i.MidiInstrumentId));
146
+ }
147
+ await Promise.all(playerPromises);
148
+ }
149
+
150
+ private fallbackToPiano(i: Instrument) {
151
+ console.warn(
152
+ `Can't find playback instrument for midiInstrumentId ${i.MidiInstrumentId}. Falling back to piano`,
153
+ );
154
+ i.MidiInstrumentId = 0;
155
+
156
+ if (this.availableInstruments.find((i) => i.midiId === 0) == null) {
157
+ throw new Error("Piano fallback failed, grand piano not supported");
158
+ }
159
+ }
160
+
161
+ async play() {
162
+ await this.ac.resume();
163
+
164
+ if (this.state === PlaybackState.INIT || this.state === PlaybackState.STOPPED) {
165
+ this.cursor.show();
166
+ }
167
+
168
+ this.setState(PlaybackState.PLAYING);
169
+ this.scheduler.start();
170
+ }
171
+
172
+ async stop() {
173
+ this.setState(PlaybackState.STOPPED);
174
+ this.stopPlayers();
175
+ this.clearTimeouts();
176
+ this.scheduler.reset();
177
+ this.cursor.reset();
178
+ this.currentIterationStep = 0;
179
+ this.cursor.hide();
180
+ }
181
+
182
+ pause() {
183
+ this.setState(PlaybackState.PAUSED);
184
+ this.ac.suspend();
185
+ this.stopPlayers();
186
+ this.scheduler.setIterationStep(this.currentIterationStep);
187
+ this.scheduler.pause();
188
+ this.clearTimeouts();
189
+ }
190
+
191
+ jumpToStep(step: number) {
192
+ this.pause();
193
+ if (this.currentIterationStep > step) {
194
+ this.cursor.reset();
195
+ this.currentIterationStep = 0;
196
+ }
197
+ while (this.currentIterationStep < step) {
198
+ this.cursor.next();
199
+ ++this.currentIterationStep;
200
+ }
201
+ let schedulerStep = this.currentIterationStep;
202
+ if (this.currentIterationStep > 0 && this.currentIterationStep < this.iterationSteps)
203
+ ++schedulerStep;
204
+ this.scheduler.setIterationStep(schedulerStep);
205
+ }
206
+
207
+ setBpm(bpm: number) {
208
+ this.playbackSettings.bpm = bpm;
209
+ if (this.scheduler) this.scheduler.wholeNoteLength = this.wholeNoteLength;
210
+ }
211
+
212
+ public on(event: PlaybackEvent, cb: (...args: unknown[]) => void) {
213
+ this.events.on(event, cb);
214
+ }
215
+
216
+ private countAndSetIterationSteps() {
217
+ this.cursor.reset();
218
+ let steps = 0;
219
+ while (!this.cursor.Iterator.EndReached) {
220
+ if (this.cursor.Iterator.CurrentVoiceEntries) {
221
+ this.scheduler.loadNotes(this.cursor.Iterator.CurrentVoiceEntries);
222
+ }
223
+ this.cursor.next();
224
+ ++steps;
225
+ }
226
+ this.iterationSteps = steps;
227
+ this.cursor.reset();
228
+ }
229
+
230
+ private notePlaybackCallback(audioDelay: number, notes: Note[]) {
231
+ if (this.state !== PlaybackState.PLAYING) return;
232
+ const scheduledNotes: Map<number, NotePlaybackInstruction[]> = new Map();
233
+
234
+ for (const note of notes) {
235
+ if (note.isRest()) {
236
+ continue;
237
+ }
238
+ const noteDuration = getNoteDuration(note, this.wholeNoteLength);
239
+ if (noteDuration === 0) continue;
240
+ const noteVolume = getNoteVolume(note);
241
+ const noteArticulation = getNoteArticulationStyle(note);
242
+
243
+ const midiPlaybackInstrument = (note.ParentVoiceEntry.ParentVoice as VoiceWithMidi).midiInstrumentId;
244
+ const fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments[0].fixedKey || 0;
245
+
246
+ if (!scheduledNotes.has(midiPlaybackInstrument)) {
247
+ scheduledNotes.set(midiPlaybackInstrument, []);
248
+ }
249
+
250
+ scheduledNotes.get(midiPlaybackInstrument).push({
251
+ note: note.halfTone - fixedKey * 12,
252
+ duration: noteDuration / 1000,
253
+ gain: noteVolume,
254
+ articulation: noteArticulation,
255
+ });
256
+ }
257
+
258
+ for (const [midiId, notes] of scheduledNotes) {
259
+ this.instrumentPlayer.schedule(midiId, this.ac.currentTime + audioDelay, notes);
260
+ }
261
+
262
+ this.timeoutHandles.push(
263
+ window.setTimeout(() => this.iterationCallback(), Math.max(0, audioDelay * 1000 - 35)), // Subtracting 35 milliseconds to compensate for update delay
264
+ window.setTimeout(() => this.events.emit(PlaybackEvent.ITERATION, notes), audioDelay * 1000),
265
+ );
266
+ }
267
+
268
+ private setState(state: PlaybackState) {
269
+ this.state = state;
270
+ this.events.emit(PlaybackEvent.STATE_CHANGE, state);
271
+ }
272
+
273
+ private stopPlayers() {
274
+ for (const i of this.sheet.Instruments) {
275
+ for (const v of i.Voices) {
276
+ this.instrumentPlayer.stop((v as VoiceWithMidi).midiInstrumentId);
277
+ }
278
+ }
279
+ }
280
+
281
+ // Used to avoid duplicate cursor movements after a rapid pause/resume action
282
+ private clearTimeouts() {
283
+ for (const h of this.timeoutHandles) {
284
+ clearTimeout(h);
285
+ }
286
+ this.timeoutHandles = [];
287
+ }
288
+
289
+ private iterationCallback() {
290
+ if (this.state !== PlaybackState.PLAYING) return;
291
+ if (this.currentIterationStep > 0) this.cursor.next();
292
+ ++this.currentIterationStep;
293
+ }
294
+ }
@@ -0,0 +1,146 @@
1
+ import StepQueue from "./internals/StepQueue";
2
+ import { Note, VoiceEntry } from "opensheetmusicdisplay";
3
+ import { IAudioContext } from "standardized-audio-context";
4
+
5
+ type NoteSchedulingCallback = (delay: number, notes: Note[]) => void;
6
+
7
+ export default class PlaybackScheduler {
8
+ public wholeNoteLength: number;
9
+
10
+ private stepQueue = new StepQueue();
11
+ private stepQueueIndex = 0;
12
+ private scheduledTicks = new Set<number>();
13
+
14
+ private currentTick = 0;
15
+ private currentTickTimestamp = 0;
16
+
17
+ private audioContext: IAudioContext;
18
+ private audioContextStartTime: number = 0;
19
+
20
+ private schedulerIntervalHandle: number = null;
21
+ private scheduleInterval: number = 200; // Milliseconds
22
+ private schedulePeriod: number = 500;
23
+ private tickDenominator: number = 1024;
24
+
25
+ private lastTickOffset: number = 300; // Hack to get the initial notes play better
26
+ private playing: boolean = false;
27
+
28
+ private noteSchedulingCallback: NoteSchedulingCallback;
29
+
30
+ constructor(
31
+ wholeNoteLength: number,
32
+ audioContext: IAudioContext,
33
+ noteSchedulingCallback: NoteSchedulingCallback,
34
+ ) {
35
+ this.noteSchedulingCallback = noteSchedulingCallback;
36
+ this.wholeNoteLength = wholeNoteLength;
37
+ this.audioContext = audioContext;
38
+ }
39
+
40
+ get schedulePeriodTicks() {
41
+ return this.schedulePeriod / this.tickDuration;
42
+ }
43
+
44
+ get audioContextTime() {
45
+ if (!this.audioContext) return 0;
46
+ return (this.audioContext.currentTime - this.audioContextStartTime) * 1000;
47
+ }
48
+
49
+ get tickDuration() {
50
+ return this.wholeNoteLength / this.tickDenominator;
51
+ }
52
+
53
+ private get calculatedTick() {
54
+ return (
55
+ this.currentTick +
56
+ Math.round((this.audioContextTime - this.currentTickTimestamp) / this.tickDuration)
57
+ );
58
+ }
59
+
60
+ start() {
61
+ this.playing = true;
62
+ this.stepQueue.sort();
63
+ this.audioContextStartTime = this.audioContext.currentTime;
64
+ this.currentTickTimestamp = this.audioContextTime;
65
+ if (!this.schedulerIntervalHandle) {
66
+ this.schedulerIntervalHandle = window.setInterval(
67
+ () => this.scheduleIterationStep(),
68
+ this.scheduleInterval,
69
+ );
70
+ }
71
+ }
72
+
73
+ setIterationStep(step: number) {
74
+ step = Math.min(this.stepQueue.steps.length - 1, step);
75
+ this.stepQueueIndex = step;
76
+ this.currentTick = this.stepQueue.steps[this.stepQueueIndex].tick;
77
+ }
78
+
79
+ pause() {
80
+ this.playing = false;
81
+ }
82
+
83
+ resume() {
84
+ this.playing = true;
85
+ this.currentTickTimestamp = this.audioContextTime;
86
+ }
87
+
88
+ reset() {
89
+ this.playing = false;
90
+ this.currentTick = 0;
91
+ this.currentTickTimestamp = 0;
92
+ this.stepQueueIndex = 0;
93
+ clearInterval(this.scheduleInterval);
94
+ this.schedulerIntervalHandle = null;
95
+ }
96
+
97
+ loadNotes(currentVoiceEntries: VoiceEntry[]) {
98
+ let thisTick = this.lastTickOffset;
99
+ if (this.stepQueue.steps.length > 0) {
100
+ thisTick = this.stepQueue.getFirstEmptyTick();
101
+ }
102
+
103
+ for (const entry of currentVoiceEntries) {
104
+ if (!entry.IsGrace) {
105
+ for (const note of entry.Notes) {
106
+ this.stepQueue.addNote(thisTick, note);
107
+ this.stepQueue.createStep(thisTick + note.Length.RealValue * this.tickDenominator);
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ private scheduleIterationStep() {
114
+ if (!this.playing) return;
115
+ this.currentTick = this.calculatedTick;
116
+ this.currentTickTimestamp = this.audioContextTime;
117
+
118
+ let nextTick = this.stepQueue.steps[this.stepQueueIndex]?.tick;
119
+ while (this.nextTickAvailableAndWithinSchedulePeriod(nextTick)) {
120
+ const step = this.stepQueue.steps[this.stepQueueIndex];
121
+
122
+ let timeToTick = (step.tick - this.currentTick) * this.tickDuration;
123
+ if (timeToTick < 0) timeToTick = 0;
124
+
125
+ this.scheduledTicks.add(step.tick);
126
+ this.noteSchedulingCallback(timeToTick / 1000, step.notes);
127
+
128
+ this.stepQueueIndex++;
129
+ nextTick = this.stepQueue.steps[this.stepQueueIndex]?.tick;
130
+ }
131
+
132
+ for (const tick of this.scheduledTicks) {
133
+ if (tick <= this.currentTick) {
134
+ this.scheduledTicks.delete(tick);
135
+ }
136
+ }
137
+ }
138
+
139
+ private nextTickAvailableAndWithinSchedulePeriod(nextTick: number | undefined) {
140
+ return (
141
+ nextTick &&
142
+ this.currentTickTimestamp + (nextTick - this.currentTick) * this.tickDuration <=
143
+ this.currentTickTimestamp + this.schedulePeriod
144
+ );
145
+ }
146
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ import PlaybackEngine from "./PlaybackEngine";
2
+
3
+ export default PlaybackEngine;
@@ -0,0 +1,57 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import { EventEmitter } from "./EventEmitter";
3
+
4
+ describe("EventEmitter", () => {
5
+ test("Single subscriber", () => {
6
+ const emitter = new EventEmitter();
7
+ const cb = vi.fn(() => {});
8
+
9
+ emitter.on("test-event", cb);
10
+ emitter.emit("test-event");
11
+
12
+ expect(cb).toHaveBeenCalledTimes(1);
13
+ });
14
+
15
+ test("Single subscriber, with arguments", () => {
16
+ const emitter = new EventEmitter();
17
+ const cb = vi.fn(() => {});
18
+
19
+ emitter.on("test-event", cb);
20
+ emitter.emit("test-event", 1, 2);
21
+
22
+ expect(cb).toHaveBeenCalledTimes(1);
23
+ expect(cb).toHaveBeenCalledWith(1, 2);
24
+ });
25
+
26
+ test("Multiple subscribers", () => {
27
+ const emitter = new EventEmitter();
28
+ const cb1 = vi.fn(() => {});
29
+ const cb2 = vi.fn(() => {});
30
+
31
+ emitter.on("test-event", cb1);
32
+ emitter.on("test-event", cb2);
33
+
34
+ emitter.emit("test-event");
35
+
36
+ expect(cb1).toHaveBeenCalledTimes(1);
37
+ expect(cb2).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ test("Multiple events", () => {
41
+ const emitter = new EventEmitter();
42
+ const cb1 = vi.fn(() => {});
43
+ const cb2 = vi.fn(() => {});
44
+ const cb3 = vi.fn(() => {});
45
+
46
+ emitter.on("event1", cb1);
47
+ emitter.on("event2", cb2);
48
+ emitter.on("event3", cb3);
49
+
50
+ emitter.emit("event1");
51
+ emitter.emit("event2");
52
+
53
+ expect(cb1).toHaveBeenCalledTimes(1);
54
+ expect(cb2).toHaveBeenCalledTimes(1);
55
+ expect(cb3).toHaveBeenCalledTimes(0);
56
+ });
57
+ });
@@ -0,0 +1,19 @@
1
+ type Callback = (...args: unknown[]) => void;
2
+
3
+ export class EventEmitter<T> {
4
+ private subscribers: Map<T, Callback[]> = new Map();
5
+
6
+ public on(event: T, callback: Callback) {
7
+ if (!this.subscribers.get(event)) {
8
+ this.subscribers.set(event, []);
9
+ }
10
+ this.subscribers.get(event)!.push(callback);
11
+ }
12
+
13
+ public emit(event: T, ...args: unknown[]) {
14
+ const subscribers = this.subscribers.get(event) || [];
15
+ for (const sub of subscribers) {
16
+ sub(...args);
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,45 @@
1
+ import { Note } from "opensheetmusicdisplay";
2
+
3
+ type ScheduledNotes = {
4
+ tick: number;
5
+ notes: Note[];
6
+ };
7
+
8
+ export default class StepQueue {
9
+ steps: ScheduledNotes[] = [];
10
+
11
+ constructor() {}
12
+
13
+ [Symbol.iterator]() {
14
+ return this.steps.values();
15
+ }
16
+
17
+ createStep(tick: number): ScheduledNotes {
18
+ let step = this.steps.find((s) => s.tick === tick);
19
+ if (!step) {
20
+ step = { tick, notes: [] };
21
+ this.steps.push(step);
22
+ }
23
+
24
+ return step;
25
+ }
26
+
27
+ addNote(tick: number, note: Note): void {
28
+ const step = this.steps.find((s) => s.tick === tick) ?? this.createStep(tick);
29
+ step.notes.push(note);
30
+ }
31
+
32
+ delete(value: ScheduledNotes): void {
33
+ const index = this.steps.findIndex((v) => v.tick === value.tick);
34
+ if (index != null) this.steps.splice(index, 1);
35
+ }
36
+
37
+ sort(): StepQueue {
38
+ this.steps.sort((a, b) => a.tick - b.tick);
39
+ return this;
40
+ }
41
+
42
+ getFirstEmptyTick(): number {
43
+ return this.sort().steps.filter((s) => !s.notes.length)[0].tick;
44
+ }
45
+ }
@@ -0,0 +1,26 @@
1
+ import { Note } from "opensheetmusicdisplay";
2
+ import { ArticulationStyle } from "../players/NotePlaybackOptions";
3
+
4
+ export function getNoteArticulationStyle(note: Note): ArticulationStyle {
5
+ if (note.ParentVoiceEntry.isStaccato()) {
6
+ return ArticulationStyle.Staccato;
7
+ } else {
8
+ return ArticulationStyle.None;
9
+ }
10
+ }
11
+
12
+ export function getNoteDuration(note: Note, wholeNoteLength) {
13
+ let duration = note.Length.RealValue * wholeNoteLength;
14
+ if (note.NoteTie) {
15
+ if (Object.is(note.NoteTie.StartNote, note) && note.NoteTie.Notes[1]) {
16
+ duration += note.NoteTie.Notes[1].Length.RealValue * wholeNoteLength;
17
+ } else {
18
+ duration = 0;
19
+ }
20
+ }
21
+ return duration;
22
+ }
23
+
24
+ export function getNoteVolume(note: Note) {
25
+ return note.ParentVoiceEntry.ParentVoice.Volume;
26
+ }