@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.
- package/.circleci/config.yml +18 -0
- package/.gitattributes +12 -0
- package/.github/workflows/ci.yml +23 -0
- package/.prettierrc +4 -0
- package/.prettierrc.js +4 -0
- package/CLAUDE.md +96 -0
- package/LICENSE +21 -0
- package/README.md +79 -0
- package/dist/PlaybackEngine.d.ts +55 -0
- package/dist/PlaybackEngine.js +211 -0
- package/dist/PlaybackScheduler.d.ts +34 -0
- package/dist/PlaybackScheduler.js +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/internals/EventEmitter.d.ts +7 -0
- package/dist/internals/EventEmitter.js +17 -0
- package/dist/internals/StepQueue.d.ts +16 -0
- package/dist/internals/StepQueue.js +32 -0
- package/dist/internals/noteHelpers.d.ts +5 -0
- package/dist/internals/noteHelpers.js +24 -0
- package/dist/midi/midiInstruments.d.ts +1 -0
- package/dist/midi/midiInstruments.js +130 -0
- package/dist/players/InstrumentPlayer.d.ts +15 -0
- package/dist/players/InstrumentPlayer.js +1 -0
- package/dist/players/NotePlaybackOptions.d.ts +13 -0
- package/dist/players/NotePlaybackOptions.js +6 -0
- package/dist/players/SoundfontPlayer.d.ts +17 -0
- package/dist/players/SoundfontPlayer.js +54 -0
- package/dist/players/musyngkiteInstruments.d.ts +2 -0
- package/dist/players/musyngkiteInstruments.js +130 -0
- package/eslint.config.js +12 -0
- package/package.json +66 -0
- package/rollup.umd.config.js +23 -0
- package/src/PlaybackEngine.test.ts +99 -0
- package/src/PlaybackEngine.ts +294 -0
- package/src/PlaybackScheduler.ts +146 -0
- package/src/index.ts +3 -0
- package/src/internals/EventEmitter.test.ts +57 -0
- package/src/internals/EventEmitter.ts +19 -0
- package/src/internals/StepQueue.ts +45 -0
- package/src/internals/noteHelpers.ts +26 -0
- package/src/midi/midiInstruments.ts +130 -0
- package/src/players/InstrumentPlayer.ts +17 -0
- package/src/players/NotePlaybackOptions.ts +15 -0
- package/src/players/SoundfontPlayer.ts +76 -0
- package/src/players/musyngkiteInstruments.ts +130 -0
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +14 -0
- package/umd/OsmdAudioPlayer.min.js +15 -0
- 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
package/.prettierrc.js
ADDED
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
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED