KEXEL's picture
1.1
b0bfea8 verified
/**
* samples.js
* purpose: parses soundfont samples, resamples if needed.
* loads sample data, handles async loading of sf3 compressed samples
*/
import { SpessaSynthWarn } from "../../utils/loggin.js";
// should be reasonable for most cases
const RESAMPLE_RATE = 48000;
export class BasicSample
{
/**
* The sample's name
* @type {string}
*/
sampleName;
/**
* Sample rate in Hz
* @type {number}
*/
sampleRate;
/**
* Original pitch of the sample as a MIDI note number
* @type {number}
*/
samplePitch;
/**
* Pitch correction, in cents. Can be negative
* @type {number}
*/
samplePitchCorrection;
/**
* Sample link, currently unused here
* @type {number}
*/
sampleLink;
/**
* Type of the sample, currently only used for SF3
* @type {number}
*/
sampleType;
/**
* Relative to the start of the sample in sample points
* @type {number}
*/
sampleLoopStartIndex;
/**
* Relative to the start of the sample in sample points
* @type {number}
*/
sampleLoopEndIndex;
/**
* Indicates if the sample is compressed
* @type {boolean}
*/
isCompressed;
/**
* The compressed sample data if it was compressed by spessasynth
* @type {Uint8Array}
*/
compressedData = undefined;
/**
* The sample's use count
* @type {number}
*/
useCount = 0;
/**
* The sample's audio data
* @type {Float32Array}
*/
sampleData = undefined;
/**
* The basic representation of a soundfont sample
* @param sampleName {string} The sample's name
* @param sampleRate {number} The sample's rate in Hz
* @param samplePitch {number} The sample's pitch as a MIDI note number
* @param samplePitchCorrection {number} The sample's pitch correction in cents
* @param sampleLink {number} The sample's link, currently unused
* @param sampleType {number} The sample's type, an enum
* @param loopStart {number} The sample's loop start relative to the sample start in sample points
* @param loopEnd {number} The sample's loop end relative to the sample start in sample points
*/
constructor(
sampleName,
sampleRate,
samplePitch,
samplePitchCorrection,
sampleLink,
sampleType,
loopStart,
loopEnd
)
{
this.sampleName = sampleName;
this.sampleRate = sampleRate;
this.samplePitch = samplePitch;
this.samplePitchCorrection = samplePitchCorrection;
this.sampleLink = sampleLink;
this.sampleType = sampleType;
this.sampleLoopStartIndex = loopStart;
this.sampleLoopEndIndex = loopEnd;
// https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format
this.isCompressed = (sampleType & 0x10) > 0;
}
/**
* @returns {Uint8Array|IndexedByteArray}
*/
getRawData()
{
const uint8 = new Uint8Array(this.sampleData.length * 2);
for (let i = 0; i < this.sampleData.length; i++)
{
const sample = Math.floor(this.sampleData[i] * 32768);
uint8[i * 2] = sample & 0xFF; // lower byte
uint8[i * 2 + 1] = (sample >> 8) & 0xFF; // upper byte
}
return uint8;
}
resampleData(newSampleRate)
{
let audioData = this.getAudioData();
const ratio = newSampleRate / this.sampleRate;
const resampled = new Float32Array(Math.floor(audioData.length * ratio));
for (let i = 0; i < resampled.length; i++)
{
resampled[i] = audioData[Math.floor(i * (1 / ratio))];
}
audioData = resampled;
this.sampleRate = newSampleRate;
// adjust loop points
this.sampleLoopStartIndex = Math.floor(this.sampleLoopStartIndex * ratio);
this.sampleLoopEndIndex = Math.floor(this.sampleLoopEndIndex * ratio);
this.sampleData = audioData;
}
/**
* @param quality {number}
* @param encodeVorbis {EncodeVorbisFunction}
*/
compressSample(quality, encodeVorbis)
{
// no need to compress
if (this.isCompressed)
{
return;
}
// compress, always mono!
try
{
// if the sample rate is too low or too high, resample
let audioData = this.getAudioData();
if (this.sampleRate < 8000 || this.sampleRate > 96000)
{
this.resampleData(RESAMPLE_RATE);
audioData = this.getAudioData();
}
this.compressedData = encodeVorbis([audioData], 1, this.sampleRate, quality);
// flag as compressed
this.sampleType |= 0x10;
this.isCompressed = true;
}
catch (e)
{
SpessaSynthWarn(`Failed to compress ${this.sampleName}. Leaving as uncompressed!`);
this.isCompressed = false;
this.compressedData = undefined;
// flag as uncompressed
this.sampleType &= 0xEF;
}
}
/**
* @returns {Float32Array}
*/
getAudioData()
{
return this.sampleData;
}
}