Skip to content
1 change: 1 addition & 0 deletions documentation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* New: Added support for envelope time keyboard- and velocity-scaling, which scales the envelope times by the played key and velocity (as opposed to the already supported slopes, which describe the curvature of a segment): Akai S1000, Ensoniq EPS/ASR, Ensoniq Mirage, Logic EXS24, Reason NN-XT, Roland S-7xx, SoundFont 2, Yamaha YSFC.
* New: Added support for per-instrument voice settings (polyphony and monophonic legato): Akai S1000, DecentSampler, Disting EX, Ensoniq Mirage, Logic EXS24, Reason NN-XT, Roland S-7xx, SFZ, Synthstrom Deluge, TAL Sampler.
* New: Added support for group volume, panning and tuning offsets: Kontakt, DecentSampler, Logic EXS24, Synclavier, TX16Wx, Waldorf Quantum/Iridium.
* New: Added support for a pitch low frequency oscillator (vibrato) with its rate, depth and delay: DecentSampler, DLS, SFZ, SoundFont 2. Previously any vibrato was dropped on conversion.
* New: Source folders and files are now processed in a stable alphabetical order instead of the file-system enumeration order, so consecutive runs behave identically (and e.g. the QPAT import numbers are assigned in a predictable order).
* New: Improved logging if WAV file could not be written.
* Fixed: Two filters which differed only in their cutoff envelope were treated as equal, so zones which are not identical could be combined into one; a filter with a cutoff envelope but without a cutoff velocity modulation could additionally throw an exception.
Expand Down
8 changes: 8 additions & 0 deletions documentation/README-FORMATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ A dspreset file contains a single preset and the description of the multi-sample

