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 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;