midi-audio-player 1.0.0
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/LICENSE +21 -0
- package/README.md +86 -0
- package/bin/cli.js +27 -0
- package/index.js +1 -0
- package/package.json +33 -0
- package/src/defaultinstrument.json +116 -0
- package/src/downloader.js +54 -0
- package/src/midiaudioplayer.js +113 -0
- package/src/webaudiofontplayer.js +255 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Maxime Larrivée-Roy
|
|
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,86 @@
|
|
|
1
|
+
# midi-audio-player
|
|
2
|
+
|
|
3
|
+
A lightweight JavaScript MIDI audio player built on top of the Web Audio API using WebAudioFont. This package enables playback of MIDI files directly in the browser with minimal setup and no heavy dependencies.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
* MIDI file playback in modern browsers
|
|
8
|
+
* Built on the Web Audio API
|
|
9
|
+
* Uses WebAudioFont for instrument rendering
|
|
10
|
+
* Lightweight and dependency-minimal
|
|
11
|
+
* Simple programmatic API
|
|
12
|
+
* CLI tool for instrument/font handling
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install midi-audio-player
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Basic Example
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import MidiAudioPlayer from 'midi-audio-player';
|
|
26
|
+
|
|
27
|
+
const player = new MidiAudioPlayer({
|
|
28
|
+
volume: 0.02,
|
|
29
|
+
instrument: instrumentData,
|
|
30
|
+
onEndFile: async () => await this.playNextSong()
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
await player.play('binarycontent');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Control Playback
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
player.play();
|
|
40
|
+
player.pause();
|
|
41
|
+
player.stop();
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Working with AudioContext
|
|
45
|
+
|
|
46
|
+
Due to browser autoplay restrictions, you should ensure that your AudioContext is resumed after a user interaction:
|
|
47
|
+
|
|
48
|
+
## CLI
|
|
49
|
+
|
|
50
|
+
This package provides a CLI tool for downloading and converting WebAudioFont assets. You need to provide a WebAudioFont ID dans the json file for the destination.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
webaudiofont 0000_Chaos_sf2_file dest/instrument.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
You can find instruments here: [WebAudioFont](https://github.com/surikov/webaudiofont#catalog-of-instruments)
|
|
57
|
+
|
|
58
|
+
## Browser Compatibility
|
|
59
|
+
|
|
60
|
+
Requires a modern browser with support for:
|
|
61
|
+
|
|
62
|
+
* Web Audio API
|
|
63
|
+
* ES Modules
|
|
64
|
+
|
|
65
|
+
## Limitations
|
|
66
|
+
|
|
67
|
+
* First playback may be delayed if the AudioContext is not initialized properly
|
|
68
|
+
* Depends on WebAudioFont instrument quality and availability
|
|
69
|
+
* Not intended for high-fidelity or professional audio rendering
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
[LICENCE](LICENSE)
|
|
75
|
+
|
|
76
|
+
## Author
|
|
77
|
+
|
|
78
|
+
Maxime Larrivée-Roy
|
|
79
|
+
|
|
80
|
+
## Repository
|
|
81
|
+
|
|
82
|
+
[https://github.com/ZmotriN/midi-audio-player](https://github.com/ZmotriN/midi-audio-player)
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
If you want, I can also tailor it more toward your Phaser/game use case or add a section comparing it with other MIDI solutions (which can be useful positioning-wise).
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { downloadWebAudioFont } from '../src/downloader.js';
|
|
4
|
+
|
|
5
|
+
// Récupération des arguments (ex: node cli.js <id> <destination>)
|
|
6
|
+
// process.argv[0] est Node, process.argv[1] est le chemin du script
|
|
7
|
+
const [id, destination] = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
if (!id || !destination) {
|
|
10
|
+
console.error("\x1b[31m%s\x1b[0m", "Erreur : Arguments manquants.");
|
|
11
|
+
console.log("\nUsage :");
|
|
12
|
+
console.log(" webaudiofont <id> <destination.json>");
|
|
13
|
+
console.log("\nExemple :");
|
|
14
|
+
console.log(" webaudiofont 0810_GeneralUserGS_sf2_file assets/sound.json\n");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function run() {
|
|
19
|
+
try {
|
|
20
|
+
await downloadWebAudioFont(id, destination);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// L'erreur est déjà logguée dans downloadWebAudioFont, on quitte proprement
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
run();
|
package/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from './src/midiaudioplayer.js';
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "midi-audio-player",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Javascript Midi Audio Player for WebAudioFont",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"audio",
|
|
7
|
+
"player",
|
|
8
|
+
"midi",
|
|
9
|
+
"js",
|
|
10
|
+
"webaudiofont"
|
|
11
|
+
],
|
|
12
|
+
"homepage": "https://github.com/ZmotriN/midi-audio-player#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/ZmotriN/midi-audio-player/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/ZmotriN/midi-audio-player.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"author": "Maxime Larrivée-Roy",
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "index.js",
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"webaudiofont": "./bin/cli.js"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"midi-player-js": "^2.0.17"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
{
|
|
2
|
+
"zones": [
|
|
3
|
+
{
|
|
4
|
+
"midi": 81,
|
|
5
|
+
"originalPitch": 4200,
|
|
6
|
+
"keyRangeLow": 0,
|
|
7
|
+
"keyRangeHigh": 43,
|
|
8
|
+
"loopStart": 256,
|
|
9
|
+
"loopEnd": 768,
|
|
10
|
+
"coarseTune": 0,
|
|
11
|
+
"fineTune": 0,
|
|
12
|
+
"sampleRate": 48000,
|
|
13
|
+
"ahdsr": false,
|
|
14
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAGAACgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCg//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJALAAAAAAAAABgA5CeiPAAAAAAAAAAAAAAAAAAAA//vUxAAAB+gfQ1TwACTywCx/NYAJAABZOW7fg6F8L8A3AOwb5L2e7x5R5EoCADB8HwfPxACAYrB8H8oCDsHw/lAQdlwPqDHh/UCEp4fhiU8/ynv6BAGOZS1NzNUNm83W5HHJGz4Kk0TfzwzSZu8CgJ3TZ11pu3hi1hoxY8qMFcPAcBX40CM1RcwUo9EFQgiY2VPldy90c24GUwVQROZnA0ZXohQyMsyIBlAWEQK/zrsHfSBGmF4i+yiKKKNrP5FDrYJM+8XdeIRR1FaEwk0mLq5Upl0G35bBliJz8DxCcgSVtAYMpU27BVbW0tSXOzHsLU+88MTkCQ3PwO05aTrtqwF1G2YFHc7M1hamc5RYnJXbn6S9OU7L2apytcaEpk5bQVbXIszWrUzuzNatW5HKL05K+z9Jyip+vcyqG3yZTDELa1DcIazEIXV3jW1lV3jW1lV7h/M/7h/M/7h/9/+f/f/n/3/58qr2JTUtyqvYlNy3TXbFLct012xS3Kemu0lLWp6arSexu7G62Ja2Ja2Ja0QsKrWx13Wb36/a/7/ew5kMjLhQNcmAxqICUIjQsMqB4zeSzGokEALDBOxI2OrTTJJMLBUwcCQYAVMDdlQ0A6Zkis4q5jIAQzknMdGQMBLAltWVxZrJm4ma0hGaGCmrpKmfS87RrhMci3muq5p4+b4wKbPCqVlsBQHbcmCKhmxoZcGGmnZmJeZOEmiGS7nGjrWX5mn+j92HZJcMqKDIgYz0vMnJTHwUzYoMiIGdQNMuTIbj/U13dLc1aMqCjSTkzEvMpCTQjQy4qMkBjPTGR3X9pMI1N9q1aWtWpqtUyspMhBTOCwyYkMeAjNSsyUjMdATMCYyIgpZJZygGm+AqXGzWrWqtWzWrWjGgAy0lMhHwEXmVERjw4AigycfMdGwMVmSDhjQwCiSU1solVxiNamhqlpYZx3jllljvHLWWLlv/F3cfyKO+/8vfx/JY/7/y9/H8lj/v/Pv5Dk5D8bn4xGJyNxuf1llvHHWWW8cdZZbx//u0xLeAL2H/Z7nNkIFVkCR3sMAFx1llvHHWVbHGrllWAbmu1l2+wACIJmablgZJEkGiBRmRmRKSQMOakoAW1jywyxmXRDkBISlWnK1b7K0xdxcdLomjI+e1lat9latrlly7rLnrbVat8FFQgpgKeCmxTcgrgVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
|
|
15
|
+
"anchor": 0.00533333
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"midi": 81,
|
|
19
|
+
"originalPitch": 5400,
|
|
20
|
+
"keyRangeLow": 44,
|
|
21
|
+
"keyRangeHigh": 55,
|
|
22
|
+
"loopStart": 128,
|
|
23
|
+
"loopEnd": 384,
|
|
24
|
+
"coarseTune": 0,
|
|
25
|
+
"fineTune": 0,
|
|
26
|
+
"sampleRate": 48000,
|
|
27
|
+
"ahdsr": false,
|
|
28
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAEsADMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzM//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJATAAAAAAAAABLAMEvhQAAAAAAAAAAAAAAAAAAAA//vUxAAACSwNaVQwACRXSC6/NYAIAAa9txyS8qHAwMDPlz5QEIIROfLn1AhBBy3lz/BCt5c/wQz5c/RBCXPlw/EEEHLPlz9EEOt8QQQcs+XPykQVh8uH4gcIHLD6w+JgcGkGbGakSCqjColKJDEMCR4mYQ8kQZ1UAiwCAIbAc4b1maEch6LAEVzMhwxuYQGCgLBWbmQSPi+kAzkOM6i/Ei2XrGgFiMbisXednEOO7NP9KJdLKedbxd7vvezh3pl/YdmreFqxnZjEgf+NyN/I1TXaWzqnqWqTOzTyyihuX0cMUlNa3S81TYWrGd63q/RRuno4xSUUbt3YzSYU1fuH8z/uH8zp6OMUlFK6e/KKTG5zKZy+ax/+4fzP+4fzP70rp78opL1PXv0lSzVy+tjjVyyrY41e4fzf91/N/3X83/dfzf91/N/3X5/3D+b/uv5v+6/m/7r/3/6/9/+sst446yy3jVyyrY41csq2ONXLKtjjVyyrKiLXdbW5JI24SEB1CQjhFAAXAIxdxbUKO0ekXEuKphJ5DkSgYKJVFFxQVwUwFDcRcUFcFcCjsTYoK4K4FHYmxQXwrgUdibFBfCvCjsTcRXhXhT8TcRXhXhT8TcRXhXhT8TcRXhXhT8TcakxBTUUzLjk5LjWqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//tkxMWDzQgxS7zzACgAADSAAAAEqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
|
29
|
+
"anchor": 0.00266667
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"midi": 81,
|
|
33
|
+
"originalPitch": 6600,
|
|
34
|
+
"keyRangeLow": 56,
|
|
35
|
+
"keyRangeHigh": 67,
|
|
36
|
+
"loopStart": 64,
|
|
37
|
+
"loopEnd": 192,
|
|
38
|
+
"coarseTune": 0,
|
|
39
|
+
"fineTune": 0,
|
|
40
|
+
"sampleRate": 48000,
|
|
41
|
+
"ahdsr": false,
|
|
42
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAE4ADs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAXAAAAAAAAABODQyktQAAAAAAAAAAAAAAAAAAAA//vkxAAABywBgbQAACYVyC6/P9AwKAASUcckkt4YWD4fLn1AhBCJz5c+oEFghy5/ghy5+cghy7+GKZd/DHLn1HIYk5d/DHL+6H5d5TOdLnbLTLKrBqyq+vlkqeD4lCzTAngBgvMYACAAGg+NZZoFwogYAGAZCwCsDgBJsJhRJLyYvYUvg4BLFAA9AKpqYxQCumE3hMhEBIkBkBLuNR5oOGzUbs0Jk7jR1yTFgpzGY2TMgWV8qtT4fmaf4xDKgxlNgzTGRZScSFbPVQxarGZbWMLiNMUC1MshXMJx+MRSeaZHnuceOvbGrV2ls6MOQUMAAnMDAzMMgRMAQkMCQufiahT9zMJfim7ul5rIDBEFgJBgHgoHQuBANAwFA1WkUbqzssyns944flv09UjUcU9kjkkk9UjZfjOyzKep8Z2kxucyq9+ax9JFNJNJNZNFNFNVNJNJNbKRRvGQxjKejfJ2Udq/9bf1darY41U0U0U1U0k0k+k0U0U+U0k0k+k0e5/zD+5/zD+5/zX93/Nf3f81/d////////7/9f+//X/v/1/7/9Jop8ppJpJ9Jop6qwppJ7KxJGp6qwpHKrKxI2qqqwo3KrKxI2qqq9RuVWV8jaqqr1G5TEFNRTMuOTkuNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxNoDwAAB/hwAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
|
|
43
|
+
"anchor": 0.00133333
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"midi": 81,
|
|
47
|
+
"originalPitch": 7800,
|
|
48
|
+
"keyRangeLow": 68,
|
|
49
|
+
"keyRangeHigh": 79,
|
|
50
|
+
"loopStart": 32,
|
|
51
|
+
"loopEnd": 96,
|
|
52
|
+
"coarseTune": 0,
|
|
53
|
+
"fineTune": 0,
|
|
54
|
+
"sampleRate": 48000,
|
|
55
|
+
"ahdsr": false,
|
|
56
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAZAAAAAAAAAA8CsVDBMAAAAAAAAAAAAAAAAAAAA//vExAAABmQBjaAAACGQBa9497AdtbAkWdckklxRyjhQ5BBYYqOFHRAXDHKHOGOUOcMco7nOJHawxynnOU5cMcHOXOcHOl3NXGMqsiEAqoMI8QsSUcIsgeiQgJISFCg5ABAyw+VQaiK8yY18FNiuxBUorwU0FdiC4ivimgrsQXEV4KaCuxBcTfBXAv4isTfiuBf6KxN+L4r8RUU3wXoL+JsU3wXoL+JsV3gvRSpMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqq6goZoAABEOiCXDs8P3G8oicJAhZjVVVBlsStNFVVUT8StNVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxOuDxEQnByYExigAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
|
|
57
|
+
"anchor": 0.00066667
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"midi": 81,
|
|
61
|
+
"originalPitch": 9000,
|
|
62
|
+
"keyRangeLow": 80,
|
|
63
|
+
"keyRangeHigh": 91,
|
|
64
|
+
"loopStart": 16,
|
|
65
|
+
"loopEnd": 48,
|
|
66
|
+
"coarseTune": 0,
|
|
67
|
+
"fineTune": 0,
|
|
68
|
+
"sampleRate": 48000,
|
|
69
|
+
"ahdsr": false,
|
|
70
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaAAAAAAAAAA8ChXkahAAAAAAAAAAAAAAAAAAAA//vExAAABkQDh+AAACGUhq9895iZZVlzMgFEYk0nBACCLQfD6gTB8/qBDBwMflC4f/gQMfqBMH/4OHP4Df+XBw5+UBN/6wJu1fapqZhlttyF2FyUYQ0W0QkFSJiRIro4TAYk8aQCAQCAKjiRIkSr+xQUFOBQUV/5BQU4KChv+CgoL4KCu/4QUN8KCn/8UFN8FFf/wgp/hQ3/8FBXYgoL/+FBX5BQ3/8FBfigopVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxNoDwAAB/gAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
|
|
71
|
+
"anchor": 0.00033333
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"midi": 81,
|
|
75
|
+
"originalPitch": 10200,
|
|
76
|
+
"keyRangeLow": 92,
|
|
77
|
+
"keyRangeHigh": 103,
|
|
78
|
+
"loopStart": 8,
|
|
79
|
+
"loopEnd": 56,
|
|
80
|
+
"coarseTune": 0,
|
|
81
|
+
"fineTune": 0,
|
|
82
|
+
"sampleRate": 48000,
|
|
83
|
+
"ahdsr": false,
|
|
84
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaAAAAAAAAAA8AljJJeAAAAAAAAAAAAAAAAAAAA//vExAAABmgDh+AAACG7Cq649JmAh3unfGYwJRhSgQBBEXB8PjogBDn8EwfP6gQdicP/g4GOXB/lHfgmH905/UclwcOf/8oA/2r/qekIFM1D+AxK8OEgo9IhpLSJFdJaehzC3CbBom0GChJpEiRn/tRxIkSJT6OJJB3BUFXSwNB3BUFQVgqCoaqBoGsqd4iBo8oGgaTWCoaxEDR6VBUFTpUFQV//BoGoiBoO//rxKCv//iIGv/+WTEFNRTMuOTkuNVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVMQU1FMy45OS41VVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//sUxNoDwAAB/gAAACAAADSAAAAEVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
|
|
85
|
+
"anchor": 0.00016667
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"midi": 81,
|
|
89
|
+
"originalPitch": 11400,
|
|
90
|
+
"keyRangeLow": 104,
|
|
91
|
+
"keyRangeHigh": 115,
|
|
92
|
+
"loopStart": 12,
|
|
93
|
+
"loopEnd": 52,
|
|
94
|
+
"coarseTune": 0,
|
|
95
|
+
"fineTune": 0,
|
|
96
|
+
"sampleRate": 48000,
|
|
97
|
+
"ahdsr": false,
|
|
98
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAADwADm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm5ubm//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaAAAAAAAAAA8CNr1v4AAAAAAAAAAAAAAAAAAAA//vExAAAB1SVheCEbeI4SC349ApomIy4jYiPtA6x5AACs4xjvkAAPmMY/wAXzGMf4AL/X+IgG4PvwQOfIQQOZcH//wQlwfBA5/1h/iB2sP/17/1GMvIAEJ8BiUYMEW0NSAKQjJWj5HCbB/EGHqQ5m2plUuYL1WvVqeGZmb/VVVf9mZr/1VVX9hYGwKgFgbB9TCwNgbB8BG6lKUreYxjfKUrfQxvylK3oYxjeUKAgICJYoCAgICUvoYxvylL9DGN9SlL9P+UrfMYxjOoYCAgIUb6lL////////qUpSzASTEFNRTMuOTkuNaqqqqpMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//sUxNoDwAAB/gAAACAAADSAAAAEqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
|
99
|
+
"anchor": 0.00008333
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"midi": 81,
|
|
103
|
+
"originalPitch": 12600,
|
|
104
|
+
"keyRangeLow": 116,
|
|
105
|
+
"keyRangeHigh": 127,
|
|
106
|
+
"loopStart": 6,
|
|
107
|
+
"loopEnd": 42,
|
|
108
|
+
"coarseTune": 0,
|
|
109
|
+
"fineTune": 0,
|
|
110
|
+
"sampleRate": 48000,
|
|
111
|
+
"ahdsr": false,
|
|
112
|
+
"file": "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU3LjcyLjEwMQAAAAAAAAAAAAAA//tUwAAAAAAAAAAAAAAAAAAAAAAAWGluZwAAAA8AAAACAAAEIADo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo6Ojo//////////////////////////////////////////////////////////////////8AAAAATGF2YzU3Ljk2AAAAAAAAAAAAAAAAJAaQAAAAAAAABCDWHfeqAAAAAAAAAAAAAAAAAAAA//vUxAAACUUrfeCE1eMLSCx88a54l3jMh4t/trUApEAAAUAGYxja+WMYx/yAAH+MY/+AYx/4AAX8xjH/7/8AF//zHyAMdoIAgAAADAYWTt4IA/Kc5/////////y4Pg/v3FZropQkAAAEiDVBBhkjdBSk5JyDlHqDmWi+kJRsJiOZ69eva6x+zAQEBCm/9VVVb/9mVVL/9mZmP/6qqqn/8ZlVV/6zMzM3/VCgICanXX+5I2JRJHaana+SSNwIQEQAwAQAQEw6j5KBCASACACAKAmBGHcdr5RNR2jtJRsed/DkTU1NTra/3Oc5znf+1rWuc7/3Na1ra/9znOc7/9rWtc7/9zWta3/4c5znO/9rTU1NXOvbW4lDtDyHkPI7TqQIQAxMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpMQU1FMy45OS41qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq//sUxNoDwAABpAAAACAAADSAAAAEqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
|
113
|
+
"anchor": 0.00004167
|
|
114
|
+
}
|
|
115
|
+
]
|
|
116
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Télécharge une police audio WebAudioFont et la convertit en JSON
|
|
6
|
+
* @param {string} id - L'ID de la police (ex: "0810_GeneralUserGS_sf2_file")
|
|
7
|
+
* @param {string} filename - Le nom du fichier de sortie (ex: "ma_police.json")
|
|
8
|
+
*/
|
|
9
|
+
export async function downloadWebAudioFont(id, filename) {
|
|
10
|
+
// Nettoyage du nom de fichier : s'assurer qu'il finit par .json
|
|
11
|
+
const cleanFilename = filename.endsWith('.json') ? filename : `${filename}.json`;
|
|
12
|
+
|
|
13
|
+
// On définit la destination par rapport au dossier de travail actuel
|
|
14
|
+
const destPath = path.join(process.cwd(), cleanFilename);
|
|
15
|
+
const url = `https://surikov.github.io/webaudiofontdata/sound/${id}.js`;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
console.log(`📡 Téléchargement de : ${id}...`);
|
|
19
|
+
|
|
20
|
+
const response = await fetch(url);
|
|
21
|
+
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
throw new Error(`Erreur HTTP: ${response.status} (Vérifiez l'ID)`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const rawContent = await response.text();
|
|
27
|
+
|
|
28
|
+
// Extraction de l'objet JS entre les premières '{' et les dernières '}'
|
|
29
|
+
const firstBrace = rawContent.indexOf('{');
|
|
30
|
+
const lastBrace = rawContent.lastIndexOf('}');
|
|
31
|
+
|
|
32
|
+
if (firstBrace === -1 || lastBrace === -1) {
|
|
33
|
+
throw new Error("Format de fichier invalide : structure d'objet introuvable.");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const objectString = rawContent.substring(firstBrace, lastBrace + 1);
|
|
37
|
+
|
|
38
|
+
// Transformation de la chaîne en objet JavaScript
|
|
39
|
+
const data = new Function(`return ${objectString}`)();
|
|
40
|
+
|
|
41
|
+
// Création du dossier parent s'il n'existe pas
|
|
42
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
43
|
+
|
|
44
|
+
// Sauvegarde en format JSON
|
|
45
|
+
await fs.writeFile(destPath, JSON.stringify(data, null, 2));
|
|
46
|
+
|
|
47
|
+
console.log(`✅ Terminé ! Fichier créé : ${destPath}`);
|
|
48
|
+
return data; // Retourne l'objet au cas où on veut l'utiliser immédiatement
|
|
49
|
+
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`❌ Échec : ${error.message}`);
|
|
52
|
+
throw error; // On relance l'erreur pour que l'appelant puisse la gérer
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import MidiPlayer from 'midi-player-js';
|
|
2
|
+
import WebAudioFontPlayer from "./webaudiofontplayer";
|
|
3
|
+
import DefaultInstrument from "./defaultinstrument.json";
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export default class MidiAudioPlayer {
|
|
8
|
+
|
|
9
|
+
#audioCtx = null;
|
|
10
|
+
#midiPlayer = null;
|
|
11
|
+
#audioPlayer = null;
|
|
12
|
+
#activeNotes = null;
|
|
13
|
+
|
|
14
|
+
#opts = {
|
|
15
|
+
instrument: DefaultInstrument,
|
|
16
|
+
volume: 0.012,
|
|
17
|
+
onEndFile: null
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
constructor(opts = {}) {
|
|
22
|
+
this.#opts = { ...this.#opts, ...opts };
|
|
23
|
+
this.#activeNotes = new Map();
|
|
24
|
+
this.#audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
25
|
+
this.#audioPlayer = new WebAudioFontPlayer(this.#audioCtx, this.#opts.instrument);
|
|
26
|
+
this.#midiPlayer = new MidiPlayer.Player(event => this.#handleMidiPipeline(event));
|
|
27
|
+
this.#midiPlayer.on('endOfFile', async () => {
|
|
28
|
+
await new Promise(resolve => requestAnimationFrame(() => setTimeout(resolve, 1)));
|
|
29
|
+
await this.#endOfFile();
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
async #endOfFile() {
|
|
35
|
+
if(typeof this.#opts.onEndFile == 'function') await this.#opts.onEndFile();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
async #handleMidiPipeline(event) {
|
|
40
|
+
if (event.name !== 'Note on' && event.name !== 'Note off') return;
|
|
41
|
+
if (!this.#midiPlayer.isPlaying()) return;
|
|
42
|
+
if (event.noteNumber === undefined) return;
|
|
43
|
+
|
|
44
|
+
const now = this.#audioCtx.currentTime;
|
|
45
|
+
|
|
46
|
+
switch (event.name) {
|
|
47
|
+
case 'Note on':
|
|
48
|
+
if (event.velocity > 0 && event.velocity <= 127) {
|
|
49
|
+
this.#stopNotePipe(event.noteNumber);
|
|
50
|
+
const vol = (event.velocity / 127) * this.#opts.volume;
|
|
51
|
+
const envelope = this.#audioPlayer.queueWaveTable(0, event.noteNumber, 2, vol);
|
|
52
|
+
this.#activeNotes.set(event.noteNumber, envelope);
|
|
53
|
+
} else {
|
|
54
|
+
this.#stopNotePipe(event.noteNumber);
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
|
|
58
|
+
case 'Note off':
|
|
59
|
+
this.#stopNotePipe(event.noteNumber);
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
#stopNotePipe(noteNumber) {
|
|
66
|
+
const envelope = this.#activeNotes.get(noteNumber);
|
|
67
|
+
if (envelope) {
|
|
68
|
+
envelope.cancel();
|
|
69
|
+
this.#activeNotes.delete(noteNumber);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
#clearActiveNotes() {
|
|
75
|
+
if (this.#activeNotes) {
|
|
76
|
+
this.#activeNotes.forEach((envelope, note) => {
|
|
77
|
+
if (envelope && envelope.cancel) {
|
|
78
|
+
envelope.cancel();
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
this.#activeNotes.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async #load(content) {
|
|
87
|
+
if(this.#midiPlayer.isPlaying()) this.#midiPlayer.stop();
|
|
88
|
+
this.#clearActiveNotes();
|
|
89
|
+
await this.#midiPlayer.loadArrayBuffer(content);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
async play(content = null) {
|
|
94
|
+
if(content) await this.#load(content);
|
|
95
|
+
await this.#audioCtx.resume();
|
|
96
|
+
await this.#midiPlayer.play();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async pause() {
|
|
101
|
+
await this.#midiPlayer.pause();
|
|
102
|
+
await this.#audioCtx.suspend();
|
|
103
|
+
await this.#clearActiveNotes();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async stop() {
|
|
108
|
+
await this.#midiPlayer.stop();
|
|
109
|
+
await this.#audioCtx.suspend();
|
|
110
|
+
await this.#clearActiveNotes();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
class WebAudioFontChannel {
|
|
4
|
+
|
|
5
|
+
constructor(audioContext) {
|
|
6
|
+
this.audioContext = audioContext;
|
|
7
|
+
this.input = audioContext.createGain();
|
|
8
|
+
|
|
9
|
+
let lastNode = this.input;
|
|
10
|
+
[32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384].forEach(freq => {
|
|
11
|
+
lastNode = this.bandEqualizer(lastNode, freq);
|
|
12
|
+
this[`band${freq < 1000 ? freq : (freq / 1024) + 'k'}`] = lastNode;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
this.limiter = audioContext.createDynamicsCompressor();
|
|
17
|
+
this.limiter.threshold.setValueAtTime(-3.0, audioContext.currentTime);
|
|
18
|
+
this.limiter.ratio.setValueAtTime(40, audioContext.currentTime);
|
|
19
|
+
this.limiter.attack.setValueAtTime(0.000, audioContext.currentTime);
|
|
20
|
+
this.limiter.release.setValueAtTime(0.25, audioContext.currentTime);
|
|
21
|
+
this.output = audioContext.createGain();
|
|
22
|
+
|
|
23
|
+
lastNode.connect(this.limiter);
|
|
24
|
+
this.limiter.connect(this.output);
|
|
25
|
+
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
bandEqualizer(from, frequency) {
|
|
29
|
+
const filter = this.audioContext.createBiquadFilter();
|
|
30
|
+
filter.frequency.setTargetAtTime(frequency, 0, 0.0001);
|
|
31
|
+
filter.type = "peaking";
|
|
32
|
+
filter.gain.setTargetAtTime(0, 0, 0.0001);
|
|
33
|
+
filter.Q.setTargetAtTime(1.0, 0, 0.0001);
|
|
34
|
+
from.connect(filter);
|
|
35
|
+
return filter;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class WebAudioFontPlayer {
|
|
40
|
+
|
|
41
|
+
envelopes = [];
|
|
42
|
+
afterTime = 0.05;
|
|
43
|
+
nearZero = 0.000001;
|
|
44
|
+
|
|
45
|
+
#audioCtx = null;
|
|
46
|
+
#preset = null;
|
|
47
|
+
|
|
48
|
+
constructor(audioCtx, preset) {
|
|
49
|
+
this.#audioCtx = audioCtx;
|
|
50
|
+
this.#preset = preset;
|
|
51
|
+
this.adjustPreset(this.#audioCtx, this.#preset);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
adjustPreset(audioContext, preset) {
|
|
56
|
+
return preset.zones.map(zone => this.adjustZone(audioContext, zone));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
adjustZone(audioContext, zone) {
|
|
61
|
+
if (zone.buffer) return Promise.resolve(zone);
|
|
62
|
+
zone.delay = 0;
|
|
63
|
+
|
|
64
|
+
if (zone.sample) {
|
|
65
|
+
const decoded = atob(zone.sample);
|
|
66
|
+
zone.buffer = audioContext.createBuffer(1, decoded.length / 2, zone.sampleRate);
|
|
67
|
+
const float32Array = zone.buffer.getChannelData(0);
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < decoded.length / 2; i++) {
|
|
70
|
+
const b1 = decoded.charCodeAt(i * 2) & 0xFF;
|
|
71
|
+
const b2 = decoded.charCodeAt(i * 2 + 1) & 0xFF;
|
|
72
|
+
let n = (b2 << 8) | b1;
|
|
73
|
+
if (n >= 32768) n -= 65536;
|
|
74
|
+
float32Array[i] = n / 32768.0;
|
|
75
|
+
}
|
|
76
|
+
this.applyZoneParameters(zone);
|
|
77
|
+
return zone;
|
|
78
|
+
|
|
79
|
+
} else if (zone.file) {
|
|
80
|
+
const decoded = atob(zone.file);
|
|
81
|
+
const uint8Array = new Uint8Array(decoded.length);
|
|
82
|
+
for (let i = 0; i < decoded.length; i++) {
|
|
83
|
+
uint8Array[i] = decoded.charCodeAt(i);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
audioContext.decodeAudioData(
|
|
87
|
+
uint8Array.buffer,
|
|
88
|
+
audioBuffer => {
|
|
89
|
+
zone.buffer = audioBuffer;
|
|
90
|
+
this.applyZoneParameters(zone);
|
|
91
|
+
return zone;
|
|
92
|
+
},
|
|
93
|
+
error => {
|
|
94
|
+
console.error("Erreur de décodage audio:", error);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
} else {
|
|
99
|
+
this.applyZoneParameters(zone);
|
|
100
|
+
return zone;
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
applyZoneParameters = (zone) => {
|
|
105
|
+
zone.loopStart = this.numValue(zone.loopStart, 0);
|
|
106
|
+
zone.loopEnd = this.numValue(zone.loopEnd, 0);
|
|
107
|
+
zone.coarseTune = this.numValue(zone.coarseTune, 0);
|
|
108
|
+
zone.fineTune = this.numValue(zone.fineTune, 0);
|
|
109
|
+
zone.originalPitch = this.numValue(zone.originalPitch, 6000);
|
|
110
|
+
zone.sampleRate = this.numValue(zone.sampleRate, 44100);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
queueWaveTable(when, pitch, duration, volume, slides) {
|
|
114
|
+
this.resumeContext(this.#audioCtx);
|
|
115
|
+
const vol = this.limitVolume(volume);
|
|
116
|
+
const zone = this.findZone(this.#audioCtx, this.#preset, pitch);
|
|
117
|
+
|
|
118
|
+
if (!zone?.buffer) return null;
|
|
119
|
+
|
|
120
|
+
const baseDetune = zone.originalPitch - 100.0 * zone.coarseTune - zone.fineTune;
|
|
121
|
+
const playbackRate = Math.pow(2, (100.0 * pitch - baseDetune) / 1200.0);
|
|
122
|
+
const startWhen = Math.max(when, this.#audioCtx.currentTime);
|
|
123
|
+
let waveDuration = duration + this.afterTime;
|
|
124
|
+
|
|
125
|
+
const loop = zone.loopStart >= 1 && zone.loopStart < zone.loopEnd;
|
|
126
|
+
if (!loop) {
|
|
127
|
+
waveDuration = Math.min(waveDuration, zone.buffer.duration / playbackRate);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const envelope = this.findEnvelope(this.#audioCtx, this.#audioCtx.destination);
|
|
131
|
+
this.setupEnvelope(this.#audioCtx, envelope, zone, vol, startWhen, waveDuration, duration);
|
|
132
|
+
|
|
133
|
+
const source = this.#audioCtx.createBufferSource();
|
|
134
|
+
source.buffer = zone.buffer;
|
|
135
|
+
source.playbackRate.setValueAtTime(playbackRate, 0);
|
|
136
|
+
|
|
137
|
+
if (slides?.length > 0) {
|
|
138
|
+
source.playbackRate.setValueAtTime(playbackRate, startWhen);
|
|
139
|
+
slides.forEach(s => {
|
|
140
|
+
const newRate = Math.pow(2, (100.0 * (pitch + s.delta) - baseDetune) / 1200.0);
|
|
141
|
+
source.playbackRate.linearRampToValueAtTime(newRate, startWhen + s.when);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
source.loop = loop;
|
|
146
|
+
if (loop) {
|
|
147
|
+
const d = zone.delay ?? 0;
|
|
148
|
+
source.loopStart = zone.loopStart / zone.sampleRate + d;
|
|
149
|
+
source.loopEnd = zone.loopEnd / zone.sampleRate + d;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
source.connect(envelope);
|
|
153
|
+
source.start(startWhen, zone.delay ?? 0);
|
|
154
|
+
source.stop(startWhen + waveDuration);
|
|
155
|
+
|
|
156
|
+
envelope.audioBufferSourceNode = source;
|
|
157
|
+
envelope.when = startWhen;
|
|
158
|
+
envelope.duration = waveDuration;
|
|
159
|
+
|
|
160
|
+
return envelope;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
setupEnvelope(audioContext, envelope, zone, volume, when, sampleDuration, noteDuration) {
|
|
164
|
+
envelope.gain.setValueAtTime(this.noZeroVolume(0), audioContext.currentTime);
|
|
165
|
+
|
|
166
|
+
const duration = Math.min(noteDuration, sampleDuration - this.afterTime);
|
|
167
|
+
const ahdsr = (zone.ahdsr && zone.ahdsr.length > 0) ? zone.ahdsr : [
|
|
168
|
+
{ duration: 0, volume: 1 },
|
|
169
|
+
{ duration: duration, volume: 1 }
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
envelope.gain.cancelScheduledValues(when);
|
|
173
|
+
const initialVol = (ahdsr[0]?.volume ?? 1) * volume;
|
|
174
|
+
envelope.gain.setValueAtTime(this.noZeroVolume(initialVol), when);
|
|
175
|
+
|
|
176
|
+
let lastTime = 0;
|
|
177
|
+
let lastVolume = ahdsr[0]?.volume ?? 1;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < ahdsr.length; i++) {
|
|
180
|
+
const stage = ahdsr[i];
|
|
181
|
+
if (stage.duration > 0) {
|
|
182
|
+
if (stage.duration + lastTime > duration) {
|
|
183
|
+
const r = 1 - (stage.duration + lastTime - duration) / stage.duration;
|
|
184
|
+
const n = lastVolume - r * (lastVolume - stage.volume);
|
|
185
|
+
envelope.gain.linearRampToValueAtTime(this.noZeroVolume(volume * n), when + duration);
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
lastTime += stage.duration;
|
|
189
|
+
lastVolume = stage.volume;
|
|
190
|
+
envelope.gain.linearRampToValueAtTime(this.noZeroVolume(volume * lastVolume), when + lastTime);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
envelope.gain.linearRampToValueAtTime(this.noZeroVolume(0), when + duration + this.afterTime);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
findEnvelope(audioContext, target) {
|
|
197
|
+
let envelope = this.envelopes.find(e =>
|
|
198
|
+
e.target === target && audioContext.currentTime > e.when + e.duration + 0.001
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if (envelope) {
|
|
202
|
+
if (envelope.audioBufferSourceNode) {
|
|
203
|
+
try { envelope.audioBufferSourceNode.stop(0); envelope.audioBufferSourceNode.disconnect(); } catch (e) { }
|
|
204
|
+
envelope.audioBufferSourceNode = null;
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
envelope = audioContext.createGain();
|
|
208
|
+
envelope.target = target;
|
|
209
|
+
envelope.connect(target);
|
|
210
|
+
envelope.cancel = () => {
|
|
211
|
+
if (envelope.when + envelope.duration > audioContext.currentTime) {
|
|
212
|
+
envelope.gain.cancelScheduledValues(0);
|
|
213
|
+
envelope.gain.setTargetAtTime(0.00001, audioContext.currentTime, 0.1);
|
|
214
|
+
envelope.when = audioContext.currentTime + 0.00001;
|
|
215
|
+
envelope.duration = 0;
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
this.envelopes.push(envelope);
|
|
219
|
+
}
|
|
220
|
+
return envelope;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
findZone = (audioContext, preset, pitch) => {
|
|
224
|
+
const zone = preset.zones.findLast(z => pitch >= z.keyRangeLow && pitch <= z.keyRangeHigh + 1);
|
|
225
|
+
if (zone) this.adjustZone(audioContext, zone);
|
|
226
|
+
return zone;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
limitVolume = (v) => {
|
|
230
|
+
const requestedVolume = v ? 1.0 * v : 0.5;
|
|
231
|
+
return Math.min(requestedVolume, 0.8);
|
|
232
|
+
};
|
|
233
|
+
noZeroVolume = (n) => n > this.nearZero ? n : this.nearZero;
|
|
234
|
+
numValue = (a, b) => typeof a === "number" ? a : b;
|
|
235
|
+
|
|
236
|
+
resumeContext(audioContext) {
|
|
237
|
+
if (audioContext.state === 'suspended') audioContext.resume().catch(() => { });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
queueChord(ctx, tgt, prst, w, pchs, d, v, s) {
|
|
241
|
+
const vol = this.limitVolume(v);
|
|
242
|
+
return pchs.map((p, i) => this.queueWaveTable(ctx, tgt, prst, w, p, d, vol - Math.random() * 0.01, s?.[i])).filter(Boolean);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
cancelQueue(audioContext) {
|
|
246
|
+
this.envelopes.forEach(e => {
|
|
247
|
+
e.gain.cancelScheduledValues(0);
|
|
248
|
+
e.gain.setValueAtTime(this.nearZero, audioContext.currentTime);
|
|
249
|
+
e.when = -1;
|
|
250
|
+
try { e.audioBufferSourceNode?.disconnect(); } catch (e) { }
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export default WebAudioFontPlayer;
|