@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,18 @@
1
+ version: 2.1
2
+ orbs:
3
+ node: circleci/node@1.1.6
4
+ jobs:
5
+ build-and-test:
6
+ executor:
7
+ name: node/default
8
+ steps:
9
+ - checkout
10
+ - node/with-cache:
11
+ steps:
12
+ - run: npm install
13
+ - run: npm test
14
+ - run: npm run build
15
+ workflows:
16
+ build-and-test:
17
+ jobs:
18
+ - build-and-test
package/.gitattributes ADDED
@@ -0,0 +1,12 @@
1
+ # Auto detect text files and ensure LF line endings
2
+ * text=auto eol=lf
3
+
4
+ # Explicitly declare text files
5
+ *.ts text eol=lf
6
+ *.js text eol=lf
7
+ *.json text eol=lf
8
+ *.yml text eol=lf
9
+ *.yaml text eol=lf
10
+ *.md text eol=lf
11
+ *.html text eol=lf
12
+ *.css text eol=lf
@@ -0,0 +1,23 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+
9
+ jobs:
10
+ build-and-test:
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ matrix:
14
+ os: [ubuntu-latest, windows-latest, macos-latest]
15
+ steps:
16
+ - uses: actions/checkout@v4
17
+ - uses: actions/setup-node@v4
18
+ with:
19
+ node-version: 24
20
+ cache: yarn
21
+ - run: yarn install --frozen-lockfile
22
+ - run: yarn test
23
+ - run: yarn build
package/.prettierrc ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "printWidth": 100,
3
+ "trailingComma": "all"
4
+ }
package/.prettierrc.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ printWidth: 120,
3
+ arrowParens: "avoid",
4
+ };
package/CLAUDE.md ADDED
@@ -0,0 +1,96 @@
1
+ # osmd-audio-player
2
+
3
+ OpenSheetMusicDisplay (OSMD) 用の非公式オーディオ再生エンジン。MusicXML楽譜の視覚表示とWeb Audio APIによる音声再生を同期する。
4
+
5
+ - **作者:** Jimmy Utterström
6
+ - **ライセンス:** MIT
7
+ - **バージョン:** 0.7.0(開発終了・メンテナンスモード)
8
+ - **リポジトリ:** https://github.com/modernized-js/osmd-audio-player([jimutt/osmd-audio-player](https://github.com/jimutt/osmd-audio-player) のfork)
9
+
10
+ ## 主な機能
11
+
12
+ - フレームワーク非依存(Vanilla JS / React / Vue で利用可能)
13
+ - マルチ楽器対応(91種類のMIDI楽器、Musyngkiteサウンドフォント)
14
+ - 楽器ごとの音量制御
15
+ - MusicXMLからのテンポ自動検出・BPM変更
16
+ - スタッカートアーティキュレーション / タイ(リガチャー)処理
17
+ - 再生位置ジャンプ(seek)
18
+ - イベント駆動アーキテクチャ(STATE_CHANGE / ITERATION)
19
+
20
+ ## アーキテクチャ
21
+
22
+ ```
23
+ MusicXML → OSMD → PlaybackEngine → PlaybackScheduler → SoundfontPlayer → Web Audio API
24
+ ```
25
+
26
+ ### コアモジュール
27
+
28
+ | ファイル | 役割 |
29
+ |---------|------|
30
+ | `src/PlaybackEngine.ts` | メインオーケストレーター。楽譜読込・状態管理・カーソル同期・イベント発行 |
31
+ | `src/PlaybackScheduler.ts` | 音声タイミング制御。1024tick/全音符の精度、500msルックアヘッドスケジューリング |
32
+ | `src/players/SoundfontPlayer.ts` | soundfont-playerによる楽器音合成 |
33
+ | `src/players/InstrumentPlayer.ts` | 楽器プレイヤーのインターフェース定義 |
34
+ | `src/internals/StepQueue.ts` | ノートスケジューリングキュー |
35
+ | `src/internals/EventEmitter.ts` | 汎用イベントシステム |
36
+ | `src/internals/noteHelpers.ts` | ノートメタデータ抽出ユーティリティ |
37
+ | `src/midi/midiInstruments.ts` | 128種MIDI楽器マッピング |
38
+ | `src/players/musyngkiteInstruments.ts` | サポート楽器フィルタリスト |
39
+
40
+ ### 再生状態遷移
41
+
42
+ ```
43
+ INIT → STOPPED → PLAYING ⇄ PAUSED
44
+
45
+ STOPPED
46
+ ```
47
+
48
+ ## 技術スタック
49
+
50
+ ### ランタイム依存
51
+
52
+ - `opensheetmusicdisplay` ^0.8.4 — MusicXMLパーサ・楽譜レンダラ
53
+ - `soundfont-player` ^0.12.0 — サウンドフォント再生
54
+ - `standardized-audio-context` ^24.1.25 — Web Audio API抽象化
55
+
56
+ ### 開発依存
57
+
58
+ - TypeScript ^3.7.5
59
+ - Jest ^26.0.1 + ts-mockito(テスト)
60
+ - Babel(トランスパイル)
61
+ - Rollup(UMDバンドル)
62
+
63
+ ## ビルド・開発コマンド
64
+
65
+ ```bash
66
+ yarn dev # TypeScript watchモード
67
+ yarn test # Jestテスト実行
68
+ yarn build # dist/ へコンパイル(CommonJS)
69
+ yarn build-umd # umd/OsmdAudioPlayer.min.js 生成
70
+ yarn build-all # 全ビルド(lib + UMD + demos)
71
+ ```
72
+
73
+ ## 出力
74
+
75
+ - **CommonJS:** `dist/index.js` + `dist/index.d.ts`
76
+ - **UMD:** `umd/OsmdAudioPlayer.min.js`(グローバル変数 `OsmdAudioPlayer`)
77
+
78
+ ## デモアプリ
79
+
80
+ | ディレクトリ | フレームワーク | 内容 |
81
+ |------------|-------------|------|
82
+ | `demos/basic/` | Vanilla JS | 最小構成の参照実装 |
83
+ | `demos/vue-player-demo/` | Vue 2 + Vuetify | フル機能UI(テンポ・音量・楽器選択) |
84
+ | `demos/react-demo/` | React | React統合例 |
85
+ | `demos/umd-web/` | Plain HTML | Node.js不要のUMDビルド例 |
86
+
87
+ ## テスト
88
+
89
+ - Jest + babel-jest + ts-mockito
90
+ - テストファイル: `src/internals/EventEmitter.test.ts`, `src/internals/PlaybackEngine.test.ts`
91
+ - カバー範囲: 状態遷移イベント、EventEmitter基本動作
92
+
93
+ ## CI
94
+
95
+ - GitHub Actions(テスト → ビルド、ubuntu/windows/macos)
96
+ - Netlifyデプロイ(デモサイト)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Jimmy Utterström
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,79 @@
1
+ # @modernized/osmd-audio-player
2
+
3
+ > **Fork of [osmd-audio-player](https://github.com/jimutt/osmd-audio-player)** by Jimmy Utterström.
4
+ > Modernized build system, updated dependencies, and stricter TypeScript typing.
5
+
6
+ Unofficial audio playback engine for [OpenSheetMusicDisplay](https://github.com/opensheetmusicdisplay/opensheetmusicdisplay).
7
+
8
+ ## Changes from upstream
9
+
10
+ - Migrated from npm/Babel/Jest to yarn/Vitest
11
+ - Upgraded TypeScript 3.7 to 5.x
12
+ - Upgraded opensheetmusicdisplay 0.8 to 1.9
13
+ - Added ESLint + Prettier
14
+ - Removed all `any` types
15
+ - GitHub Actions CI (ubuntu/windows/macos)
16
+
17
+ ## Install
18
+
19
+ ```
20
+ yarn add @modernized/osmd-audio-player
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```typescript
26
+ import { OpenSheetMusicDisplay } from "opensheetmusicdisplay";
27
+ import AudioPlayer from "@modernized/osmd-audio-player";
28
+
29
+ const osmd = new OpenSheetMusicDisplay(document.getElementById("score"));
30
+ const audioPlayer = new AudioPlayer();
31
+
32
+ await osmd.load(scoreXml);
33
+ await osmd.render();
34
+ await audioPlayer.loadScore(osmd);
35
+
36
+ audioPlayer.play();
37
+ ```
38
+
39
+ ## Features
40
+
41
+ - Framework agnostic (Vanilla JS / React / Vue)
42
+ - Multi-instrument support (91 MIDI instruments via Musyngkite soundfont)
43
+ - Individual volume controls per instrument
44
+ - Automatic tempo detection from score / BPM control
45
+ - Staccato articulation / tie handling
46
+ - Seek (jump to step)
47
+ - Event-driven architecture (STATE_CHANGE / ITERATION)
48
+
49
+ ## API
50
+
51
+ ```typescript
52
+ const audioPlayer = new AudioPlayer();
53
+
54
+ await audioPlayer.loadScore(osmd); // Load score from OSMD instance
55
+ await audioPlayer.play(); // Start playback
56
+ audioPlayer.pause(); // Pause playback
57
+ await audioPlayer.stop(); // Stop and reset to beginning
58
+ audioPlayer.jumpToStep(step); // Seek to specific step
59
+ audioPlayer.setBpm(120); // Set tempo
60
+
61
+ audioPlayer.on("state-change", (state) => { /* INIT | PLAYING | STOPPED | PAUSED */ });
62
+ audioPlayer.on("iteration", (notes) => { /* Called for each note group */ });
63
+ ```
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ yarn install
69
+ yarn dev # TypeScript watch mode
70
+ yarn test # Run tests (Vitest)
71
+ yarn build # Compile to dist/
72
+ yarn build-umd # UMD bundle
73
+ yarn lint # ESLint
74
+ yarn format # Prettier
75
+ ```
76
+
77
+ ## License
78
+
79
+ MIT - Original work by [Jimmy Utterström](https://github.com/jimutt/osmd-audio-player)
@@ -0,0 +1,55 @@
1
+ import { OpenSheetMusicDisplay, Instrument, Voice } from "opensheetmusicdisplay";
2
+ import { InstrumentPlayer, PlaybackInstrument } from "./players/InstrumentPlayer";
3
+ import { IAudioContext } from "standardized-audio-context";
4
+ export declare enum PlaybackState {
5
+ INIT = "INIT",
6
+ PLAYING = "PLAYING",
7
+ STOPPED = "STOPPED",
8
+ PAUSED = "PAUSED"
9
+ }
10
+ export declare enum PlaybackEvent {
11
+ STATE_CHANGE = "state-change",
12
+ ITERATION = "iteration"
13
+ }
14
+ interface PlaybackSettings {
15
+ bpm: number;
16
+ masterVolume: number;
17
+ }
18
+ export default class PlaybackEngine {
19
+ private ac;
20
+ private defaultBpm;
21
+ private cursor;
22
+ private sheet;
23
+ private scheduler;
24
+ private instrumentPlayer;
25
+ private events;
26
+ private iterationSteps;
27
+ private currentIterationStep;
28
+ private timeoutHandles;
29
+ playbackSettings: PlaybackSettings;
30
+ state: PlaybackState;
31
+ availableInstruments: PlaybackInstrument[];
32
+ scoreInstruments: Instrument[];
33
+ ready: boolean;
34
+ constructor(context?: IAudioContext, instrumentPlayer?: InstrumentPlayer);
35
+ get wholeNoteLength(): number;
36
+ getPlaybackInstrument(voiceId: number): PlaybackInstrument;
37
+ setInstrument(voice: Voice, midiInstrumentId: number): Promise<void>;
38
+ loadScore(osmd: OpenSheetMusicDisplay): Promise<void>;
39
+ private initInstruments;
40
+ private loadInstruments;
41
+ private fallbackToPiano;
42
+ play(): Promise<void>;
43
+ stop(): Promise<void>;
44
+ pause(): void;
45
+ jumpToStep(step: number): void;
46
+ setBpm(bpm: number): void;
47
+ on(event: PlaybackEvent, cb: (...args: unknown[]) => void): void;
48
+ private countAndSetIterationSteps;
49
+ private notePlaybackCallback;
50
+ private setState;
51
+ private stopPlayers;
52
+ private clearTimeouts;
53
+ private iterationCallback;
54
+ }
55
+ export {};
@@ -0,0 +1,211 @@
1
+ import PlaybackScheduler from "./PlaybackScheduler";
2
+ import { SoundfontPlayer } from "./players/SoundfontPlayer";
3
+ import { getNoteDuration, getNoteVolume, getNoteArticulationStyle } from "./internals/noteHelpers";
4
+ import { EventEmitter } from "./internals/EventEmitter";
5
+ import { AudioContext } from "standardized-audio-context";
6
+ export var PlaybackState;
7
+ (function (PlaybackState) {
8
+ PlaybackState["INIT"] = "INIT";
9
+ PlaybackState["PLAYING"] = "PLAYING";
10
+ PlaybackState["STOPPED"] = "STOPPED";
11
+ PlaybackState["PAUSED"] = "PAUSED";
12
+ })(PlaybackState || (PlaybackState = {}));
13
+ export var PlaybackEvent;
14
+ (function (PlaybackEvent) {
15
+ PlaybackEvent["STATE_CHANGE"] = "state-change";
16
+ PlaybackEvent["ITERATION"] = "iteration";
17
+ })(PlaybackEvent || (PlaybackEvent = {}));
18
+ export default class PlaybackEngine {
19
+ constructor(context = new AudioContext(), instrumentPlayer = new SoundfontPlayer()) {
20
+ this.defaultBpm = 100;
21
+ this.scoreInstruments = [];
22
+ this.ready = false;
23
+ this.ac = context;
24
+ this.ac.suspend();
25
+ this.instrumentPlayer = instrumentPlayer;
26
+ this.instrumentPlayer.init(this.ac);
27
+ this.availableInstruments = this.instrumentPlayer.instruments;
28
+ this.events = new EventEmitter();
29
+ this.cursor = null;
30
+ this.sheet = null;
31
+ this.scheduler = null;
32
+ this.iterationSteps = 0;
33
+ this.currentIterationStep = 0;
34
+ this.timeoutHandles = [];
35
+ this.playbackSettings = {
36
+ bpm: this.defaultBpm,
37
+ masterVolume: 1,
38
+ };
39
+ this.setState(PlaybackState.INIT);
40
+ }
41
+ get wholeNoteLength() {
42
+ return Math.round((60 / this.playbackSettings.bpm) * 4000);
43
+ }
44
+ getPlaybackInstrument(voiceId) {
45
+ if (!this.sheet)
46
+ return null;
47
+ const voice = this.sheet.Instruments.flatMap((i) => i.Voices).find((v) => v.VoiceId === voiceId);
48
+ return this.availableInstruments.find((i) => i.midiId === voice.midiInstrumentId);
49
+ }
50
+ async setInstrument(voice, midiInstrumentId) {
51
+ await this.instrumentPlayer.load(midiInstrumentId);
52
+ voice.midiInstrumentId = midiInstrumentId;
53
+ }
54
+ async loadScore(osmd) {
55
+ this.ready = false;
56
+ this.sheet = osmd.Sheet;
57
+ this.scoreInstruments = this.sheet.Instruments;
58
+ this.cursor = osmd.cursor;
59
+ if (this.sheet.HasBPMInfo) {
60
+ this.setBpm(this.sheet.DefaultStartTempoInBpm);
61
+ }
62
+ await this.loadInstruments();
63
+ this.initInstruments();
64
+ this.scheduler = new PlaybackScheduler(this.wholeNoteLength, this.ac, (delay, notes) => this.notePlaybackCallback(delay, notes));
65
+ this.countAndSetIterationSteps();
66
+ this.ready = true;
67
+ this.setState(PlaybackState.STOPPED);
68
+ }
69
+ initInstruments() {
70
+ for (const i of this.sheet.Instruments) {
71
+ for (const v of i.Voices) {
72
+ v.midiInstrumentId = i.MidiInstrumentId;
73
+ }
74
+ }
75
+ }
76
+ async loadInstruments() {
77
+ const playerPromises = [];
78
+ for (const i of this.sheet.Instruments) {
79
+ const pbInstrument = this.availableInstruments.find((pbi) => pbi.midiId === i.MidiInstrumentId);
80
+ if (pbInstrument == null) {
81
+ this.fallbackToPiano(i);
82
+ }
83
+ playerPromises.push(this.instrumentPlayer.load(i.MidiInstrumentId));
84
+ }
85
+ await Promise.all(playerPromises);
86
+ }
87
+ fallbackToPiano(i) {
88
+ console.warn(`Can't find playback instrument for midiInstrumentId ${i.MidiInstrumentId}. Falling back to piano`);
89
+ i.MidiInstrumentId = 0;
90
+ if (this.availableInstruments.find((i) => i.midiId === 0) == null) {
91
+ throw new Error("Piano fallback failed, grand piano not supported");
92
+ }
93
+ }
94
+ async play() {
95
+ await this.ac.resume();
96
+ if (this.state === PlaybackState.INIT || this.state === PlaybackState.STOPPED) {
97
+ this.cursor.show();
98
+ }
99
+ this.setState(PlaybackState.PLAYING);
100
+ this.scheduler.start();
101
+ }
102
+ async stop() {
103
+ this.setState(PlaybackState.STOPPED);
104
+ this.stopPlayers();
105
+ this.clearTimeouts();
106
+ this.scheduler.reset();
107
+ this.cursor.reset();
108
+ this.currentIterationStep = 0;
109
+ this.cursor.hide();
110
+ }
111
+ pause() {
112
+ this.setState(PlaybackState.PAUSED);
113
+ this.ac.suspend();
114
+ this.stopPlayers();
115
+ this.scheduler.setIterationStep(this.currentIterationStep);
116
+ this.scheduler.pause();
117
+ this.clearTimeouts();
118
+ }
119
+ jumpToStep(step) {
120
+ this.pause();
121
+ if (this.currentIterationStep > step) {
122
+ this.cursor.reset();
123
+ this.currentIterationStep = 0;
124
+ }
125
+ while (this.currentIterationStep < step) {
126
+ this.cursor.next();
127
+ ++this.currentIterationStep;
128
+ }
129
+ let schedulerStep = this.currentIterationStep;
130
+ if (this.currentIterationStep > 0 && this.currentIterationStep < this.iterationSteps)
131
+ ++schedulerStep;
132
+ this.scheduler.setIterationStep(schedulerStep);
133
+ }
134
+ setBpm(bpm) {
135
+ this.playbackSettings.bpm = bpm;
136
+ if (this.scheduler)
137
+ this.scheduler.wholeNoteLength = this.wholeNoteLength;
138
+ }
139
+ on(event, cb) {
140
+ this.events.on(event, cb);
141
+ }
142
+ countAndSetIterationSteps() {
143
+ this.cursor.reset();
144
+ let steps = 0;
145
+ while (!this.cursor.Iterator.EndReached) {
146
+ if (this.cursor.Iterator.CurrentVoiceEntries) {
147
+ this.scheduler.loadNotes(this.cursor.Iterator.CurrentVoiceEntries);
148
+ }
149
+ this.cursor.next();
150
+ ++steps;
151
+ }
152
+ this.iterationSteps = steps;
153
+ this.cursor.reset();
154
+ }
155
+ notePlaybackCallback(audioDelay, notes) {
156
+ if (this.state !== PlaybackState.PLAYING)
157
+ return;
158
+ const scheduledNotes = new Map();
159
+ for (const note of notes) {
160
+ if (note.isRest()) {
161
+ continue;
162
+ }
163
+ const noteDuration = getNoteDuration(note, this.wholeNoteLength);
164
+ if (noteDuration === 0)
165
+ continue;
166
+ const noteVolume = getNoteVolume(note);
167
+ const noteArticulation = getNoteArticulationStyle(note);
168
+ const midiPlaybackInstrument = note.ParentVoiceEntry.ParentVoice.midiInstrumentId;
169
+ const fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments[0].fixedKey || 0;
170
+ if (!scheduledNotes.has(midiPlaybackInstrument)) {
171
+ scheduledNotes.set(midiPlaybackInstrument, []);
172
+ }
173
+ scheduledNotes.get(midiPlaybackInstrument).push({
174
+ note: note.halfTone - fixedKey * 12,
175
+ duration: noteDuration / 1000,
176
+ gain: noteVolume,
177
+ articulation: noteArticulation,
178
+ });
179
+ }
180
+ for (const [midiId, notes] of scheduledNotes) {
181
+ this.instrumentPlayer.schedule(midiId, this.ac.currentTime + audioDelay, notes);
182
+ }
183
+ this.timeoutHandles.push(window.setTimeout(() => this.iterationCallback(), Math.max(0, audioDelay * 1000 - 35)), // Subtracting 35 milliseconds to compensate for update delay
184
+ window.setTimeout(() => this.events.emit(PlaybackEvent.ITERATION, notes), audioDelay * 1000));
185
+ }
186
+ setState(state) {
187
+ this.state = state;
188
+ this.events.emit(PlaybackEvent.STATE_CHANGE, state);
189
+ }
190
+ stopPlayers() {
191
+ for (const i of this.sheet.Instruments) {
192
+ for (const v of i.Voices) {
193
+ this.instrumentPlayer.stop(v.midiInstrumentId);
194
+ }
195
+ }
196
+ }
197
+ // Used to avoid duplicate cursor movements after a rapid pause/resume action
198
+ clearTimeouts() {
199
+ for (const h of this.timeoutHandles) {
200
+ clearTimeout(h);
201
+ }
202
+ this.timeoutHandles = [];
203
+ }
204
+ iterationCallback() {
205
+ if (this.state !== PlaybackState.PLAYING)
206
+ return;
207
+ if (this.currentIterationStep > 0)
208
+ this.cursor.next();
209
+ ++this.currentIterationStep;
210
+ }
211
+ }
@@ -0,0 +1,34 @@
1
+ import { Note, VoiceEntry } from "opensheetmusicdisplay";
2
+ import { IAudioContext } from "standardized-audio-context";
3
+ type NoteSchedulingCallback = (delay: number, notes: Note[]) => void;
4
+ export default class PlaybackScheduler {
5
+ wholeNoteLength: number;
6
+ private stepQueue;
7
+ private stepQueueIndex;
8
+ private scheduledTicks;
9
+ private currentTick;
10
+ private currentTickTimestamp;
11
+ private audioContext;
12
+ private audioContextStartTime;
13
+ private schedulerIntervalHandle;
14
+ private scheduleInterval;
15
+ private schedulePeriod;
16
+ private tickDenominator;
17
+ private lastTickOffset;
18
+ private playing;
19
+ private noteSchedulingCallback;
20
+ constructor(wholeNoteLength: number, audioContext: IAudioContext, noteSchedulingCallback: NoteSchedulingCallback);
21
+ get schedulePeriodTicks(): number;
22
+ get audioContextTime(): number;
23
+ get tickDuration(): number;
24
+ private get calculatedTick();
25
+ start(): void;
26
+ setIterationStep(step: number): void;
27
+ pause(): void;
28
+ resume(): void;
29
+ reset(): void;
30
+ loadNotes(currentVoiceEntries: VoiceEntry[]): void;
31
+ private scheduleIterationStep;
32
+ private nextTickAvailableAndWithinSchedulePeriod;
33
+ }
34
+ export {};
@@ -0,0 +1,105 @@
1
+ import StepQueue from "./internals/StepQueue";
2
+ export default class PlaybackScheduler {
3
+ constructor(wholeNoteLength, audioContext, noteSchedulingCallback) {
4
+ this.stepQueue = new StepQueue();
5
+ this.stepQueueIndex = 0;
6
+ this.scheduledTicks = new Set();
7
+ this.currentTick = 0;
8
+ this.currentTickTimestamp = 0;
9
+ this.audioContextStartTime = 0;
10
+ this.schedulerIntervalHandle = null;
11
+ this.scheduleInterval = 200; // Milliseconds
12
+ this.schedulePeriod = 500;
13
+ this.tickDenominator = 1024;
14
+ this.lastTickOffset = 300; // Hack to get the initial notes play better
15
+ this.playing = false;
16
+ this.noteSchedulingCallback = noteSchedulingCallback;
17
+ this.wholeNoteLength = wholeNoteLength;
18
+ this.audioContext = audioContext;
19
+ }
20
+ get schedulePeriodTicks() {
21
+ return this.schedulePeriod / this.tickDuration;
22
+ }
23
+ get audioContextTime() {
24
+ if (!this.audioContext)
25
+ return 0;
26
+ return (this.audioContext.currentTime - this.audioContextStartTime) * 1000;
27
+ }
28
+ get tickDuration() {
29
+ return this.wholeNoteLength / this.tickDenominator;
30
+ }
31
+ get calculatedTick() {
32
+ return (this.currentTick +
33
+ Math.round((this.audioContextTime - this.currentTickTimestamp) / this.tickDuration));
34
+ }
35
+ start() {
36
+ this.playing = true;
37
+ this.stepQueue.sort();
38
+ this.audioContextStartTime = this.audioContext.currentTime;
39
+ this.currentTickTimestamp = this.audioContextTime;
40
+ if (!this.schedulerIntervalHandle) {
41
+ this.schedulerIntervalHandle = window.setInterval(() => this.scheduleIterationStep(), this.scheduleInterval);
42
+ }
43
+ }
44
+ setIterationStep(step) {
45
+ step = Math.min(this.stepQueue.steps.length - 1, step);
46
+ this.stepQueueIndex = step;
47
+ this.currentTick = this.stepQueue.steps[this.stepQueueIndex].tick;
48
+ }
49
+ pause() {
50
+ this.playing = false;
51
+ }
52
+ resume() {
53
+ this.playing = true;
54
+ this.currentTickTimestamp = this.audioContextTime;
55
+ }
56
+ reset() {
57
+ this.playing = false;
58
+ this.currentTick = 0;
59
+ this.currentTickTimestamp = 0;
60
+ this.stepQueueIndex = 0;
61
+ clearInterval(this.scheduleInterval);
62
+ this.schedulerIntervalHandle = null;
63
+ }
64
+ loadNotes(currentVoiceEntries) {
65
+ let thisTick = this.lastTickOffset;
66
+ if (this.stepQueue.steps.length > 0) {
67
+ thisTick = this.stepQueue.getFirstEmptyTick();
68
+ }
69
+ for (const entry of currentVoiceEntries) {
70
+ if (!entry.IsGrace) {
71
+ for (const note of entry.Notes) {
72
+ this.stepQueue.addNote(thisTick, note);
73
+ this.stepQueue.createStep(thisTick + note.Length.RealValue * this.tickDenominator);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ scheduleIterationStep() {
79
+ if (!this.playing)
80
+ return;
81
+ this.currentTick = this.calculatedTick;
82
+ this.currentTickTimestamp = this.audioContextTime;
83
+ let nextTick = this.stepQueue.steps[this.stepQueueIndex]?.tick;
84
+ while (this.nextTickAvailableAndWithinSchedulePeriod(nextTick)) {
85
+ const step = this.stepQueue.steps[this.stepQueueIndex];
86
+ let timeToTick = (step.tick - this.currentTick) * this.tickDuration;
87
+ if (timeToTick < 0)
88
+ timeToTick = 0;
89
+ this.scheduledTicks.add(step.tick);
90
+ this.noteSchedulingCallback(timeToTick / 1000, step.notes);
91
+ this.stepQueueIndex++;
92
+ nextTick = this.stepQueue.steps[this.stepQueueIndex]?.tick;
93
+ }
94
+ for (const tick of this.scheduledTicks) {
95
+ if (tick <= this.currentTick) {
96
+ this.scheduledTicks.delete(tick);
97
+ }
98
+ }
99
+ }
100
+ nextTickAvailableAndWithinSchedulePeriod(nextTick) {
101
+ return (nextTick &&
102
+ this.currentTickTimestamp + (nextTick - this.currentTick) * this.tickDuration <=
103
+ this.currentTickTimestamp + this.schedulePeriod);
104
+ }
105
+ }
@@ -0,0 +1,2 @@
1
+ import PlaybackEngine from "./PlaybackEngine";
2
+ export default PlaybackEngine;
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import PlaybackEngine from "./PlaybackEngine";
2
+ export default PlaybackEngine;
@@ -0,0 +1,7 @@
1
+ type Callback = (...args: unknown[]) => void;
2
+ export declare class EventEmitter<T> {
3
+ private subscribers;
4
+ on(event: T, callback: Callback): void;
5
+ emit(event: T, ...args: unknown[]): void;
6
+ }
7
+ export {};