Skip to content

Commit

Permalink
Merge pull request #78 from spessasus/write-dls
Browse files Browse the repository at this point in the history
Add DLS Write Support
  • Loading branch information
spessasus authored Nov 16, 2024
2 parents d76be05 + 2a237ca commit 494588b
Show file tree
Hide file tree
Showing 38 changed files with 1,867 additions and 226 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ It’s a powerful and versatile library that allows you to:
- Play MIDI files using SF2/SF3/DLS files
- Write MIDI files
- Write SF2/SF3 files
- Convert DLS to SF2
- Convert DLS to SF2 (and back)
- [and more!](#easy-integration)

```shell
Expand Down Expand Up @@ -103,14 +103,15 @@ This repository contains both the library and a complete musical web application
- **Variable compression quality:** You choose between file size and quality!
- **Compression preserving:** Avoid decompressing and recompressing uncompressed samples for minimal quality loss!

#### Read and play DLS Level 1 or 2 files
#### Read and write DLS Level 1 or 2 files
- Read DLS (DownLoadable Sounds) files as SF2 files!
- **Works like a normal soundfont:** *Saving it as sf2 is still [just one function!](https://github.com/spessasus/SpessaSynth/wiki/SoundFont2-Class#write)*
- Converts articulators to both **modulators** and **generators**!
- Works with both unsigned 8-bit samples and signed 16-bit samples!
- **Covers special generator cases:** *such as modLfoToPitch*!
- **Correct volume:** *looking at you, Viena and gm.sf2!*
- Support built right into the synthesizer!
- **Convert SF2 to DLS:** [with limitations](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem);

### Export MIDI as WAV
- Save the MIDI file as WAV audio!
Expand All @@ -121,6 +122,7 @@ This repository contains both the library and a complete musical web application

## Limitations
- Synth's performance may be suboptimal, especially on mobile devices.
- [SF2 to DLS Conversion](https://github.com/spessasus/SpessaSynth/wiki/DLS-Conversion-Problem)
- Audio may sometimes sound distored in Chrome, Edge, Brave,
etc. due to a **[Chromium Bug](https://issues.chromium.org/issues/367304685).**
I can't do anything about it, only hope that it gets fixed.
Expand Down Expand Up @@ -169,6 +171,7 @@ npm install --save spessasynth_lib
- Export the modified MIDI file to .mid
- Export the trimmed SoundFont to .sf2
- Export a DLS file to .sf2
- Export an SF2 file to .dls
- Or compress it as .sf3!
- Bundle both as .rmi with metadata such as album cover!
- Comes bundled with a compressed [GeneralUser GS](https://schristiancollins.com/generaluser.php) SoundFont to get you started
Expand Down
35 changes: 23 additions & 12 deletions src/spessasynth_lib/soundfont/basic_soundfont/basic_preset.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,14 @@ export class BasicPreset
return [];
}

function isInRange(min, max, number)
/**
* @param range {SoundFontRange}
* @param number {number}
* @returns {boolean}
*/
function isInRange(range, number)
{
return number >= min && number <= max;
return number >= range.min && number <= range.max;
}

/**
Expand Down Expand Up @@ -182,51 +187,57 @@ export class BasicPreset
*/
let globalPresetGenerators = this.presetZones[0].isGlobal ? [...this.presetZones[0].generators] : [];

/**
* @type {Modulator[]}
*/
let globalPresetModulators = this.presetZones[0].isGlobal ? [...this.presetZones[0].modulators] : [];
const globalKeyRange = this.presetZones[0].isGlobal ? this.presetZones[0].keyRange : { min: 0, max: 127 };
const globalVelRange = this.presetZones[0].isGlobal ? this.presetZones[0].velRange : { min: 0, max: 127 };

// find the preset zones in range
let presetZonesInRange = this.presetZones.filter(currentZone =>
(
isInRange(
currentZone.keyRange.min,
currentZone.keyRange.max,
currentZone.hasKeyRange ? currentZone.keyRange : globalKeyRange,
midiNote
)
&&
isInRange(
currentZone.velRange.min,
currentZone.velRange.max,
currentZone.hasVelRange ? currentZone.velRange : globalVelRange,
velocity
)
) && !currentZone.isGlobal);

presetZonesInRange.forEach(zone =>
{
// global zone is already taken into account earlier
if (zone.instrument.instrumentZones.length < 1)
{
return;
}
let presetGenerators = zone.generators;
let presetModulators = zone.modulators;
const firstZone = zone.instrument.instrumentZones[0];
/**
* global zone is always first, so it or nothing
* @type {Generator[]}
*/
let globalInstrumentGenerators = zone.instrument.instrumentZones[0].isGlobal ? [...zone.instrument.instrumentZones[0].generators] : [];
let globalInstrumentModulators = zone.instrument.instrumentZones[0].isGlobal ? [...zone.instrument.instrumentZones[0].modulators] : [];
let globalInstrumentGenerators = firstZone.isGlobal ? [...firstZone.generators] : [];
let globalInstrumentModulators = firstZone.isGlobal ? [...firstZone.modulators] : [];
const globalKeyRange = firstZone.isGlobal ? firstZone.keyRange : { min: 0, max: 127 };
const globalVelRange = firstZone.isGlobal ? firstZone.velRange : { min: 0, max: 127 };


let instrumentZonesInRange = zone.instrument.instrumentZones
.filter(currentZone =>
(
isInRange(
currentZone.keyRange.min,
currentZone.keyRange.max,
currentZone.hasKeyRange ? currentZone.keyRange : globalKeyRange,
midiNote
)
&&
isInRange(
currentZone.velRange.min,
currentZone.velRange.max,
currentZone.hasVelRange ? currentZone.velRange : globalVelRange,
velocity
)
) && !currentZone.isGlobal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { SpessaSynthWarn } from "../../utils/loggin.js";
import { consoleColors } from "../../utils/other.js";
import { write } from "./write_sf2/write.js";
import { defaultModulators, Modulator } from "./modulator.js";
import { writeDLS } from "./write_dls/write_dls.js";

class BasicSoundFont
{
Expand Down Expand Up @@ -243,5 +244,6 @@ class BasicSoundFont
}

BasicSoundFont.prototype.write = write;
BasicSoundFont.prototype.writeDLS = writeDLS;

export { BasicSoundFont };
35 changes: 30 additions & 5 deletions src/spessasynth_lib/soundfont/basic_soundfont/basic_zone.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,57 @@ export class BasicZone
{
/**
* The zone's velocity range
* min -1 means that it is a default value
* @type {SoundFontRange}
*/
velRange = { min: 0, max: 127 };
velRange = { min: -1, max: 127 };

/**
* The zone's key range
* min -1 means that it is a default value
* @type {SoundFontRange}
*/
keyRange = { min: 0, max: 127 };

keyRange = { min: -1, max: 127 };
/**
* Indicates if the zone is global
* @type {boolean}
*/
isGlobal = false;

/**
* The zone's generators
* @type {Generator[]}
*/
generators = [];

/**
* The zone's modulators
* @type {Modulator[]}
*/
modulators = [];

/**
* @returns {boolean}
*/
get hasKeyRange()
{
return this.keyRange.min !== -1;
}

/**
* @returns {boolean}
*/
get hasVelRange()
{
return this.velRange.min !== -1;
}

/**
* @param generatorType {generatorTypes}
* @param notFoundValue {number}
* @returns {number}
*/
getGeneratorValue(generatorType, notFoundValue)
{
return this.generators.find(g => g.generatorType === generatorType)?.generatorValue || notFoundValue;
}
}

14 changes: 10 additions & 4 deletions src/spessasynth_lib/soundfont/basic_soundfont/generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ generatorLimits[generatorTypes.fineTune] = { min: -12700, max: 12700, def: 0 };
generatorLimits[generatorTypes.scaleTuning] = { min: 0, max: 1200, def: 100 };
generatorLimits[generatorTypes.exclusiveClass] = { min: 0, max: 99999, def: 0 };
generatorLimits[generatorTypes.overridingRootKey] = { min: 0 - 1, max: 127, def: -1 };
generatorLimits[generatorTypes.sampleModes] = { min: 0, max: 3, def: 0 };

export class Generator
{
Expand All @@ -155,19 +156,24 @@ export class Generator
* Constructs a new generator
* @param type {generatorTypes|number}
* @param value {number}
* @param validate {boolean}
*/
constructor(type = generatorTypes.INVALID, value = 0)
constructor(type = generatorTypes.INVALID, value = 0, validate = true)
{
this.generatorType = type;
if (value === undefined)
{
throw new Error("No value provided.");
}
const lim = generatorLimits[type];
this.generatorValue = Math.round(value);
if (lim !== undefined)
if (validate)
{
this.generatorValue = Math.max(lim.min, Math.min(lim.max, this.generatorValue));
const lim = generatorLimits[type];

if (lim !== undefined)
{
this.generatorValue = Math.max(lim.min, Math.min(lim.max, this.generatorValue));
}
}
}
}
Expand Down
25 changes: 20 additions & 5 deletions src/spessasynth_lib/soundfont/basic_soundfont/riff_chunk.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,40 @@ export function writeRIFFChunk(chunk, prepend = undefined)
* @param header {string}
* @param data {Uint8Array}
* @param addZeroByte {Boolean}
* @param isList {boolean}
* @returns {IndexedByteArray}
*/
export function writeRIFFOddSize(header, data, addZeroByte = false)
export function writeRIFFOddSize(header, data, addZeroByte = false, isList = false)
{
if (addZeroByte)
{
const tempData = new Uint8Array(data.length + 1);
tempData.set(data, 0);
data = tempData;
}
let finalSize = 8 + data.length;
let offset = 8;
let finalSize = offset + data.length;
let writtenSize = data.length;
if (finalSize % 2 !== 0)
{
finalSize++;
}
let headerWritten = header;
if (isList)
{
finalSize += 4;
writtenSize += 4;
offset += 4;
headerWritten = "LIST";
}
const outArray = new IndexedByteArray(finalSize);
writeStringAsBytes(outArray, header);
writeDword(outArray, data.length);
outArray.set(data, 8);
writeStringAsBytes(outArray, headerWritten);
writeDword(outArray, writtenSize);
if (isList)
{
writeStringAsBytes(outArray, header);
}
outArray.set(data, offset);
return outArray;
}

Expand Down
Loading

0 comments on commit 494588b

Please sign in to comment.