UNPKG

spessasynth_core

Version:

MIDI and SoundFont2/DLS library with no compromises

368 lines (334 loc) 11 kB
import { RiffChunk } from "../basic_soundfont/riff_chunk.js"; import { IndexedByteArray } from "../../utils/indexed_array.js"; import { readLittleEndian, signedInt8 } from "../../utils/byte_functions/little_endian.js"; import { stbvorbis } from "../../externals/stbvorbis_sync/stbvorbis_sync.min.js"; import { SpessaSynthInfo, SpessaSynthWarn } from "../../utils/loggin.js"; import { readBytesAsString } from "../../utils/byte_functions/string.js"; import { BasicSample, sampleTypes } from "../basic_soundfont/basic_sample.js"; import { consoleColors } from "../../utils/other.js"; export const SF3_BIT_FLIT = 0x10; export class SoundFontSample extends BasicSample { /** * Linked sample index for retrieving linked samples in sf2 * @type {number} */ linkedSampleIndex; /** * The handle to the core sf2 file for dynamic sample reading * @type {Uint8Array} */ sf2FileArrayHandle; /** * Start index of the sample in the file byte array * @type {number} */ s16leStart = 0; /** * End index of the sample in the file byte array * @type {number} */ s16leEnd = 0; /** * Creates a sample * @param sampleName {string} * @param sampleStartIndex {number} * @param sampleEndIndex {number} * @param sampleLoopStartIndex {number} * @param sampleLoopEndIndex {number} * @param sampleRate {number} * @param samplePitch {number} * @param samplePitchCorrection {number} * @param linkedSampleIndex {number} * @param sampleType {number} * @param sampleDataArray {IndexedByteArray|Float32Array} * @param sampleIndex {number} initial sample index when loading the sfont * Used for SF2Pack support */ constructor( sampleName, sampleStartIndex, sampleEndIndex, sampleLoopStartIndex, sampleLoopEndIndex, sampleRate, samplePitch, samplePitchCorrection, linkedSampleIndex, sampleType, sampleDataArray, sampleIndex ) { // read sf3 // https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format const compressed = (sampleType & SF3_BIT_FLIT) > 0; // remove the compression flag sampleType &= ~SF3_BIT_FLIT; super( sampleName, sampleRate, samplePitch, samplePitchCorrection, sampleType, sampleLoopStartIndex - (sampleStartIndex / 2), sampleLoopEndIndex - (sampleStartIndex / 2) ); this.isCompressed = compressed; this.sampleName = sampleName; // in bytes this.sampleStartIndex = sampleStartIndex; this.sampleEndIndex = sampleEndIndex; this.sampleID = sampleIndex; // in bytes this.sampleLength = this.sampleEndIndex - this.sampleStartIndex; const smplStart = sampleDataArray.currentIndex; // three data types in: // SF2 (s16le) // SF3 (vorbis) // SF2Pack ( if (this.isCompressed) { // correct loop points this.sampleLoopStartIndex += this.sampleStartIndex / 2; this.sampleLoopEndIndex += this.sampleStartIndex / 2; this.sampleLength = 99999999; // set to 999,999 before we decode it // copy the compressed data, it can be preserved during writing this.compressedData = sampleDataArray.slice( this.sampleStartIndex / 2 + smplStart, this.sampleEndIndex / 2 + smplStart ); } else { if (sampleDataArray instanceof Float32Array) { // float32 array from SF2pack, copy directly this.sampleData = sampleDataArray.slice( this.sampleStartIndex / 2, this.sampleEndIndex / 2 ); } else { // regular sf2 s16le this.s16leStart = smplStart + this.sampleStartIndex; this.s16leEnd = smplStart + this.sampleEndIndex; this.sf2FileArrayHandle = sampleDataArray; } } this.linkedSampleIndex = linkedSampleIndex; } /** * @param samplesArray {BasicSample[]} */ getLinkedSample(samplesArray) { if (this.linkedSample || !this.isLinked) { return; } const linkedSample = samplesArray[this.linkedSampleIndex]; if (!linkedSample) { // log as info because it's common and not really dangerous SpessaSynthInfo(`%cInvalid linked sample for ${this.sampleName}. Setting to mono.`, consoleColors.warn); this.setSampleType(sampleTypes.monoSample); } else { this.setLinkedSample(samplesArray[this.linkedSampleIndex], this.sampleType); } } /** * @private * Decode binary vorbis into a float32 pcm * @returns {Float32Array} */ decodeVorbis() { if (this.sampleData) { return this.sampleData; } if (this.sampleLength < 1) { // eos, do not do anything return new Float32Array(0); } // get the compressed byte stream // reset array and being decoding try { /** * @type {{data: Float32Array[], error: (string|null), sampleRate: number, eof: boolean}} */ const vorbis = stbvorbis.decode(this.compressedData); const decoded = vorbis.data[0]; if (decoded === undefined) { SpessaSynthWarn(`Error decoding sample ${this.sampleName}: Vorbis decode returned undefined.`); return new Float32Array(0); } // clip // because vorbis can go above 1 sometimes for (let i = 0; i < decoded.length; i++) { // magic number is 32,767 / 32,768 decoded[i] = Math.max(-1, Math.min(decoded[i], 0.999969482421875)); } return decoded; } catch (e) { // do not error out, fill with silence SpessaSynthWarn(`Error decoding sample ${this.sampleName}: ${e}`); return new Float32Array(this.sampleLoopEndIndex + 1); } } /** * @param audioData {Float32Array} */ setAudioData(audioData) { super.setAudioData(audioData); } /** * Loads the audio data and stores it for reuse * @returns {Float32Array} The audioData */ getAudioData() { if (this.sampleData) { return this.sampleData; } // SF2Pack is decoded during load time // start loading data if it is not loaded if (this.sampleLength < 1) { SpessaSynthWarn(`Invalid sample ${this.sampleName}! Invalid length: ${this.sampleLength}`); return new Float32Array(1); } if (this.isCompressed) { // SF3 // if compressed, decode this.sampleData = this.decodeVorbis(); return this.sampleData; } // SF2 // read the sample data let audioData = new Float32Array(this.sampleLength / 2); let convertedSigned16 = new Int16Array( this.sf2FileArrayHandle.buffer.slice(this.s16leStart, this.s16leEnd) ); // convert to float for (let i = 0; i < convertedSigned16.length; i++) { audioData[i] = convertedSigned16[i] / 32768; } this.sampleData = audioData; return audioData; } /** * @param allowVorbis * @returns {Uint8Array} */ getRawData(allowVorbis = true) { if (this.dataOverriden) { return this.encodeS16LE(); } else { if (this.compressedData) { if (allowVorbis) { return this.compressedData; } return this.encodeS16LE(); } return this.sf2FileArrayHandle.slice(this.s16leStart, this.s16leEnd); } } } /** * Reads the generatorTranslator from the shdr read * @param sampleHeadersChunk {RiffChunk} * @param smplChunkData {IndexedByteArray|Float32Array} * @param linkSamples {boolean} * @returns {SoundFontSample[]} */ export function readSamples(sampleHeadersChunk, smplChunkData, linkSamples = true) { /** * @type {SoundFontSample[]} */ let samples = []; let index = 0; while (sampleHeadersChunk.chunkData.length > sampleHeadersChunk.chunkData.currentIndex) { const sample = readSample(index, sampleHeadersChunk.chunkData, smplChunkData); samples.push(sample); index++; } // remove EOS samples.pop(); // link samples if (linkSamples) { samples.forEach(s => s.getLinkedSample(samples)); } return samples; } /** * Reads it into a sample * @param index {number} * @param sampleHeaderData {IndexedByteArray} * @param smplArrayData {IndexedByteArray|Float32Array} * @returns {SoundFontSample} */ function readSample(index, sampleHeaderData, smplArrayData) { // read the sample name let sampleName = readBytesAsString(sampleHeaderData, 20); // read the sample start index let sampleStartIndex = readLittleEndian(sampleHeaderData, 4) * 2; // read the sample end index let sampleEndIndex = readLittleEndian(sampleHeaderData, 4) * 2; // read the sample looping start index let sampleLoopStartIndex = readLittleEndian(sampleHeaderData, 4); // read the sample looping end index let sampleLoopEndIndex = readLittleEndian(sampleHeaderData, 4); // read the sample rate let sampleRate = readLittleEndian(sampleHeaderData, 4); // read the original sample pitch let samplePitch = sampleHeaderData[sampleHeaderData.currentIndex++]; if (samplePitch > 127) { // if it's out of range, then default to 60 samplePitch = 60; } // read the sample pitch correction let samplePitchCorrection = signedInt8(sampleHeaderData[sampleHeaderData.currentIndex++]); // read the link to the other channel let sampleLink = readLittleEndian(sampleHeaderData, 2); let sampleType = readLittleEndian(sampleHeaderData, 2); return new SoundFontSample( sampleName, sampleStartIndex, sampleEndIndex, sampleLoopStartIndex, sampleLoopEndIndex, sampleRate, samplePitch, samplePitchCorrection, sampleLink, sampleType, smplArrayData, index ); }