There are no metadata fields (category, creator, etc.) specified in the format. Therefore, information is stored and retrieved from Broadcast Audio Extension chunks in the WAV files. If no such chunks are present an [automatic detection](#automatic-metadata-detection) is applied.

A pitch LFO (vibrato) is converted as an `<lfo>` bound to the group tuning, carrying its rate (in Hertz), depth and delay. The oscillator's fade-in has no equivalent and is dropped, and the waveform is mapped to the nearest of sine, square and saw.

### Source Options

* Create one multi-sample per group: Creates a separate multi-sample for each group instead of one multi-sample which contains all groups. Intended for presets which contain several alternative kits or articulations as groups and switch between them via their user interface (only one group is enabled at a time). Disabled groups are converted as well when this option is enabled; when it is off, only the enabled groups are converted.
Expand Down Expand Up @@ -275,6 +277,8 @@ Both the program (.zbp) as well as the bank (.zbb) are stored as monoliths (zipp
The DLS format (*.dls) is a standardized file format developed for storing and distributing collections of digital musical instrument sounds, enabling their use in software synthesizers and hardware devices compatible with the MIDI protocol. It encapsulates audio samples, instrument definitions, articulations, and performance parameters into a single file. Developed in the 1990s initially by the Interactive Audio Special Interest Group (IASIG) and later standardized by the MIDI Manufacturers Association (MMA), with the first formal specification released in 1999.
There is no write support.

The amplitude and pitch envelopes, the sample loops and a pitch LFO (vibrato) are read. The vibrato's depth, its frequency (converted from absolute pitch cents to Hertz) and its start delay are carried over to the pitch LFO. Only a connection which is not modulated by a controller is read as the vibrato; the format normally contains a second one controlled by the modulation wheel, which is the amount the wheel can dial in and would sound permanently if it were converted.

## Elektron Tonverk

The Elektron Tonverk is a dedicated hardware sampler that marks an important milestone for Elektron as its first instrument to support multi-samples. This allows users to map multiple sampled sounds across keys or velocity ranges, creating more expressive and realistic instruments than single-sample playback alone.
Expand Down Expand Up @@ -609,6 +613,8 @@ The SFZ file contains only the description of the multi-sample. The related samp

SFZ can only mark a sample as a one-shot (`loop_mode=one_shot`) if it has no loop. A looped zone therefore keeps its loop and is not written as a one-shot.

A pitch LFO (vibrato) is read and written via the `pitchlfo_freq` (Hertz), `pitchlfo_depth` (cent), `pitchlfo_delay` and `pitchlfo_fade` (seconds) opcodes.

### Source Options

* Log unsupported SFZ opcodes: If enabled, opcodes which are found in the source but are not used (not supported) as input for the conversion are logged.
Expand All @@ -627,6 +633,8 @@ The conversion process creates one destination file for each preset found in a S

There are metadata fields for creator and some description specified in the format. However, additional information like a category is retrieved from Broadcast Audio Extension chunks in the WAV files. If no such chunks are present an [automatic detection](#automatic-metadata-detection) is applied.

The vibrato low frequency oscillator (generators `vibLfoToPitch`, `freqVibLFO` and `delayVibLFO`) is converted to and from the pitch LFO, carrying its depth, rate and delay. The waveform is always a triangle in this format and there is no fade-in.

### Source Options

* Log unused SF2 generators: If enabled, generators which are found in the source but are not used (not supported) as input for the conversion are logged.
Expand Down
Binary file modified documentation/SupportedFeaturesSampleFormats.ods
Binary file not shown.
122 changes: 122 additions & 0 deletions src/main/java/de/mossgrabers/convertwithmoss/core/model/ILfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Written by Jürgen Moßgraber - mossgrabers.de
// (c) 2019-2026
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt

package de.mossgrabers.convertwithmoss.core.model;

import de.mossgrabers.convertwithmoss.core.model.enumeration.LfoWaveform;


/**
* Interface to a low frequency oscillator. All times are given in seconds and the rate is given in
* Hertz, so that the values are comparable across formats. A format which stores a tempo
* synchronized rate needs to convert it, since the tempo is not part of a multi-sample.
*
* @author Jürgen Moßgraber
*/
public interface ILfo
{
/**
* Get the waveform.
*
* @return The waveform
*/
LfoWaveform getWaveform ();


/**
* Set the waveform.
*
* @param waveform The waveform
*/
void setWaveform (LfoWaveform waveform);


/**
* Get the rate.
*
* @return The rate in Hertz or -1 if not set
*/
double getRate ();


/**
* Set the rate.
*
* @param rate The rate in Hertz
*/
void setRate (double rate);


/**
* Get the delay, which is the time before the modulation starts.
*
* @return The delay in seconds or -1 if not set
*/
double getDelay ();


/**
* Set the delay, which is the time before the modulation starts.
*
* @param delay The delay in seconds
*/
void setDelay (double delay);


/**
* Get the fade-in, which is the time it takes to reach the full modulation depth.
*
* @return The fade-in in seconds or -1 if not set
*/
double getFadeIn ();


/**
* Set the fade-in, which is the time it takes to reach the full modulation depth.
*
* @param fadeIn The fade-in in seconds
*/
void setFadeIn (double fadeIn);


/**
* Get the phase at which the waveform starts.
*
* @return The start phase in the range of [0..1] or -1 if not set
*/
double getStartPhase ();


/**
* Set the phase at which the waveform starts.
*
* @param startPhase The start phase in the range of [0..1]
*/
void setStartPhase (double startPhase);


/**
* Check if the oscillator restarts when a note is triggered. If it does not, it runs freely in
* the background.
*
* @return True if it restarts on a note
*/
boolean isKeySync ();


/**
* Set if the oscillator restarts when a note is triggered.
*
* @param isKeySync True if it restarts on a note
*/
void setKeySync (boolean isKeySync);


/**
* Check if any value was set. A rate needs to be present for the oscillator to be of any use.
*
* @return True if a rate was set
*/
boolean isSet ();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Written by Jürgen Moßgraber - mossgrabers.de
// (c) 2019-2026
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt

package de.mossgrabers.convertwithmoss.core.model;

/**
* Interface to a low frequency oscillator modulator. The modulation depth of the pitch modulation
* maps to -4800..4800 cent, which is the same range as the one of the pitch envelope modulator.
*
* @author Jürgen Moßgraber
*/
public interface ILfoModulator extends IModulator
{
/**
* Get the modulation source.
*
* @return The modulation source, never null
*/
ILfo getSource ();


/**
* Set the modulation source.
*
* @param source The modulation source
*/
void setSource (ILfo source);
}
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,15 @@ public interface ISampleZone
IEnvelopeModulator getPitchEnvelopeModulator ();


/**
* Get the low frequency oscillator modulator for the pitch, which is commonly called vibrato. A
* depth of zero means that there is no vibrato.
*
* @return The modulator, never null
*/
ILfoModulator getPitchLfoModulator ();


/**
* Get pitch bend up value.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Written by Jürgen Moßgraber - mossgrabers.de
// (c) 2019-2026
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt

package de.mossgrabers.convertwithmoss.core.model.enumeration;

/**
* Different waveforms of a low frequency oscillator. Only the shapes which are present in most
* formats are listed, a format which knows further shapes needs to map them to the closest one.
*
* @author Jürgen Moßgraber
*/
public enum LfoWaveform
{
/** A sine wave. */
SINE,
/** A triangle wave. */
TRIANGLE,
/** A square wave. */
SQUARE,
/** A rising saw-tooth wave. */
SAWTOOTH_UP,
/** A falling saw-tooth wave. */
SAWTOOTH_DOWN,
/** A random (sample and hold) wave. */
RANDOM,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Written by Jürgen Moßgraber - mossgrabers.de
// (c) 2019-2026
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt

package de.mossgrabers.convertwithmoss.core.model.implementation;

import java.util.Objects;

import de.mossgrabers.convertwithmoss.core.model.ILfo;
import de.mossgrabers.convertwithmoss.core.model.enumeration.LfoWaveform;


/**
* Default implementation of a low frequency oscillator. All values are unset by default, which
* means that a format which does not fill them writes nothing.
*
* @author Jürgen Moßgraber
*/
public class DefaultLfo implements ILfo
{
private LfoWaveform waveform = LfoWaveform.TRIANGLE;
private double rate = -1;
private double delay = -1;
private double fadeIn = -1;
private double startPhase = -1;
private boolean isKeySync = false;


/** {@inheritDoc} */
@Override
public LfoWaveform getWaveform ()
{
return this.waveform;
}


/** {@inheritDoc} */
@Override
public void setWaveform (final LfoWaveform waveform)
{
this.waveform = waveform;
}


/** {@inheritDoc} */
@Override
public double getRate ()
{
return this.rate;
}


/** {@inheritDoc} */
@Override
public void setRate (final double rate)
{
this.rate = rate;
}


/** {@inheritDoc} */
@Override
public double getDelay ()
{
return this.delay;
}


/** {@inheritDoc} */
@Override
public void setDelay (final double delay)
{
this.delay = delay;
}


/** {@inheritDoc} */
@Override
public double getFadeIn ()
{
return this.fadeIn;
}


/** {@inheritDoc} */
@Override
public void setFadeIn (final double fadeIn)
{
this.fadeIn = fadeIn;
}


/** {@inheritDoc} */
@Override
public double getStartPhase ()
{
return this.startPhase;
}


/** {@inheritDoc} */
@Override
public void setStartPhase (final double startPhase)
{
this.startPhase = startPhase;
}


/** {@inheritDoc} */
@Override
public boolean isKeySync ()
{
return this.isKeySync;
}


/** {@inheritDoc} */
@Override
public void setKeySync (final boolean isKeySync)
{
this.isKeySync = isKeySync;
}


/** {@inheritDoc} */
@Override
public boolean isSet ()
{
return this.rate >= 0;
}


/** {@inheritDoc} */
@Override
public int hashCode ()
{
return Objects.hash (Double.valueOf (this.delay), Double.valueOf (this.fadeIn), Boolean.valueOf (this.isKeySync), Double.valueOf (this.rate), Double.valueOf (this.startPhase), this.waveform);
}


/** {@inheritDoc} */
@Override
public boolean equals (final Object obj)
{
if (this == obj)
return true;
if (obj == null || this.getClass () != obj.getClass ())
return false;
final DefaultLfo other = (DefaultLfo) obj;
return Double.doubleToLongBits (this.delay) == Double.doubleToLongBits (other.delay) && Double.doubleToLongBits (this.fadeIn) == Double.doubleToLongBits (other.fadeIn) && this.isKeySync == other.isKeySync && Double.doubleToLongBits (this.rate) == Double.doubleToLongBits (other.rate) && Double.doubleToLongBits (this.startPhase) == Double.doubleToLongBits (other.startPhase) && this.waveform == other.waveform;
}
}
Loading