@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,17 @@
1
+ export class EventEmitter {
2
+ constructor() {
3
+ this.subscribers = new Map();
4
+ }
5
+ on(event, callback) {
6
+ if (!this.subscribers.get(event)) {
7
+ this.subscribers.set(event, []);
8
+ }
9
+ this.subscribers.get(event).push(callback);
10
+ }
11
+ emit(event, ...args) {
12
+ const subscribers = this.subscribers.get(event) || [];
13
+ for (const sub of subscribers) {
14
+ sub(...args);
15
+ }
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ import { Note } from "opensheetmusicdisplay";
2
+ type ScheduledNotes = {
3
+ tick: number;
4
+ notes: Note[];
5
+ };
6
+ export default class StepQueue {
7
+ steps: ScheduledNotes[];
8
+ constructor();
9
+ [Symbol.iterator](): ArrayIterator<ScheduledNotes>;
10
+ createStep(tick: number): ScheduledNotes;
11
+ addNote(tick: number, note: Note): void;
12
+ delete(value: ScheduledNotes): void;
13
+ sort(): StepQueue;
14
+ getFirstEmptyTick(): number;
15
+ }
16
+ export {};
@@ -0,0 +1,32 @@
1
+ export default class StepQueue {
2
+ constructor() {
3
+ this.steps = [];
4
+ }
5
+ [Symbol.iterator]() {
6
+ return this.steps.values();
7
+ }
8
+ createStep(tick) {
9
+ let step = this.steps.find((s) => s.tick === tick);
10
+ if (!step) {
11
+ step = { tick, notes: [] };
12
+ this.steps.push(step);
13
+ }
14
+ return step;
15
+ }
16
+ addNote(tick, note) {
17
+ const step = this.steps.find((s) => s.tick === tick) ?? this.createStep(tick);
18
+ step.notes.push(note);
19
+ }
20
+ delete(value) {
21
+ const index = this.steps.findIndex((v) => v.tick === value.tick);
22
+ if (index != null)
23
+ this.steps.splice(index, 1);
24
+ }
25
+ sort() {
26
+ this.steps.sort((a, b) => a.tick - b.tick);
27
+ return this;
28
+ }
29
+ getFirstEmptyTick() {
30
+ return this.sort().steps.filter((s) => !s.notes.length)[0].tick;
31
+ }
32
+ }
@@ -0,0 +1,5 @@
1
+ import { Note } from "opensheetmusicdisplay";
2
+ import { ArticulationStyle } from "../players/NotePlaybackOptions";
3
+ export declare function getNoteArticulationStyle(note: Note): ArticulationStyle;
4
+ export declare function getNoteDuration(note: Note, wholeNoteLength: any): number;
5
+ export declare function getNoteVolume(note: Note): number;
@@ -0,0 +1,24 @@
1
+ import { ArticulationStyle } from "../players/NotePlaybackOptions";
2
+ export function getNoteArticulationStyle(note) {
3
+ if (note.ParentVoiceEntry.isStaccato()) {
4
+ return ArticulationStyle.Staccato;
5
+ }
6
+ else {
7
+ return ArticulationStyle.None;
8
+ }
9
+ }
10
+ export function getNoteDuration(note, wholeNoteLength) {
11
+ let duration = note.Length.RealValue * wholeNoteLength;
12
+ if (note.NoteTie) {
13
+ if (Object.is(note.NoteTie.StartNote, note) && note.NoteTie.Notes[1]) {
14
+ duration += note.NoteTie.Notes[1].Length.RealValue * wholeNoteLength;
15
+ }
16
+ else {
17
+ duration = 0;
18
+ }
19
+ }
20
+ return duration;
21
+ }
22
+ export function getNoteVolume(note) {
23
+ return note.ParentVoiceEntry.ParentVoice.Volume;
24
+ }
@@ -0,0 +1 @@
1
+ export declare const midiInstruments: [number, string][];
@@ -0,0 +1,130 @@
1
+ export const midiInstruments = [
2
+ [0, "Acoustic Grand Piano"],
3
+ [1, "Bright Acoustic Piano"],
4
+ [2, "Electric Grand Piano"],
5
+ [3, "Honky-tonk Piano"],
6
+ [4, "Electric Piano 1"],
7
+ [5, "Electric Piano 2"],
8
+ [6, "Harpsichord"],
9
+ [7, "Clavi"],
10
+ [8, "Celesta"],
11
+ [9, "Glockenspiel"],
12
+ [10, "Music Box"],
13
+ [11, "Vibraphone"],
14
+ [12, "Marimba"],
15
+ [13, "Xylophone"],
16
+ [14, "Tubular Bells"],
17
+ [15, "Dulcimer"],
18
+ [16, "Drawbar Organ"],
19
+ [17, "Percussive Organ"],
20
+ [18, "Rock Organ"],
21
+ [19, "Church Organ"],
22
+ [20, "Reed Organ"],
23
+ [21, "Accordion"],
24
+ [22, "Harmonica"],
25
+ [23, "Tango Accordion"],
26
+ [24, "Acoustic Guitar (nylon)"],
27
+ [25, "Acoustic Guitar (steel)"],
28
+ [26, "Electric Guitar (jazz)"],
29
+ [27, "Electric Guitar (clean)"],
30
+ [28, "Electric Guitar (muted)"],
31
+ [29, "Overdriven Guitar"],
32
+ [30, "Distortion Guitar"],
33
+ [31, "Guitar harmonics"],
34
+ [32, "Acoustic Bass"],
35
+ [33, "Electric Bass (finger)"],
36
+ [34, "Electric Bass (pick)"],
37
+ [35, "Fretless Bass"],
38
+ [36, "Slap Bass 1"],
39
+ [37, "Slap Bass 2"],
40
+ [38, "Synth Bass 1"],
41
+ [39, "Synth Bass 2"],
42
+ [40, "Violin"],
43
+ [41, "Viola"],
44
+ [42, "Cello"],
45
+ [43, "Contrabass"],
46
+ [44, "Tremolo Strings"],
47
+ [45, "Pizzicato Strings"],
48
+ [46, "Orchestral Harp"],
49
+ [47, "Timpani"],
50
+ [48, "String Ensemble 1"],
51
+ [49, "String Ensemble 2"],
52
+ [50, "SynthStrings 1"],
53
+ [51, "SynthStrings 2"],
54
+ [52, "Choir Aahs"],
55
+ [53, "Voice Oohs"],
56
+ [54, "Synth Choir"],
57
+ [55, "Orchestra Hit"],
58
+ [56, "Trumpet"],
59
+ [57, "Trombone"],
60
+ [58, "Tuba"],
61
+ [59, "Muted Trumpet"],
62
+ [60, "French Horn"],
63
+ [61, "Brass Section"],
64
+ [62, "SynthBrass 1"],
65
+ [63, "SynthBrass 2"],
66
+ [64, "Soprano Sax"],
67
+ [65, "Alto Sax"],
68
+ [66, "Tenor Sax"],
69
+ [67, "Baritone Sax"],
70
+ [68, "Oboe"],
71
+ [69, "English Horn"],
72
+ [70, "Bassoon"],
73
+ [71, "Clarinet"],
74
+ [72, "Piccolo"],
75
+ [73, "Flute"],
76
+ [74, "Recorder"],
77
+ [75, "Pan Flute"],
78
+ [76, "Blown Bottle"],
79
+ [77, "Shakuhachi"],
80
+ [78, "Whistle"],
81
+ [79, "Ocarina"],
82
+ [80, "Lead 1 (square)"],
83
+ [81, "Lead 2 (sawtooth)"],
84
+ [82, "Lead 3 (calliope)"],
85
+ [83, "Lead 4 (chiff)"],
86
+ [84, "Lead 5 (charang)"],
87
+ [85, "Lead 6 (voice)"],
88
+ [86, "Lead 7 (fifths)"],
89
+ [87, "Lead 8 (bass + lead)"],
90
+ [88, "Pad 1 (new age)"],
91
+ [89, "Pad 2 (warm)"],
92
+ [90, "Pad 3 (polysynth)"],
93
+ [91, "Pad 4 (choir)"],
94
+ [92, "Pad 5 (bowed)"],
95
+ [93, "Pad 6 (metallic)"],
96
+ [94, "Pad 7 (halo)"],
97
+ [95, "Pad 8 (sweep)"],
98
+ [96, "FX 1 (rain)"],
99
+ [97, "FX 2 (soundtrack)"],
100
+ [98, "FX 3 (crystal)"],
101
+ [99, "FX 4 (atmosphere)"],
102
+ [100, "FX 5 (brightness)"],
103
+ [101, "FX 6 (goblins)"],
104
+ [102, "FX 7 (echoes)"],
105
+ [103, "FX 8 (sci-fi)"],
106
+ [104, "Sitar"],
107
+ [105, "Banjo"],
108
+ [106, "Shamisen"],
109
+ [107, "Koto"],
110
+ [108, "Kalimba"],
111
+ [109, "Bag pipe"],
112
+ [110, "Fiddle"],
113
+ [111, "Shanai"],
114
+ [112, "Tinkle Bell"],
115
+ [113, "Agogo"],
116
+ [114, "Steel Drums"],
117
+ [115, "Woodblock"],
118
+ [116, "Taiko Drum"],
119
+ [117, "Melodic Tom"],
120
+ [118, "Synth Drum"],
121
+ [119, "Reverse Cymbal"],
122
+ [120, "Guitar Fret Noise"],
123
+ [121, "Breath Noise"],
124
+ [122, "Seashore"],
125
+ [123, "Bird Tweet"],
126
+ [124, "Telephone Ring"],
127
+ [125, "Helicopter"],
128
+ [126, "Applause"],
129
+ [127, "Gunshot"],
130
+ ];
@@ -0,0 +1,15 @@
1
+ import { NotePlaybackInstruction } from "./NotePlaybackOptions";
2
+ import { IAudioContext } from "standardized-audio-context";
3
+ export interface PlaybackInstrument {
4
+ midiId: number;
5
+ name: string;
6
+ loaded: boolean;
7
+ }
8
+ export interface InstrumentPlayer {
9
+ instruments: PlaybackInstrument[];
10
+ init: (audioContext: IAudioContext) => void;
11
+ load: (midiId: number) => Promise<void>;
12
+ schedule: (midiId: number, time: number, notes: NotePlaybackInstruction[]) => void;
13
+ play: (midiId: number, options: NotePlaybackInstruction) => void;
14
+ stop: (midiId: number) => void;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ export declare enum ArticulationStyle {
2
+ None = 0,
3
+ Staccato = 1,
4
+ Legato = 2
5
+ }
6
+ export interface NotePlaybackStyle {
7
+ articulation: ArticulationStyle;
8
+ }
9
+ export interface NotePlaybackInstruction extends NotePlaybackStyle {
10
+ note: number;
11
+ gain: number;
12
+ duration: number;
13
+ }
@@ -0,0 +1,6 @@
1
+ export var ArticulationStyle;
2
+ (function (ArticulationStyle) {
3
+ ArticulationStyle[ArticulationStyle["None"] = 0] = "None";
4
+ ArticulationStyle[ArticulationStyle["Staccato"] = 1] = "Staccato";
5
+ ArticulationStyle[ArticulationStyle["Legato"] = 2] = "Legato";
6
+ })(ArticulationStyle || (ArticulationStyle = {}));
@@ -0,0 +1,17 @@
1
+ import { InstrumentPlayer, PlaybackInstrument } from "./InstrumentPlayer";
2
+ import { NotePlaybackStyle, NotePlaybackInstruction } from "./NotePlaybackOptions";
3
+ import { IAudioContext } from "standardized-audio-context";
4
+ export declare class SoundfontPlayer implements InstrumentPlayer {
5
+ instruments: PlaybackInstrument[];
6
+ private players;
7
+ private audioContext;
8
+ constructor();
9
+ init(audioContext: IAudioContext): void;
10
+ load(midiId: number): Promise<void>;
11
+ play: (midiId: string | number, options: NotePlaybackStyle) => void;
12
+ stop(midiId: number): void;
13
+ schedule(midiId: number, time: number, notes: NotePlaybackInstruction[]): void;
14
+ private applyDynamics;
15
+ private verifyPlayerLoaded;
16
+ private getSoundfontInstrumentName;
17
+ }
@@ -0,0 +1,54 @@
1
+ import { ArticulationStyle, } from "./NotePlaybackOptions";
2
+ import { midiInstruments } from "../midi/midiInstruments";
3
+ import supportedSoundfontInstruments from "./musyngkiteInstruments";
4
+ import * as Soundfont from "soundfont-player";
5
+ export class SoundfontPlayer {
6
+ constructor() {
7
+ this.players = new Map();
8
+ this.instruments = midiInstruments
9
+ .filter((i) => supportedSoundfontInstruments.includes(this.getSoundfontInstrumentName(i[1])))
10
+ .map((i) => ({
11
+ midiId: i[0],
12
+ name: i[1],
13
+ loaded: false,
14
+ }));
15
+ }
16
+ init(audioContext) {
17
+ this.audioContext = audioContext;
18
+ }
19
+ async load(midiId) {
20
+ const instrument = this.instruments.find((i) => i.midiId === midiId);
21
+ if (!instrument) {
22
+ throw new Error("SoundfontPlayer does not support midi instrument ID " + midiId);
23
+ }
24
+ if (this.players.has(midiId))
25
+ return;
26
+ const player = await Soundfont.instrument(this.audioContext, this.getSoundfontInstrumentName(instrument.name));
27
+ this.players.set(midiId, player);
28
+ }
29
+ stop(midiId) {
30
+ if (!this.players.has(midiId))
31
+ return;
32
+ this.players.get(midiId).stop();
33
+ }
34
+ schedule(midiId, time, notes) {
35
+ this.verifyPlayerLoaded(midiId);
36
+ this.applyDynamics(notes);
37
+ this.players.get(midiId).schedule(time, notes);
38
+ }
39
+ applyDynamics(notes) {
40
+ for (const note of notes) {
41
+ if (note.articulation === ArticulationStyle.Staccato) {
42
+ note.gain = Math.max(note.gain + 0.3, note.gain * 1.3);
43
+ note.duration = Math.min(note.duration * 0.4, 0.4);
44
+ }
45
+ }
46
+ }
47
+ verifyPlayerLoaded(midiId) {
48
+ if (!this.players.has(midiId))
49
+ throw new Error("No soundfont player loaded for midi instrument " + midiId);
50
+ }
51
+ getSoundfontInstrumentName(midiName) {
52
+ return midiName.toLowerCase().replace(/\s+/g, "_");
53
+ }
54
+ }
@@ -0,0 +1,2 @@
1
+ declare const _default: string[];
2
+ export default _default;
@@ -0,0 +1,130 @@
1
+ export default [
2
+ "accordion",
3
+ "acoustic_bass",
4
+ "acoustic_grand_piano",
5
+ "acoustic_guitar_nylon",
6
+ "acoustic_guitar_steel",
7
+ "agogo",
8
+ "alto_sax",
9
+ "applause",
10
+ "bagpipe",
11
+ "banjo",
12
+ "baritone_sax",
13
+ "bassoon",
14
+ "bird_tweet",
15
+ "blown_bottle",
16
+ "brass_section",
17
+ "breath_noise",
18
+ "bright_acoustic_piano",
19
+ "celesta",
20
+ "cello",
21
+ "choir_aahs",
22
+ "church_organ",
23
+ "clarinet",
24
+ "clavinet",
25
+ "contrabass",
26
+ "distortion_guitar",
27
+ "drawbar_organ",
28
+ "dulcimer",
29
+ "electric_bass_finger",
30
+ "electric_bass_pick",
31
+ "electric_grand_piano",
32
+ "electric_guitar_clean",
33
+ "electric_guitar_jazz",
34
+ "electric_guitar_muted",
35
+ "electric_piano_1",
36
+ "electric_piano_2",
37
+ "english_horn",
38
+ "fiddle",
39
+ "flute",
40
+ "french_horn",
41
+ "fretless_bass",
42
+ "fx_1_rain",
43
+ "fx_2_soundtrack",
44
+ "fx_3_crystal",
45
+ "fx_4_atmosphere",
46
+ "fx_5_brightness",
47
+ "fx_6_goblins",
48
+ "fx_7_echoes",
49
+ "fx_8_scifi",
50
+ "glockenspiel",
51
+ "guitar_fret_noise",
52
+ "guitar_harmonics",
53
+ "gunshot",
54
+ "harmonica",
55
+ "harpsichord",
56
+ "helicopter",
57
+ "honkytonk_piano",
58
+ "kalimba",
59
+ "koto",
60
+ "lead_1_square",
61
+ "lead_2_sawtooth",
62
+ "lead_3_calliope",
63
+ "lead_4_chiff",
64
+ "lead_5_charang",
65
+ "lead_6_voice",
66
+ "lead_7_fifths",
67
+ "lead_8_bass__lead",
68
+ "marimba",
69
+ "melodic_tom",
70
+ "music_box",
71
+ "muted_trumpet",
72
+ "oboe",
73
+ "ocarina",
74
+ "orchestra_hit",
75
+ "orchestral_harp",
76
+ "overdriven_guitar",
77
+ "pad_1_new_age",
78
+ "pad_2_warm",
79
+ "pad_3_polysynth",
80
+ "pad_4_choir",
81
+ "pad_5_bowed",
82
+ "pad_6_metallic",
83
+ "pad_7_halo",
84
+ "pad_8_sweep",
85
+ "pan_flute",
86
+ "percussive_organ",
87
+ "piccolo",
88
+ "pizzicato_strings",
89
+ "recorder",
90
+ "reed_organ",
91
+ "reverse_cymbal",
92
+ "rock_organ",
93
+ "seashore",
94
+ "shakuhachi",
95
+ "shamisen",
96
+ "shanai",
97
+ "sitar",
98
+ "slap_bass_1",
99
+ "slap_bass_2",
100
+ "soprano_sax",
101
+ "steel_drums",
102
+ "string_ensemble_1",
103
+ "string_ensemble_2",
104
+ "synth_bass_1",
105
+ "synth_bass_2",
106
+ "synth_brass_1",
107
+ "synth_brass_2",
108
+ "synth_choir",
109
+ "synth_drum",
110
+ "synth_strings_1",
111
+ "synth_strings_2",
112
+ "taiko_drum",
113
+ "tango_accordion",
114
+ "telephone_ring",
115
+ "tenor_sax",
116
+ "timpani",
117
+ "tinkle_bell",
118
+ "tremolo_strings",
119
+ "trombone",
120
+ "trumpet",
121
+ "tuba",
122
+ "tubular_bells",
123
+ "vibraphone",
124
+ "viola",
125
+ "violin",
126
+ "voice_oohs",
127
+ "whistle",
128
+ "woodblock",
129
+ "xylophone",
130
+ ];
@@ -0,0 +1,12 @@
1
+ import eslint from "@eslint/js";
2
+ import tseslint from "typescript-eslint";
3
+ import prettier from "eslint-config-prettier";
4
+
5
+ export default tseslint.config(
6
+ eslint.configs.recommended,
7
+ ...tseslint.configs.recommended,
8
+ prettier,
9
+ {
10
+ ignores: ["dist/", "umd/", "demos/", "rollup.umd.config.js"],
11
+ },
12
+ );
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@modernized/osmd-audio-player",
3
+ "version": "1.0.1",
4
+ "description": "OSMD audio player (fork of osmd-audio-player with modernized build system)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "dev": "tsc -w",
10
+ "test": "vitest run",
11
+ "prepare": "yarn build",
12
+ "build": "tsc -p tsconfig.build.json",
13
+ "build-umd": "rollup -c rollup.umd.config.js",
14
+ "install-demo:vue": "yarn --cwd demos/vue-player-demo install",
15
+ "install-demo:basic": "yarn --cwd demos/basic install",
16
+ "install-demos": "run-p install-demo:*",
17
+ "build-demo:vue": "yarn --cwd demos/vue-player-demo build",
18
+ "build-demo:basic": "yarn --cwd demos/basic build",
19
+ "build-demos": "run-p build-demo:*",
20
+ "build-all": "run-s build build-umd build-demos",
21
+ "netlify-build": "run-s build install-demos build-demos",
22
+ "lint": "eslint src/",
23
+ "format": "prettier --write src/"
24
+ },
25
+ "author": "Jimmy Utterström",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/modernized-js/osmd-audio-player"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "keywords": [
35
+ "MusicXML",
36
+ "Music XML",
37
+ "audio player",
38
+ "MusicXML audio",
39
+ "music notation"
40
+ ],
41
+ "dependencies": {
42
+ "opensheetmusicdisplay": "^1.9.7",
43
+ "soundfont-player": "^0.12.0",
44
+ "standardized-audio-context": "^25.3.77"
45
+ },
46
+ "resolutions": {
47
+ "tar": ">=7.5.7"
48
+ },
49
+ "devDependencies": {
50
+ "@eslint/js": "^10.0.1",
51
+ "@rollup/plugin-commonjs": "^29.0.2",
52
+ "@rollup/plugin-node-resolve": "^16.0.3",
53
+ "@rollup/plugin-terser": "^1.0.0",
54
+ "@rollup/plugin-typescript": "^12.3.0",
55
+ "eslint": "^10.2.0",
56
+ "eslint-config-prettier": "^10.1.8",
57
+ "jsdom": "^29.0.1",
58
+ "npm-run-all": "^4.1.5",
59
+ "prettier": "^3.8.1",
60
+ "rollup": "^4.60.1",
61
+ "tslib": "^2.8.1",
62
+ "typescript": "^5.9.0",
63
+ "typescript-eslint": "^8.58.0",
64
+ "vitest": "^4.1.2"
65
+ }
66
+ }
@@ -0,0 +1,23 @@
1
+ import typescript from "@rollup/plugin-typescript";
2
+ import { nodeResolve } from "@rollup/plugin-node-resolve";
3
+ import commonjs from "@rollup/plugin-commonjs";
4
+ import terser from "@rollup/plugin-terser";
5
+
6
+ export default {
7
+ input: "src/index.ts",
8
+ output: [
9
+ {
10
+ file: "umd/OsmdAudioPlayer.min.js",
11
+ format: "umd",
12
+ name: "OsmdAudioPlayer",
13
+ },
14
+ ],
15
+ plugins: [
16
+ typescript({ declaration: false }),
17
+ nodeResolve(),
18
+ commonjs({
19
+ include: "node_modules/**",
20
+ }),
21
+ terser(),
22
+ ],
23
+ };
@@ -0,0 +1,99 @@
1
+ import { describe, test, expect, vi } from "vitest";
2
+ import PlaybackEngine from ".";
3
+ import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
4
+ import { PlaybackEvent, PlaybackState } from "./PlaybackEngine";
5
+ import { IAudioContext } from "standardized-audio-context";
6
+
7
+ vi.mock("./PlaybackScheduler");
8
+
9
+ describe("PlaybackEngine", () => {
10
+ describe("Events", () => {
11
+ test("Playback state event on loadScore()", async () => {
12
+ const acMock = createMockedAudioContext();
13
+ const osmdMock = createOsmdMock();
14
+ const stateCb = vi.fn();
15
+
16
+ const pbEngine = new PlaybackEngine(acMock);
17
+
18
+ pbEngine.on(PlaybackEvent.STATE_CHANGE, stateCb);
19
+ await pbEngine.loadScore(osmdMock);
20
+
21
+ expect(stateCb).toHaveBeenCalledTimes(1);
22
+ expect(stateCb).toHaveBeenCalledWith(PlaybackState.STOPPED);
23
+ });
24
+
25
+ test("Playback state event on play()", async () => {
26
+ const acMock = createMockedAudioContext();
27
+ const osmdMock = createOsmdMock();
28
+ const stateCb = vi.fn();
29
+
30
+ const pbEngine = new PlaybackEngine(acMock);
31
+
32
+ await pbEngine.loadScore(osmdMock);
33
+ pbEngine.on(PlaybackEvent.STATE_CHANGE, stateCb);
34
+ await pbEngine.play();
35
+
36
+ expect(stateCb).toHaveBeenCalledTimes(1);
37
+ expect(stateCb).toHaveBeenCalledWith(PlaybackState.PLAYING);
38
+ });
39
+
40
+ test("Playback state event on stop()", async () => {
41
+ const acMock = createMockedAudioContext();
42
+ const osmdMock = createOsmdMock();
43
+ const stateCb = vi.fn();
44
+
45
+ const pbEngine = new PlaybackEngine(acMock);
46
+
47
+ await pbEngine.loadScore(osmdMock);
48
+ await pbEngine.play();
49
+ pbEngine.on(PlaybackEvent.STATE_CHANGE, stateCb);
50
+ await pbEngine.stop();
51
+
52
+ expect(stateCb).toHaveBeenCalledTimes(1);
53
+ expect(stateCb).toHaveBeenCalledWith(PlaybackState.STOPPED);
54
+ });
55
+
56
+ test("Playback state event on pause()", async () => {
57
+ const acMock = createMockedAudioContext();
58
+ const osmdMock = createOsmdMock();
59
+ const stateCb = vi.fn();
60
+
61
+ const pbEngine = new PlaybackEngine(acMock);
62
+
63
+ await pbEngine.loadScore(osmdMock);
64
+ await pbEngine.play();
65
+ pbEngine.on(PlaybackEvent.STATE_CHANGE, stateCb);
66
+ await pbEngine.pause();
67
+
68
+ expect(stateCb).toHaveBeenCalledTimes(1);
69
+ expect(stateCb).toHaveBeenCalledWith(PlaybackState.PAUSED);
70
+ });
71
+ });
72
+ });
73
+
74
+ function createMockedAudioContext(): IAudioContext {
75
+ return {
76
+ currentTime: 0,
77
+ suspend: vi.fn(async () => {}),
78
+ resume: vi.fn(async () => {}),
79
+ } as unknown as IAudioContext;
80
+ }
81
+
82
+ function createOsmdMock(): OpenSheetMusicDisplay {
83
+ return {
84
+ cursor: {
85
+ Iterator: { EndReached: true },
86
+ show: vi.fn(),
87
+ hide: vi.fn(),
88
+ next: vi.fn(),
89
+ reset: vi.fn(),
90
+ VoicesUnderCursor: vi.fn(() => []),
91
+ },
92
+ Sheet: {
93
+ Instruments: [],
94
+ SheetPlaybackSetting: { rhythm: {} },
95
+ HasBPMInfo: false,
96
+ DefaultStartTempoInBpm: 120,
97
+ },
98
+ } as unknown as OpenSheetMusicDisplay;
99
+ }