spessasynth_core 1.0.11 → 1.1.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/README.md +87 -4
- package/package.json +1 -1
- package/spessasynth_core/midi_parser/midi_loader.js +7 -2
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/controller_control.js +13 -9
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_off.js +8 -8
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/note_on.js +5 -5
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/program_control.js +7 -7
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/system_exclusive.js +2 -2
- package/spessasynth_core/synthetizer/worklet_system/worklet_methods/tuning_control.js +9 -10
- package/spessasynth_core/synthetizer/worklet_system/worklet_utilities/worklet_processor_channel.js +1 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
A SoundFont2 synthesizer library, made for use with node.js.
|
|
3
3
|
A fork of [SpessaSynth](https://github.com/spessasus/SpessaSynth).
|
|
4
4
|
|
|
5
|
-
[Jump to
|
|
5
|
+
[Jump to the API reference](#api-reference)
|
|
6
6
|
|
|
7
7
|
`npm install --save spessasynth_core`
|
|
8
8
|
|
|
@@ -19,7 +19,7 @@ A fork of [SpessaSynth](https://github.com/spessasus/SpessaSynth).
|
|
|
19
19
|
- No SoundFont size limit
|
|
20
20
|
- No dependencies
|
|
21
21
|
|
|
22
|
-
### Example:
|
|
22
|
+
### Example: Render a MIDI file to a .wav file
|
|
23
23
|
```js
|
|
24
24
|
const fs = require('fs');
|
|
25
25
|
// spessasynth_core is an es6 module
|
|
@@ -53,6 +53,89 @@ import("spessasynth_core").then(core => {
|
|
|
53
53
|
});
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
### Example 2: play a MIDI file to speakers
|
|
57
|
+
via npm package `speaker`
|
|
58
|
+
```js
|
|
59
|
+
const fs = require('fs');
|
|
60
|
+
const Speaker = require("speaker");
|
|
61
|
+
const SAMPLE_RATE = 44100; // hertz
|
|
62
|
+
const BLOCK_SIZE = 128; // samples
|
|
63
|
+
|
|
64
|
+
// usage: node test.js <sf path> <midi path>
|
|
65
|
+
import("spessasynth_core").then(core => {
|
|
66
|
+
// Disable logging
|
|
67
|
+
core.SpessaSynthLogging(false, false, false, false);
|
|
68
|
+
|
|
69
|
+
// Read arguments and load the input files
|
|
70
|
+
const [,, soundfontName, midiName] = process.argv;
|
|
71
|
+
const soundfont = fs.readFileSync(soundfontName);
|
|
72
|
+
const mid = new core.MIDI(fs.readFileSync(midiName)); // parse the midi
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
// Initialize synth and sequencer
|
|
76
|
+
const synth = new core.Synthesizer(soundfont, SAMPLE_RATE, BLOCK_SIZE);
|
|
77
|
+
const seq = new core.Sequencer(synth);
|
|
78
|
+
|
|
79
|
+
// Load new song and disable the loop
|
|
80
|
+
seq.loadNewSongList([mid]);
|
|
81
|
+
seq.loop = false;
|
|
82
|
+
|
|
83
|
+
// make the program stop after the sequence is finished
|
|
84
|
+
const time = seq.duration + 1
|
|
85
|
+
let isPlaying = true;
|
|
86
|
+
setTimeout(() => isPlaying = false, time * 1000);
|
|
87
|
+
console.log(`Playing "${mid.midiName || "<unnamed song>"}"`);
|
|
88
|
+
|
|
89
|
+
// Calculate length and allocate buffers
|
|
90
|
+
const outLeft = new Float32Array(BLOCK_SIZE);
|
|
91
|
+
const outRight = new Float32Array(BLOCK_SIZE);
|
|
92
|
+
|
|
93
|
+
// Wait for sf3 support to load and render
|
|
94
|
+
synth.sf3supportReady.then(() => {
|
|
95
|
+
const speakerOut = new Speaker({
|
|
96
|
+
channels: 2,
|
|
97
|
+
bitDepth: 16,
|
|
98
|
+
sampleRate: SAMPLE_RATE,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
function render()
|
|
102
|
+
{
|
|
103
|
+
// Render audio samples from the synthesizer
|
|
104
|
+
synth.render([outLeft, outRight]);
|
|
105
|
+
|
|
106
|
+
// Prepare buffer for WAV output
|
|
107
|
+
const wavData = new Int16Array(outLeft.length * 2); // 2 channels
|
|
108
|
+
|
|
109
|
+
// Interleave audio data
|
|
110
|
+
let offset = 0;
|
|
111
|
+
for (let i = 0; i < outLeft.length; i++)
|
|
112
|
+
{
|
|
113
|
+
// Float ranges from -1 to 1, int16 ranges from -32768 to 32767, convert it here
|
|
114
|
+
const sampleL = Math.max(-1, Math.min(1, outLeft[i])) * 32767;
|
|
115
|
+
const sampleR = Math.max(-1, Math.min(1, outRight[i])) * 32767;
|
|
116
|
+
|
|
117
|
+
// Interleave data: L, R, L, R, etc...
|
|
118
|
+
wavData[offset++] = sampleL;
|
|
119
|
+
wavData[offset++] = sampleR;
|
|
120
|
+
}
|
|
121
|
+
// Write WAV data to speaker
|
|
122
|
+
speakerOut.write(wavData);
|
|
123
|
+
|
|
124
|
+
if(isPlaying)
|
|
125
|
+
{
|
|
126
|
+
setImmediate(render);
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
{
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
render();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
56
139
|
## API reference
|
|
57
140
|
### Contents
|
|
58
141
|
- [Importing the library](#importing-the-library)
|
|
@@ -207,12 +290,12 @@ synth.muteChannel(channel, isMuted);
|
|
|
207
290
|
- channel - the channel to mute/unmute. Usually ranges from 0 to 15, but it depends on the channel count.
|
|
208
291
|
- isMuted - if the channel should be muted. boolean.
|
|
209
292
|
|
|
210
|
-
###
|
|
293
|
+
### transposeAllChannels
|
|
211
294
|
|
|
212
295
|
Transposes the synth up or down in semitones. Floating point values can be used for more precise tuning.
|
|
213
296
|
|
|
214
297
|
```js
|
|
215
|
-
synth.
|
|
298
|
+
synth.transposeAllChannels(semitones);
|
|
216
299
|
```
|
|
217
300
|
|
|
218
301
|
- semitones - the amount of semitones to transpose the synth by. Can be positive or negative or zero. Zero resets the pitch.
|
package/package.json
CHANGED
|
@@ -36,8 +36,8 @@ export class MIDI{
|
|
|
36
36
|
throw new RangeError(`Invalid MIDI header chunk size! Expected 6, got ${headerChunk.size}`);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
// format
|
|
40
|
-
readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
39
|
+
// format
|
|
40
|
+
const format = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
41
41
|
// tracks count
|
|
42
42
|
this.tracksAmount = readBytesAsUintBigEndian(headerChunk.data, 2);
|
|
43
43
|
// time division
|
|
@@ -95,6 +95,11 @@ export class MIDI{
|
|
|
95
95
|
let runningByte = undefined;
|
|
96
96
|
|
|
97
97
|
let totalTicks = 0;
|
|
98
|
+
// format 2 plays sequentially
|
|
99
|
+
if(format === 2 && i > 0)
|
|
100
|
+
{
|
|
101
|
+
totalTicks += this.tracks[i - 1][this.tracks[i - 1].length - 1].ticks;
|
|
102
|
+
}
|
|
98
103
|
// loop until we reach the end of track
|
|
99
104
|
while(trackChunk.data.currentIndex < trackChunk.size)
|
|
100
105
|
{
|
|
@@ -12,9 +12,10 @@ import { SpessaSynthInfo } from '../../../utils/loggin.js'
|
|
|
12
12
|
import { SYNTHESIZER_GAIN } from '../../synthesizer.js'
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* @param
|
|
17
|
-
* @param
|
|
15
|
+
* Changes a MIDI controller
|
|
16
|
+
* @param channel {number} - The MIDI Channel to use
|
|
17
|
+
* @param controllerNumber {number} - The MIDI controller number
|
|
18
|
+
* @param controllerValue {number} - The new value
|
|
18
19
|
* @this {Synthesizer}
|
|
19
20
|
*/
|
|
20
21
|
export function controllerChange(channel, controllerNumber, controllerValue)
|
|
@@ -141,6 +142,7 @@ export function controllerChange(channel, controllerNumber, controllerValue)
|
|
|
141
142
|
}
|
|
142
143
|
|
|
143
144
|
/**
|
|
145
|
+
* Resets all controllers and programs
|
|
144
146
|
* @this {Synthesizer}
|
|
145
147
|
*/
|
|
146
148
|
export function resetAllControllers()
|
|
@@ -176,7 +178,7 @@ export function resetAllControllers()
|
|
|
176
178
|
|
|
177
179
|
/**
|
|
178
180
|
* Resets all controllers for channel
|
|
179
|
-
* @param channel {number}
|
|
181
|
+
* @param channel {number} - The MIDI Channel to use
|
|
180
182
|
* @this {Synthesizer}
|
|
181
183
|
*/
|
|
182
184
|
export function resetControllers(channel)
|
|
@@ -248,7 +250,8 @@ export function resetParameters(channel)
|
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
/**
|
|
251
|
-
*
|
|
253
|
+
* Sets the main volume of the synthesizer
|
|
254
|
+
* @param volume {number} - 0-1 the volume
|
|
252
255
|
* @this {Synthesizer}
|
|
253
256
|
*/
|
|
254
257
|
export function setMainVolume(volume)
|
|
@@ -258,10 +261,10 @@ export function setMainVolume(volume)
|
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
/**
|
|
261
|
-
*
|
|
264
|
+
* Sets the master stereo panning of the synthesizer
|
|
265
|
+
* @param pan {number} - -1 to 1 the pan, -1 is left, 0 is the middle, 1 is right
|
|
262
266
|
* @this {Synthesizer}
|
|
263
267
|
*/
|
|
264
|
-
|
|
265
268
|
export function setMasterPan(pan)
|
|
266
269
|
{
|
|
267
270
|
this.pan = pan;
|
|
@@ -272,8 +275,9 @@ export function setMasterPan(pan)
|
|
|
272
275
|
}
|
|
273
276
|
|
|
274
277
|
/**
|
|
275
|
-
*
|
|
276
|
-
* @param
|
|
278
|
+
* Mutes/unmutes a channel
|
|
279
|
+
* @param channel {number} - The MIDI channel to use
|
|
280
|
+
* @param isMuted {boolean} - If the channel should be muted or not
|
|
277
281
|
* @this {Synthesizer}
|
|
278
282
|
*/
|
|
279
283
|
export function muteChannel(channel, isMuted)
|
|
@@ -3,9 +3,9 @@ import { consoleColors } from '../../../utils/other.js'
|
|
|
3
3
|
import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
* @param channel {number}
|
|
8
|
-
* @param midiNote {number}
|
|
6
|
+
* Releases a note
|
|
7
|
+
* @param channel {number} - The MIDI channel to use
|
|
8
|
+
* @param midiNote {number} - The MIDI key number
|
|
9
9
|
* @this {Synthesizer}
|
|
10
10
|
*/
|
|
11
11
|
export function noteOff(channel, midiNote)
|
|
@@ -46,8 +46,8 @@ export function noteOff(channel, midiNote)
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Stops a note nearly instantly
|
|
49
|
-
* @param channel {number}
|
|
50
|
-
* @param midiNote {number}
|
|
49
|
+
* @param channel {number} - The MIDI channel to use
|
|
50
|
+
* @param midiNote {number} - The MIDI key number
|
|
51
51
|
* @this {Synthesizer}
|
|
52
52
|
*/
|
|
53
53
|
export function killNote(channel, midiNote)
|
|
@@ -64,8 +64,8 @@ export function killNote(channel, midiNote)
|
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* stops all notes
|
|
67
|
-
* @param channel {number}
|
|
68
|
-
* @param force {boolean}
|
|
67
|
+
* @param channel {number} - The MIDI channel to use
|
|
68
|
+
* @param force {boolean} - If the notes should stop instantly or release normally
|
|
69
69
|
* @this {Synthesizer}
|
|
70
70
|
*/
|
|
71
71
|
export function stopAll(channel, force = false)
|
|
@@ -91,7 +91,7 @@ export function stopAll(channel, force = false)
|
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
93
|
* @this {Synthesizer}
|
|
94
|
-
* @param force {boolean}
|
|
94
|
+
* @param force {boolean} - If the notes should stop instantly or release normally
|
|
95
95
|
*/
|
|
96
96
|
export function stopAllChannels(force = false)
|
|
97
97
|
{
|
|
@@ -5,11 +5,11 @@ import { VOICE_CAP } from "../../synthesizer.js";
|
|
|
5
5
|
import { SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
9
|
-
* @param channel {number}
|
|
10
|
-
* @param midiNote {number}
|
|
11
|
-
* @param velocity {number}
|
|
12
|
-
* @param enableDebugging {boolean}
|
|
8
|
+
* Starts playing a MIDI note
|
|
9
|
+
* @param channel {number} - The MIDI Channel to use
|
|
10
|
+
* @param midiNote {number} - The MIDI key number
|
|
11
|
+
* @param velocity {number} - The velocity (how hard is the key pressed)
|
|
12
|
+
* @param enableDebugging {boolean} - used internally, ignore
|
|
13
13
|
* @this {Synthesizer}
|
|
14
14
|
*/
|
|
15
15
|
export function noteOn(channel, midiNote, velocity, enableDebugging = false)
|
|
@@ -5,12 +5,11 @@ import { generatorTypes } from '../../../soundfont/chunk/generators.js'
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* executes a program change
|
|
8
|
-
* @param channel {number}
|
|
9
|
-
* @param programNumber {number}
|
|
10
|
-
* @param userChange {boolean}
|
|
8
|
+
* @param channel {number} - The MIDI Channel to use
|
|
9
|
+
* @param programNumber {number} - The MIDI program number
|
|
11
10
|
* @this {Synthesizer}
|
|
12
11
|
*/
|
|
13
|
-
export function programChange(channel, programNumber
|
|
12
|
+
export function programChange(channel, programNumber)
|
|
14
13
|
{
|
|
15
14
|
/**
|
|
16
15
|
* @type {WorkletProcessorChannel}
|
|
@@ -48,8 +47,8 @@ export function setPreset(channel, preset)
|
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* Toggles drums on a given channel
|
|
51
|
-
* @param channel {number}
|
|
52
|
-
* @param isDrum {boolean}
|
|
50
|
+
* @param channel {number} - The MIDI Channel to use
|
|
51
|
+
* @param isDrum {boolean} - boolean, if the channel should be drums
|
|
53
52
|
* @this {Synthesizer}
|
|
54
53
|
*/
|
|
55
54
|
export function setDrums(channel, isDrum)
|
|
@@ -68,7 +67,8 @@ export function setDrums(channel, isDrum)
|
|
|
68
67
|
}
|
|
69
68
|
|
|
70
69
|
/**
|
|
71
|
-
*
|
|
70
|
+
* Reloads the soundfont, stops all voices
|
|
71
|
+
* @param buffer {ArrayBuffer} - the new soundfont buffer
|
|
72
72
|
* @this {Synthesizer}
|
|
73
73
|
*/
|
|
74
74
|
export function reloadSoundFont(buffer)
|
|
@@ -2,8 +2,8 @@ import { arrayToHexString, consoleColors } from '../../../utils/other.js'
|
|
|
2
2
|
import { SpessaSynthInfo, SpessaSynthWarn } from '../../../utils/loggin.js'
|
|
3
3
|
/**
|
|
4
4
|
* Executes a system exclusive
|
|
5
|
-
* @param messageData {number[]} -
|
|
6
|
-
* @param channelOffset {number}
|
|
5
|
+
* @param messageData {number[]} - The binary message data without f0
|
|
6
|
+
* @param channelOffset {number} - Channel offset (for midi ports). Defaults to 0.
|
|
7
7
|
* @this {Synthesizer}
|
|
8
8
|
*/
|
|
9
9
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
1
|
+
import {customControllers, NON_CC_INDEX_OFFSET} from '../worklet_utilities/worklet_processor_channel.js'
|
|
2
|
+
import {consoleColors} from '../../../utils/other.js'
|
|
3
|
+
import {modulatorSources} from '../../../soundfont/chunk/modulators.js'
|
|
4
|
+
import {computeModulators} from '../worklet_utilities/worklet_modulator.js'
|
|
5
|
+
import {SpessaSynthInfo} from '../../../utils/loggin.js'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Transposes all channels by given amount of semitones
|
|
@@ -22,9 +22,9 @@ export function transposeAllChannels(semitones, force = false)
|
|
|
22
22
|
/**
|
|
23
23
|
* Transposes the channel by given amount of semitones
|
|
24
24
|
* @this {Synthesizer}
|
|
25
|
-
* @param channel {number}
|
|
26
|
-
* @param semitones {number} Can be float
|
|
27
|
-
* @param force {boolean} defaults to false, if true transposes the channel even if it's a drum channel
|
|
25
|
+
* @param channel {number} - The MIDI Channel to use
|
|
26
|
+
* @param semitones {number} - Can be a float
|
|
27
|
+
* @param force {boolean} - defaults to false, if true transposes the channel even if it's a drum channel
|
|
28
28
|
*/
|
|
29
29
|
export function transposeChannel(channel, semitones, force=false)
|
|
30
30
|
{
|
|
@@ -99,7 +99,6 @@ export function setModulationDepth(channel, cents)
|
|
|
99
99
|
*/
|
|
100
100
|
export function pitchWheel(channel, MSB, LSB)
|
|
101
101
|
{
|
|
102
|
-
|
|
103
|
-
this.workletProcessorChannels[channel].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = bend;
|
|
102
|
+
this.workletProcessorChannels[channel].midiControllers[NON_CC_INDEX_OFFSET + modulatorSources.pitchWheel] = (LSB | (MSB << 7));
|
|
104
103
|
this.workletProcessorChannels[channel].voices.forEach(v => computeModulators(v, this.workletProcessorChannels[channel].midiControllers));
|
|
105
104
|
}
|