spessasynth_core
Version:
MIDI and SoundFont2/DLS library with no compromises
317 lines (283 loc) • 9.28 kB
JavaScript
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 { SpessaSynthInfo, SpessaSynthWarn } from "../../utils/loggin.js";
import { readBytesAsString } from "../../utils/byte_functions/string.js";
import { BasicSample } from "../basic_soundfont/basic_sample.js";
import { consoleColors } from "../../utils/other.js";
/**
* samples.js
* purpose: parses soundfont samples
*/
export const SF3_BIT_FLIT = 0x10;
export class SoundFontSample extends BasicSample
{
/**
* Linked sample index for retrieving linked samples in sf2
* @type {number}
*/
linkedSampleIndex;
/**
* The sliced sample from the smpl chunk
* @type {Uint8Array}
*/
s16leData;
/**
* 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.dataOverriden = false;
this.isCompressed = compressed;
this.sampleName = sampleName;
// in bytes
this.startByteOffset = sampleStartIndex;
this.endByteOffset = sampleEndIndex;
this.sampleID = sampleIndex;
const smplStart = sampleDataArray.currentIndex;
// three data types in:
// SF2 (s16le)
// SF3 (vorbis)
// SF2Pack (
if (this.isCompressed)
{
// correct loop points
this.sampleLoopStartIndex += this.startByteOffset / 2;
this.sampleLoopEndIndex += this.startByteOffset / 2;
// copy the compressed data, it can be preserved during writing
this.compressedData = sampleDataArray.slice(
this.startByteOffset / 2 + smplStart,
this.endByteOffset / 2 + smplStart
);
}
else
{
if (sampleDataArray instanceof Float32Array)
{
// float32 array from SF2pack, copy directly
this.sampleData = sampleDataArray.slice(
this.startByteOffset / 2,
this.endByteOffset / 2
);
this.dataOverriden = true;
}
else
{
// regular sf2 s16le
this.s16leData = sampleDataArray.slice(
smplStart + this.startByteOffset,
smplStart + this.endByteOffset
);
}
}
this.linkedSampleIndex = linkedSampleIndex;
}
/**
* @param samplesArray {BasicSample[]}
*/
getLinkedSample(samplesArray)
{
if (this.linkedSample || !this.isLinked)
{
return;
}
const linked = samplesArray[this.linkedSampleIndex];
if (!linked)
{
// log as info because it's common and not really dangerous
SpessaSynthInfo(`%cInvalid linked sample for ${this.sampleName}. Setting to mono.`, consoleColors.warn);
this.unlinkSample();
}
else
{
// check for corrupted files (like FluidR3_GM.sf2 that link EVERYTHING to a single sample)
if (linked.linkedSample)
{
SpessaSynthInfo(
`%cInvalid linked sample for ${this.sampleName}: Already linked to ${linked.linkedSample.sampleName}`,
consoleColors.warn
);
this.unlinkSample();
}
else
{
this.setLinkedSample(linked, this.sampleType);
}
}
}
/**
* @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
// SF3 is decoded in BasicSample
if (this.isCompressed)
{
return super.getAudioData();
}
// start loading data if it is not loaded
const byteLength = this.endByteOffset - this.startByteOffset;
if (byteLength < 1)
{
SpessaSynthWarn(`Invalid sample ${this.sampleName}! Invalid length: ${byteLength}`);
return new Float32Array(1);
}
// SF2
// read the sample data
let audioData = new Float32Array(byteLength / 2);
let convertedSigned16 = new Int16Array(
this.s16leData.buffer
);
// 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)
{
if (this.dataOverriden || this.compressedData)
{
// return vorbis or encode manually
return super.getRawData(allowVorbis);
}
// copy the smpl directly
return this.s16leData;
}
}
/**
* 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
);
}