diff --git a/README.md b/README.md
index 7c230f4..30fa33a 100644
--- a/README.md
+++ b/README.md
@@ -209,6 +209,17 @@ Since the format supports only one layer of a multisample, multiple files are cr
# Changes
+## 4.6
+
+* New: SF2, SFZ, MPC: Support for Pitch bend range settings.
+* New: SF2, SFZ, Decent Sampler, MPC: Support for filter settings (incl. filter envelope).
+* New: SF2, SFZ, MPC: Support for Pitch envelope settings.
+* Fixed: SFZ: Logging of unsupported opcodes did add up.
+* Fixed: SFZ: Sample paths in metadata now always use forward slash.
+* Fixed: Decent Sampler: Sample files from dslibrary could not be written.
+* Fixed: Decent Sampler: Tuning was not read correctly (off by factor 100).
+* Fixed: Decent Sampler: Round-robin was not read and not written correctly.
+
## 4.5
* New: Support for amplitude envelope: Decent Sampler, MPC Keygroups, SFZ: read/write; SF2: read
diff --git a/pom.xml b/pom.xml
index 16c6dc9..0d3dc20 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
Jürgen Moßgraber
http://www.mossgrabers.de
- 4.5.0
+ 4.6.0
UTF-8
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/IMultisampleSource.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/IMultisampleSource.java
index 9fd8a73..556c209 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/IMultisampleSource.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/IMultisampleSource.java
@@ -4,10 +4,12 @@
package de.mossgrabers.sampleconverter.core;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
import java.io.File;
import java.util.List;
+import java.util.Optional;
/**
@@ -135,4 +137,21 @@ public interface IMultisampleSource
* @return The name, usually the source file
*/
String getMappingName ();
+
+
+ /**
+ * Checks all samples in all layers for filter settings. Only if all samples contain the same
+ * filter settings a result is returned.
+ *
+ * @return The filter if a global filter setting is found
+ */
+ Optional getGlobalFilter ();
+
+
+ /**
+ * Sets a filter on all samples in all layers.
+ *
+ * @param filter The filter to set
+ */
+ void setGlobalFilter (IFilter filter);
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/creator/AbstractCreator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/creator/AbstractCreator.java
index 05d80c0..5021e26 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/creator/AbstractCreator.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/creator/AbstractCreator.java
@@ -26,6 +26,7 @@
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.zip.ZipEntry;
@@ -82,6 +83,19 @@ protected static String createSafeFilename (final String filename)
}
+ /**
+ * Format the path and filename replacing all slashes with forward slashes.
+ *
+ * @param path A path
+ * @param filename A filename
+ * @return The formatted path
+ */
+ protected String formatFileName (final String path, final String filename)
+ {
+ return new StringBuilder ().append (path).append ('/').append (filename).toString ().replace ('\\', '/');
+ }
+
+
protected static int check (final int value, final int defaultValue)
{
return value < 0 ? defaultValue : value;
@@ -264,4 +278,18 @@ protected static double clamp (double value, double minimum, double maximum)
{
return Math.max (minimum, Math.min (value, maximum));
}
+
+
+ /**
+ * Format a double attribute with a dot as the fraction separator.
+ *
+ * @param value The value to format
+ * @param fractions The number of fractions to format
+ * @return The formatted value
+ */
+ public static String formatDouble (final double value, final int fractions)
+ {
+ final String formatPattern = "%." + fractions + "f";
+ return String.format (Locale.US, formatPattern, Double.valueOf (value));
+ }
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/AbstractDetectorTask.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/AbstractDetectorTask.java
index 74f9cc4..d9a7c35 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/AbstractDetectorTask.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/AbstractDetectorTask.java
@@ -6,7 +6,7 @@
import de.mossgrabers.sampleconverter.core.IMultisampleSource;
import de.mossgrabers.sampleconverter.core.INotifier;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.exception.ParseException;
import de.mossgrabers.sampleconverter.file.wav.FormatChunk;
import de.mossgrabers.sampleconverter.file.wav.WaveFile;
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/MultisampleSource.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/MultisampleSource.java
index 82ddce6..f01709f 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/MultisampleSource.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/detector/MultisampleSource.java
@@ -5,12 +5,15 @@
package de.mossgrabers.sampleconverter.core.detector;
import de.mossgrabers.sampleconverter.core.IMultisampleSource;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
/**
@@ -24,11 +27,11 @@ public class MultisampleSource implements IMultisampleSource
private final String [] subPath;
private String name;
private final String mappingName;
- private String description = "";
- private String creator = "";
- private String category = "";
- private String [] keywords = new String [0];
- private List sampleMetadata = Collections.emptyList ();
+ private String description = "";
+ private String creator = "";
+ private String category = "";
+ private String [] keywords = new String [0];
+ private List layers = Collections.emptyList ();
/**
@@ -68,7 +71,7 @@ public File getFolder ()
@Override
public List getLayers ()
{
- return this.sampleMetadata;
+ return this.layers;
}
@@ -155,9 +158,9 @@ public void setKeywords (final String [] keywords)
/** {@inheritDoc} */
@Override
- public void setVelocityLayers (final List sampleMetadata)
+ public void setVelocityLayers (final List layers)
{
- this.sampleMetadata = new ArrayList<> (sampleMetadata);
+ this.layers = new ArrayList<> (layers);
}
@@ -167,4 +170,40 @@ public String getMappingName ()
{
return this.mappingName;
}
+
+
+ /** {@inheritDoc} */
+ @Override
+ public Optional getGlobalFilter ()
+ {
+ IFilter globalFilter = null;
+ for (final IVelocityLayer layer: this.layers)
+ {
+ for (final ISampleMetadata sampleMetadata: layer.getSampleMetadata ())
+ {
+ final Optional optFilter = sampleMetadata.getFilter ();
+ if (optFilter.isEmpty ())
+ return Optional.empty ();
+
+ IFilter filter = optFilter.get ();
+ if (globalFilter == null)
+ globalFilter = filter;
+ else if (!globalFilter.equals (filter))
+ return Optional.empty ();
+ }
+ }
+ return Optional.ofNullable (globalFilter);
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setGlobalFilter (final IFilter filter)
+ {
+ for (final IVelocityLayer layer: this.layers)
+ {
+ for (final ISampleMetadata sampleMetadata: layer.getSampleMetadata ())
+ sampleMetadata.setFilter (filter);
+ }
+ }
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/Envelope.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/Envelope.java
deleted file mode 100644
index c09868c..0000000
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/Envelope.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Written by Jürgen Moßgraber - mossgrabers.de
-// (c) 2019-2021
-// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
-
-package de.mossgrabers.sampleconverter.core.model;
-
-/**
- * Interface to an envelope e.g. volume, filter and pitch.
- *
- * @author Jürgen Moßgraber
- */
-public class Envelope implements IEnvelope
-{
- private double delay = -1;
- private double start = -1;
- private double attack = -1;
- private double hold = -1;
- private double decay = -1;
- private double sustain = -1;
- private double release = -1;
-
-
- /** {@inheritDoc} */
- @Override
- public double getDelay ()
- {
- return this.delay;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public void setDelay (final double delay)
- {
- this.delay = delay;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public double getStart ()
- {
- return this.start;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public void setStart (final double start)
- {
- this.start = start;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public double getAttack ()
- {
- return this.attack;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public void setAttack (final double attack)
- {
- this.attack = attack;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public double getHold ()
- {
- return this.hold;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public void setHold (final double hold)
- {
- this.hold = hold;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public double getDecay ()
- {
- return this.decay;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public void setDecay (final double decay)
- {
- this.decay = decay;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public double getSustain ()
- {
- return this.sustain;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public void setSustain (final double sustain)
- {
- this.sustain = sustain;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public double getRelease ()
- {
- return this.release;
- }
-
-
- /** {@inheritDoc} */
- @Override
- public void setRelease (final double release)
- {
- this.release = release;
- }
-}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/EnvelopeAccess.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/EnvelopeAccess.java
deleted file mode 100644
index 4a50c6a..0000000
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/EnvelopeAccess.java
+++ /dev/null
@@ -1,23 +0,0 @@
-// Written by Jürgen Moßgraber - mossgrabers.de
-// (c) 2019-2021
-// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
-
-package de.mossgrabers.sampleconverter.core.model;
-
-/**
- * Access to envelopes.
- *
- * @author Jürgen Moßgraber
- */
-public class EnvelopeAccess implements IEnvelopeAccess
-{
- private final IEnvelope amplitudeEnvelope = new Envelope ();
-
-
- /** {@inheritDoc} */
- @Override
- public IEnvelope getAmplitudeEnvelope ()
- {
- return this.amplitudeEnvelope;
- }
-}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IEnvelopeAccess.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IEnvelopeAccess.java
index 9b05f21..681a992 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IEnvelopeAccess.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IEnvelopeAccess.java
@@ -17,4 +17,28 @@ public interface IEnvelopeAccess
* @return The envelope
*/
IEnvelope getAmplitudeEnvelope ();
+
+
+ /**
+ * Get the pitch envelope.
+ *
+ * @return The envelope
+ */
+ IEnvelope getPitchEnvelope ();
+
+
+ /**
+ * Set the modulation depth of the pitch envelope.
+ *
+ * @param depth The depth in the range of [-12000..12000] cents
+ */
+ void setPitchEnvelopeDepth (int depth);
+
+
+ /**
+ * Get the modulation depth of the pitch envelope.
+ *
+ * @return The depth in the range of [-12000..12000] cents
+ */
+ int getPitchEnvelopeDepth ();
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IFilter.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IFilter.java
new file mode 100644
index 0000000..06804e7
--- /dev/null
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IFilter.java
@@ -0,0 +1,77 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2021
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.sampleconverter.core.model;
+
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+
+
+/**
+ * Interface to a filters' settings.
+ *
+ * @author Jürgen Moßgraber
+ */
+public interface IFilter
+{
+ /** The maximum filter cutoff frequency. */
+ public static final double MAX_FREQUENCY = 20000;
+ /** The maximum filter envelope depth. */
+ public static final int MAX_ENVELOPE_DEPTH = 12000;
+
+
+ /**
+ * Get the type of filter.
+ *
+ * @return The type
+ */
+ FilterType getType ();
+
+
+ /**
+ * Get the number of poles, if any.
+ *
+ * @return The number of poles
+ */
+ int getPoles ();
+
+
+ /**
+ * The cutoff in hertz.
+ *
+ * @return The cutoff
+ */
+ double getCutoff ();
+
+
+ /**
+ * The resonance in dB.
+ *
+ * @return The resonance
+ */
+ double getResonance ();
+
+
+ /**
+ * Set the modulation depth of the filter envelope.
+ *
+ * @param depth The depth in the range of [-12000..12000] cents
+ */
+ void setEnvelopeDepth (int depth);
+
+
+ /**
+ * Get the modulation depth of the filter envelope.
+ *
+ * @return The depth in the range of [-12000..12000] cents
+ */
+ int getEnvelopeDepth ();
+
+
+ /**
+ * Get the filter envelope.
+ *
+ * @return The envelope
+ */
+ IEnvelope getEnvelope ();
+}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleLoop.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleLoop.java
index 4717399..2e7288c 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleLoop.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleLoop.java
@@ -4,6 +4,8 @@
package de.mossgrabers.sampleconverter.core.model;
+import de.mossgrabers.sampleconverter.core.model.enumeration.LoopType;
+
/**
* Interface to the loop of a sample.
*
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleMetadata.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleMetadata.java
index d1295df..32ca505 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleMetadata.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/ISampleMetadata.java
@@ -4,6 +4,8 @@
package de.mossgrabers.sampleconverter.core.model;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
+
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
@@ -103,7 +105,7 @@ public interface ISampleMetadata extends IEnvelopeAccess
*
* @param loop The loop to add
*/
- void addLoop (SampleLoop loop);
+ void addLoop (ISampleLoop loop);
/**
@@ -111,7 +113,7 @@ public interface ISampleMetadata extends IEnvelopeAccess
*
* @return The loops, if any
*/
- List getLoops ();
+ List getLoops ();
/**
@@ -374,4 +376,52 @@ public interface ISampleMetadata extends IEnvelopeAccess
* @throws IOException Could not write the data
*/
void writeSample (OutputStream outputStream) throws IOException;
+
+
+ /**
+ * Get pitch bend up value.
+ *
+ * @return The cents to bend down (if negative) or up in cents (-9600 to 9600)
+ */
+ int getBendUp ();
+
+
+ /**
+ * Set pitch bend up value.
+ *
+ * @param cents The cents to bend down (if negative) or up in cents (-9600 to 9600)
+ */
+ void setBendUp (int cents);
+
+
+ /**
+ * Get pitch bend down value.
+ *
+ * @return The cents to bend down (if negative) or up in cents (-9600 to 9600)
+ */
+ int getBendDown ();
+
+
+ /**
+ * Set pitch bend down value.
+ *
+ * @param cents The cents to bend down (if negative) or up in cents (-9600 to 9600)
+ */
+ void setBendDown (int cents);
+
+
+ /**
+ * Get a filter.
+ *
+ * @return The filter
+ */
+ Optional getFilter ();
+
+
+ /**
+ * Set a filter.
+ *
+ * @param filter The filter to set
+ */
+ void setFilter (IFilter filter);
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/FilterType.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/FilterType.java
new file mode 100644
index 0000000..3f00354
--- /dev/null
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/FilterType.java
@@ -0,0 +1,22 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2021
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.sampleconverter.core.model.enumeration;
+
+/**
+ * Different types of filters.
+ *
+ * @author Jürgen Moßgraber
+ */
+public enum FilterType
+{
+ /** A low pass filter. */
+ LOW_PASS,
+ /** A high pass filter. */
+ HIGH_PASS,
+ /** A band pass filter. */
+ BAND_PASS,
+ /** A band rejection filter. */
+ BAND_REJECTION,
+}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/LoopType.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/LoopType.java
similarity index 83%
rename from src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/LoopType.java
rename to src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/LoopType.java
index 23837da..3844da8 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/LoopType.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/LoopType.java
@@ -2,7 +2,7 @@
// (c) 2019-2021
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
-package de.mossgrabers.sampleconverter.core.model;
+package de.mossgrabers.sampleconverter.core.model.enumeration;
/**
* The playback type of a loop in a sample.
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/PlayLogic.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/PlayLogic.java
similarity index 82%
rename from src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/PlayLogic.java
rename to src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/PlayLogic.java
index 2b86291..e17dbb5 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/PlayLogic.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/PlayLogic.java
@@ -2,7 +2,7 @@
// (c) 2019-2021
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
-package de.mossgrabers.sampleconverter.core.model;
+package de.mossgrabers.sampleconverter.core.model.enumeration;
/**
* Logic to apply how to play layers.
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/AbstractEnvelope.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/AbstractEnvelope.java
new file mode 100644
index 0000000..fda96a4
--- /dev/null
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/AbstractEnvelope.java
@@ -0,0 +1,53 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2021
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.sampleconverter.core.model.implementation;
+
+import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IEnvelopeAccess;
+
+
+/**
+ * Access to envelopes.
+ *
+ * @author Jürgen Moßgraber
+ */
+public abstract class AbstractEnvelope implements IEnvelopeAccess
+{
+ private final IEnvelope amplitudeEnvelope = new DefaultEnvelope ();
+ private final IEnvelope pitchEnvelope = new DefaultEnvelope ();
+ private int pitchEnvelopeDepth = 0;
+
+
+ /** {@inheritDoc} */
+ @Override
+ public IEnvelope getAmplitudeEnvelope ()
+ {
+ return this.amplitudeEnvelope;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public IEnvelope getPitchEnvelope ()
+ {
+ return this.pitchEnvelope;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setPitchEnvelopeDepth (final int depth)
+ {
+ this.pitchEnvelopeDepth = depth;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public int getPitchEnvelopeDepth ()
+ {
+ return this.pitchEnvelopeDepth;
+ }
+}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultEnvelope.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultEnvelope.java
new file mode 100644
index 0000000..629aa16
--- /dev/null
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultEnvelope.java
@@ -0,0 +1,188 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2021
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.sampleconverter.core.model.implementation;
+
+import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+
+
+/**
+ * Interface to an envelope e.g. volume, filter and pitch.
+ *
+ * @author Jürgen Moßgraber
+ */
+public class DefaultEnvelope implements IEnvelope
+{
+ private double delay = -1;
+ private double start = -1;
+ private double attack = -1;
+ private double hold = -1;
+ private double decay = -1;
+ private double sustain = -1;
+ private double release = -1;
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getDelay ()
+ {
+ return this.delay;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setDelay (final double delay)
+ {
+ this.delay = delay;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getStart ()
+ {
+ return this.start;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setStart (final double start)
+ {
+ this.start = start;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getAttack ()
+ {
+ return this.attack;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setAttack (final double attack)
+ {
+ this.attack = attack;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getHold ()
+ {
+ return this.hold;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setHold (final double hold)
+ {
+ this.hold = hold;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getDecay ()
+ {
+ return this.decay;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setDecay (final double decay)
+ {
+ this.decay = decay;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getSustain ()
+ {
+ return this.sustain;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setSustain (final double sustain)
+ {
+ this.sustain = sustain;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getRelease ()
+ {
+ return this.release;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setRelease (final double release)
+ {
+ this.release = release;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode ()
+ {
+ final int prime = 31;
+ int result = 1;
+ long temp;
+ temp = Double.doubleToLongBits (this.attack);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits (this.decay);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits (this.delay);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits (this.hold);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits (this.release);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits (this.start);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ temp = Double.doubleToLongBits (this.sustain);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ return result;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals (Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass () != obj.getClass ())
+ return false;
+ DefaultEnvelope other = (DefaultEnvelope) obj;
+ if (Double.doubleToLongBits (this.attack) != Double.doubleToLongBits (other.attack))
+ return false;
+ if (Double.doubleToLongBits (this.decay) != Double.doubleToLongBits (other.decay))
+ return false;
+ if (Double.doubleToLongBits (this.delay) != Double.doubleToLongBits (other.delay))
+ return false;
+ if (Double.doubleToLongBits (this.hold) != Double.doubleToLongBits (other.hold))
+ return false;
+ if (Double.doubleToLongBits (this.release) != Double.doubleToLongBits (other.release))
+ return false;
+ if (Double.doubleToLongBits (this.start) != Double.doubleToLongBits (other.start))
+ return false;
+ return Double.doubleToLongBits (this.sustain) == Double.doubleToLongBits (other.sustain);
+ }
+}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultFilter.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultFilter.java
new file mode 100644
index 0000000..3049787
--- /dev/null
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultFilter.java
@@ -0,0 +1,147 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2021
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.sampleconverter.core.model.implementation;
+
+import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+
+
+/**
+ * Default implementation for a filters' settings.
+ *
+ * @author Jürgen Moßgraber
+ */
+public class DefaultFilter implements IFilter
+{
+ protected FilterType type;
+ protected int poles;
+ protected double cutoff;
+ protected double resonance;
+ protected int envelopeDepth;
+ protected IEnvelope envelope = new DefaultEnvelope ();
+
+
+ /**
+ * Constructor.
+ *
+ * @param type The type of the filter
+ * @param poles The number of poles of the filter, if any
+ * @param cutoff The cutoff frequency
+ * @param resonance The resonance
+ */
+ public DefaultFilter (final FilterType type, final int poles, final double cutoff, final double resonance)
+ {
+ this.type = type;
+ this.poles = poles;
+ this.cutoff = cutoff;
+ this.resonance = resonance;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public FilterType getType ()
+ {
+ return this.type;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getCutoff ()
+ {
+ return this.cutoff;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public double getResonance ()
+ {
+ return this.resonance;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public int getPoles ()
+ {
+ return this.poles;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public IEnvelope getEnvelope ()
+ {
+ return this.envelope;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setEnvelopeDepth (final int envelopeDepth)
+ {
+ this.envelopeDepth = envelopeDepth;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public int getEnvelopeDepth ()
+ {
+ return this.envelopeDepth;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public int hashCode ()
+ {
+ final int prime = 31;
+ int result = 1;
+ long temp;
+ temp = Double.doubleToLongBits (this.cutoff);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ result = prime * result + ((this.envelope == null) ? 0 : this.envelope.hashCode ());
+ result = prime * result + this.envelopeDepth;
+ result = prime * result + this.poles;
+ temp = Double.doubleToLongBits (this.resonance);
+ result = prime * result + (int) (temp ^ (temp >>> 32));
+ result = prime * result + ((this.type == null) ? 0 : this.type.hashCode ());
+ return result;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean equals (final Object obj)
+ {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass () != obj.getClass ())
+ return false;
+ DefaultFilter other = (DefaultFilter) obj;
+ if (Double.doubleToLongBits (this.cutoff) != Double.doubleToLongBits (other.cutoff))
+ return false;
+ if (this.envelope == null)
+ {
+ if (other.envelope != null)
+ return false;
+ }
+ else if (!this.envelope.equals (other.envelope))
+ return false;
+ if (this.envelopeDepth != other.envelopeDepth)
+ return false;
+ if (this.poles != other.poles)
+ return false;
+ if (Double.doubleToLongBits (this.resonance) != Double.doubleToLongBits (other.resonance))
+ return false;
+ return this.type == other.type;
+ }
+}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/SampleLoop.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultSampleLoop.java
similarity index 80%
rename from src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/SampleLoop.java
rename to src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultSampleLoop.java
index 4983a41..1ce23a5 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/SampleLoop.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultSampleLoop.java
@@ -2,14 +2,17 @@
// (c) 2019-2021
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
-package de.mossgrabers.sampleconverter.core.model;
+package de.mossgrabers.sampleconverter.core.model.implementation;
+
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
+import de.mossgrabers.sampleconverter.core.model.enumeration.LoopType;
/**
* The loop of a sample.
*
* @author Jürgen Moßgraber
*/
-public class SampleLoop implements ISampleLoop
+public class DefaultSampleLoop implements ISampleLoop
{
private LoopType loopType = LoopType.FORWARD;
private int loopStart = -1;
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/DefaultSampleMetadata.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultSampleMetadata.java
similarity index 66%
rename from src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/DefaultSampleMetadata.java
rename to src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultSampleMetadata.java
index b70b3ee..ffc854b 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/DefaultSampleMetadata.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultSampleMetadata.java
@@ -2,9 +2,14 @@
// (c) 2019-2021
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
-package de.mossgrabers.sampleconverter.core.model;
+package de.mossgrabers.sampleconverter.core.model.implementation;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
+import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
import de.mossgrabers.sampleconverter.format.wav.WavSampleMetadata;
+import de.mossgrabers.sampleconverter.ui.tools.Functions;
import java.io.File;
import java.io.FileInputStream;
@@ -24,47 +29,51 @@
*
* @author Jürgen Moßgraber
*/
-public class DefaultSampleMetadata extends EnvelopeAccess implements ISampleMetadata
+public class DefaultSampleMetadata extends AbstractEnvelope implements ISampleMetadata
{
- protected final File sampleFile;
- protected final File zipFile;
-
- protected final String filename;
- protected boolean isMonoFile = false;
- protected int sampleRate = 44100;
-
- protected Optional combinedFilename = Optional.empty ();
- protected Optional filenameWithoutLayer = Optional.empty ();
-
- protected PlayLogic playLogic = PlayLogic.ALWAYS;
- protected int start = -1;
- protected int stop = -1;
- protected int keyRoot = -1;
- protected int keyLow = 0;
- protected int keyHigh = 127;
- protected int crossfadeNotesLow = 0;
- protected int crossfadeNotesHigh = 0;
- protected int velocityLow = 1;
- protected int velocityHigh = 127;
- protected int crossfadeVelocitiesLow = 0;
- protected int crossfadeVelocitiesHigh = 0;
-
- protected double gain = 0;
- protected double tune = 0;
- protected double keyTracking = 1.0;
- protected boolean isReversed = false;
-
- protected List loops = new ArrayList<> (1);
+ protected final String filename;
+ protected final File sampleFile;
+ protected final File zipFile;
+ protected final File zipEntry;
+
+ protected boolean isMonoFile = false;
+ protected int sampleRate = 44100;
+
+ protected Optional combinedFilename = Optional.empty ();
+ protected Optional filenameWithoutLayer = Optional.empty ();
+
+ protected PlayLogic playLogic = PlayLogic.ALWAYS;
+ protected int start = -1;
+ protected int stop = -1;
+ protected int keyRoot = -1;
+ protected int keyLow = 0;
+ protected int keyHigh = 127;
+ protected int crossfadeNotesLow = 0;
+ protected int crossfadeNotesHigh = 0;
+ protected int velocityLow = 1;
+ protected int velocityHigh = 127;
+ protected int crossfadeVelocitiesLow = 0;
+ protected int crossfadeVelocitiesHigh = 0;
+
+ protected double gain = 0;
+ protected double tune = 0;
+ protected double keyTracking = 1.0;
+ protected int bendUp = 0;
+ protected int bendDown = 0;
+ protected boolean isReversed = false;
+ protected IFilter filter = null;
+
+ protected List loops = new ArrayList<> (1);
/**
- * Constructor.
+ * Constructor for a sample stored in the file system.
*
* @param sampleFile The file where the sample is stored
*/
public DefaultSampleMetadata (final File sampleFile)
{
- this (sampleFile.getName (), sampleFile, null);
+ this (sampleFile.getName (), sampleFile, null, null);
}
@@ -72,37 +81,28 @@ public DefaultSampleMetadata (final File sampleFile)
* Constructor for a sample stored in a ZIP file.
*
* @param zipFile The ZIP file which contains the WAV files
- * @param filename The name of the samples' file in the ZIP file
+ * @param zipEntry The relative path in the ZIP where the file is stored
*/
- public DefaultSampleMetadata (final File zipFile, final String filename)
+ public DefaultSampleMetadata (final File zipFile, final File zipEntry)
{
- this (filename, null, zipFile);
+ this (zipEntry.getName (), null, zipFile, zipEntry);
}
/**
* Constructor.
*
- * @param filename The name of the file where the sample is stored
- */
- public DefaultSampleMetadata (final String filename)
- {
- this (filename, null, null);
- }
-
-
- /**
- * Constructor.
- *
- * @param filename The name of the file where the sample is stored
+ * @param filename The name of the file where the sample is stored (must not contain any paths!)
* @param sampleFile The file where the sample is stored
* @param zipFile The ZIP file which contains the WAV files
+ * @param zipEntry The relative path in the ZIP where the file is stored
*/
- private DefaultSampleMetadata (final String filename, final File sampleFile, final File zipFile)
+ protected DefaultSampleMetadata (final String filename, final File sampleFile, final File zipFile, final File zipEntry)
{
this.filename = filename;
this.sampleFile = sampleFile;
this.zipFile = zipFile;
+ this.zipEntry = zipEntry;
}
@@ -180,7 +180,7 @@ public void setStop (final int stop)
/** {@inheritDoc} */
@Override
- public void addLoop (final SampleLoop loop)
+ public void addLoop (final ISampleLoop loop)
{
this.loops.add (loop);
}
@@ -188,7 +188,7 @@ public void addLoop (final SampleLoop loop)
/** {@inheritDoc} */
@Override
- public List getLoops ()
+ public List getLoops ()
{
return this.loops;
}
@@ -386,6 +386,38 @@ public void setKeyTracking (final double keyTracking)
}
+ /** {@inheritDoc} */
+ @Override
+ public int getBendUp ()
+ {
+ return this.bendUp;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setBendUp (int cents)
+ {
+ this.bendUp = cents;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public int getBendDown ()
+ {
+ return this.bendDown;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setBendDown (int cents)
+ {
+ this.bendDown = cents;
+ }
+
+
/** {@inheritDoc} */
@Override
public boolean isReversed ()
@@ -402,6 +434,22 @@ public void setReversed (final boolean isReversed)
}
+ /** {@inheritDoc} */
+ @Override
+ public Optional getFilter ()
+ {
+ return Optional.ofNullable (this.filter);
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void setFilter (final IFilter filter)
+ {
+ this.filter = filter;
+ }
+
+
/** {@inheritDoc} */
@Override
public void setCombinedName (final String combinedName)
@@ -468,9 +516,10 @@ public void writeSample (final OutputStream outputStream) throws IOException
try (final ZipFile zf = new ZipFile (this.zipFile))
{
- final ZipEntry entry = zf.getEntry (this.filename);
+ final String path = this.zipEntry.getPath ().replace ('\\', '/');
+ final ZipEntry entry = zf.getEntry (path);
if (entry == null)
- throw new FileNotFoundException (String.format ("The sample '%s' was not found in the ZIP file.", this.filename));
+ throw new FileNotFoundException (Functions.getMessage ("IDS_NOTIFY_ERR_FILE_NOT_FOUND_IN_ZIP", path));
try (final InputStream in = zf.getInputStream (entry))
{
@@ -494,7 +543,7 @@ public void addMissingInfoFromWaveFile (final boolean addRootKey, final boolean
if (this.sampleFile != null)
wavSampleMetadata = new WavSampleMetadata (this.sampleFile);
else
- wavSampleMetadata = new WavSampleMetadata (this.zipFile, this.filename);
+ wavSampleMetadata = new WavSampleMetadata (this.zipFile, this.zipEntry);
if (this.start < 0)
this.start = 0;
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/VelocityLayer.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultVelocityLayer.java
similarity index 73%
rename from src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/VelocityLayer.java
rename to src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultVelocityLayer.java
index dc237ef..64b7bf5 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/VelocityLayer.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultVelocityLayer.java
@@ -2,7 +2,10 @@
// (c) 2019-2021
// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
-package de.mossgrabers.sampleconverter.core.model;
+package de.mossgrabers.sampleconverter.core.model.implementation;
+
+import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
import java.util.ArrayList;
import java.util.List;
@@ -13,7 +16,7 @@
*
* @author Jürgen Moßgraber
*/
-public class VelocityLayer implements IVelocityLayer
+public class DefaultVelocityLayer implements IVelocityLayer
{
private List samples = new ArrayList<> ();
private String name;
@@ -22,7 +25,7 @@ public class VelocityLayer implements IVelocityLayer
/**
* Constructor.
*/
- public VelocityLayer ()
+ public DefaultVelocityLayer ()
{
// Intentionally empty
}
@@ -33,7 +36,7 @@ public VelocityLayer ()
*
* @param name The layers' name
*/
- public VelocityLayer (final String name)
+ public DefaultVelocityLayer (final String name)
{
this.name = name;
}
@@ -44,7 +47,7 @@ public VelocityLayer (final String name)
*
* @param samples The layers' samples
*/
- public VelocityLayer (final List samples)
+ public DefaultVelocityLayer (final List samples)
{
this.samples = samples;
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/AbstractZone.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/AbstractZone.java
index 8a436db..f4a8825 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/AbstractZone.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/AbstractZone.java
@@ -4,8 +4,11 @@
package de.mossgrabers.sampleconverter.file.sf2;
+import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
+import java.util.Optional;
/**
@@ -21,12 +24,14 @@ public abstract class AbstractZone
/** Index to the first modulator of the zone in the PMOD list. */
protected final int firstModulator;
+ protected final int numberOfModulators;
/** If true, this is a global zone which applies to the whole preset. */
protected boolean isGlobal = false;
/** The generators assigned to the zone. */
protected Map generators = new HashMap<> ();
+ protected List modulators = new ArrayList<> ();
/**
@@ -35,12 +40,14 @@ public abstract class AbstractZone
* @param firstGenerator Index to the first generator of the zone in the PGEN list
* @param numberOfGenerators The number of generators in this zone
* @param firstModulator Index to the first modulator of the zone in the PMOD list
+ * @param numberOfModulators The number of modulators of this zone
*/
- protected AbstractZone (final int firstGenerator, final int numberOfGenerators, final int firstModulator)
+ protected AbstractZone (final int firstGenerator, final int numberOfGenerators, final int firstModulator, final int numberOfModulators)
{
this.firstGenerator = firstGenerator;
this.numberOfGenerators = numberOfGenerators;
this.firstModulator = firstModulator;
+ this.numberOfModulators = numberOfModulators;
}
@@ -99,6 +106,17 @@ public int getFirstModulator ()
}
+ /**
+ * Get the number of modulators in this zone.
+ *
+ * @return The number of modulators in this zone
+ */
+ public int getNumberOfModulators ()
+ {
+ return this.numberOfModulators;
+ }
+
+
/**
* Get all generators of the zone.
*
@@ -144,4 +162,39 @@ public boolean hasGenerator (final int generatorID)
{
return this.generators.keySet ().contains (Integer.valueOf (generatorID));
}
+
+
+ /**
+ * Add a modulator to the zone.
+ *
+ * @param sourceModulator The ID of the source modulator
+ * @param destinationGenerator The destination of the modulator
+ * @param modAmount A signed value indicating the degree to which the source modulates the
+ * destination
+ * @param amountSourceOperand Indicates the degree to which the source modulates the destination
+ * is to be controlled by the specified modulation source
+ * @param transformOperand Indicates that a transform of the specified type will be applied to
+ * the modulation source before application to the modulator
+ */
+ public void addModulator (final int sourceModulator, final int destinationGenerator, final int modAmount, final int amountSourceOperand, final int transformOperand)
+ {
+ this.modulators.add (new Sf2Modulator (sourceModulator, destinationGenerator, modAmount, amountSourceOperand, transformOperand));
+ }
+
+
+ /**
+ * Get a specific modulator if present.
+ *
+ * @param modulatorID The ID of the modulator to get
+ * @return The optional result
+ */
+ public Optional getModulator (final Integer modulatorID)
+ {
+ for (final Sf2Modulator modulator: this.modulators)
+ {
+ if (modulator.getControllerSource () == modulatorID.intValue ())
+ return Optional.of (modulator);
+ }
+ return Optional.empty ();
+ }
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Generator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Generator.java
index 144e1c4..92d6b43 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Generator.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Generator.java
@@ -14,46 +14,70 @@
*/
public class Generator
{
+ /** The ID of the modulation envelope to pitch generator. */
+ public static final int MOD_ENV_TO_PITCH = 7;
+
+ /** The ID of the initial filter cutoff generator. */
+ public static final int INITIAL_FILTER_CUTOFF = 8;
+ /** The ID of the initial filter resonance generator. */
+ public static final int INITIAL_FILTER_RESONANCE = 9;
+
+ /** The ID of the modulation envelope to filter cutoff generator. */
+ public static final int MOD_ENV_TO_FILTER_CUTOFF = 11;
+
/** The ID of the panorama generator. */
- public static final int PANORAMA = 17;
+ public static final int PANORAMA = 17;
+
+ /** The ID of the modulation envelope delay generator. */
+ public static final int MOD_ENV_DELAY = 25;
+ /** The ID of the modulation envelope attack generator. */
+ public static final int MOD_ENV_ATTACK = 26;
+ /** The ID of the modulation envelope hold generator. */
+ public static final int MOD_ENV_HOLD = 27;
+ /** The ID of the modulation envelope decay generator. */
+ public static final int MOD_ENV_DECAY = 28;
+ /** The ID of the modulation envelope sustain generator. */
+ public static final int MOD_ENV_SUSTAIN = 29;
+ /** The ID of the modulation envelope release generator. */
+ public static final int MOD_ENV_RELEASE = 30;
/** The ID of the volume envelope delay generator. */
- public static final int VOL_ENV_DELAY = 33;
+ public static final int VOL_ENV_DELAY = 33;
/** The ID of the volume envelope attack generator. */
- public static final int VOL_ENV_ATTACK = 34;
+ public static final int VOL_ENV_ATTACK = 34;
/** The ID of the volume envelope hold generator. */
- public static final int VOL_ENV_HOLD = 35;
+ public static final int VOL_ENV_HOLD = 35;
/** The ID of the volume envelope decay generator. */
- public static final int VOL_ENV_DECAY = 36;
+ public static final int VOL_ENV_DECAY = 36;
/** The ID of the volume envelope sustain generator. */
- public static final int VOL_ENV_SUSTAIN = 37;
+ public static final int VOL_ENV_SUSTAIN = 37;
/** The ID of the volume envelope release generator. */
- public static final int VOL_ENV_RELEASE = 38;
+ public static final int VOL_ENV_RELEASE = 38;
/** The ID of the instrument generator. */
- public static final int INSTRUMENT = 41;
+ public static final int INSTRUMENT = 41;
/** The ID of the key range generator. */
- public static final int KEY_RANGE = 43;
+ public static final int KEY_RANGE = 43;
/** The ID of the velocity range generator. */
- public static final int VELOCITY_RANGE = 44;
+ public static final int VELOCITY_RANGE = 44;
/** The ID of the initial gain attenuation generator. */
- public static final int INITIAL_ATTENUATION = 48;
+ public static final int INITIAL_ATTENUATION = 48;
/** The ID of the coarse tune generator. */
- public static final int COARSE_TUNE = 51;
+ public static final int COARSE_TUNE = 51;
/** The ID of the fine tune generator. */
- public static final int FINE_TUNE = 52;
+ public static final int FINE_TUNE = 52;
/** The ID of the sample ID generator. */
- public static final int SAMPLE_ID = 53;
+ public static final int SAMPLE_ID = 53;
/** The ID of the sample modes generator. */
- public static final int SAMPLE_MODES = 54;
+ public static final int SAMPLE_MODES = 54;
/** The ID of the scale tuning generator. */
- public static final int SCALE_TUNE = 56;
+ public static final int SCALE_TUNE = 56;
/** The ID of the overriding root key generator. */
- public static final int OVERRIDING_ROOT_KEY = 58;
+ public static final int OVERRIDING_ROOT_KEY = 58;
/** The generator names. */
- public static final String [] GENERATORS = new String [61];
- private static final int [] DEFAULT_VALUES = new int [61];
+ public static final String [] GENERATORS = new String [61];
+ private static final int [] DEFAULT_VALUES = new int [61];
static
{
GENERATORS[0] = "startAddrsOffset";
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2File.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2File.java
index 98a262d..b7f7715 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2File.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2File.java
@@ -36,12 +36,16 @@ public class Sf2File
/** The length of the PBAG structure. */
private static final int LENGTH_PBAG = 4;
+ /** The length of the PMOD structure. */
+ private static final int LENGTH_PMOD = 10;
/** The length of the PGEN structure. */
private static final int LENGTH_PGEN = 4;
/** The length of the INST structure. */
private static final int LENGTH_INST = 22;
/** The length of the IBAG structure. */
private static final int LENGTH_IBAG = 4;
+ /** The length of the IMOD structure. */
+ private static final int LENGTH_IMOD = 10;
/** The length of the IGEN structure. */
private static final int LENGTH_IGEN = 4;
/** The length of the SHDR structure. */
@@ -395,7 +399,7 @@ public void visitChunk (final RIFFChunk group, final RIFFChunk chunk) throws Par
this.parsePresetGenerators (chunk);
break;
case SF_PMOD_ID:
- // Modulators are currently not supported
+ this.parsePresetModulators (chunk);
break;
case SF_INST_ID:
this.parseInstruments (chunk);
@@ -404,7 +408,7 @@ public void visitChunk (final RIFFChunk group, final RIFFChunk chunk) throws Par
this.parseInstrumentZones (chunk);
break;
case SF_IMOD_ID:
- // Modulators are currently not supported
+ this.parseInstrumentModulators (chunk);
break;
case SF_IGEN_ID:
this.parseInstrumentGenerators (chunk);
@@ -448,8 +452,10 @@ private void parsePresetZones (final RIFFChunk chunk) throws ParseException
{
final int offset = (firstZoneIndex + zoneCounter) * LENGTH_PBAG;
final int generatorIndex = chunk.twoBytesAsInt (offset);
- final int nextGeneratorIndex = chunk.twoBytesAsInt ((firstZoneIndex + zoneCounter + 1) * LENGTH_PBAG);
- preset.addZone (new Sf2PresetZone (generatorIndex, nextGeneratorIndex - generatorIndex, chunk.twoBytesAsInt (offset + 2)));
+ final int modulatorIndex = chunk.twoBytesAsInt (offset + 2);
+ final int nextGeneratorIndex = chunk.twoBytesAsInt (offset + LENGTH_PBAG);
+ final int nextModulatorIndex = chunk.twoBytesAsInt (offset + LENGTH_PBAG + 2);
+ preset.addZone (new Sf2PresetZone (generatorIndex, nextGeneratorIndex - generatorIndex, modulatorIndex, nextModulatorIndex - modulatorIndex));
}
}
}
@@ -460,6 +466,53 @@ private void parsePresetZones (final RIFFChunk chunk) throws ParseException
}
+ /**
+ * Parse the preset modulators chunk (PMOD) and assign the parsed modulators to their
+ * preset.
+ *
+ * @param chunk The chunk to parse
+ * @throws ParseException Error if the chunk is unsound
+ */
+ private void parsePresetModulators (final RIFFChunk chunk) throws ParseException
+ {
+ // Check for sound PMOD structure
+ final long size = chunk.getSize ();
+ if (size % LENGTH_PMOD > 0)
+ throw new ParseException (Functions.getMessage ("IDS_NOTIFY_ERR_BROKEN_PRESET_MODULATORS"));
+
+ for (int i = 0; i < Sf2File.this.presets.size () - 1; i++)
+ {
+ final Sf2Preset preset = Sf2File.this.presets.get (i);
+ for (int zoneIndex = 0; zoneIndex < preset.getZoneCount (); zoneIndex++)
+ this.parsePresetZoneModulators (chunk, preset.getZone (zoneIndex));
+ }
+ }
+
+
+ /**
+ * Parse all modulators of a preset zone.
+ *
+ * @param chunk The chunk to parse
+ * @param zone The zone
+ */
+ private void parsePresetZoneModulators (final RIFFChunk chunk, final Sf2PresetZone zone)
+ {
+ final int firstModulator = zone.getFirstModulator ();
+ final int numberOfModulators = zone.getNumberOfModulators ();
+
+ for (int index = 0; index < numberOfModulators; index++)
+ {
+ final int offset = (firstModulator + index) * LENGTH_PMOD;
+ final int sourceModulator = chunk.twoBytesAsInt (offset);
+ final int destinationGenerator = chunk.twoBytesAsInt (offset + 2);
+ final int modAmount = chunk.twoBytesAsInt (offset + 4);
+ final int amountSourceOperand = chunk.twoBytesAsInt (offset + 6);
+ final int transformOperand = chunk.twoBytesAsInt (offset + 8);
+ zone.addModulator (sourceModulator, destinationGenerator, modAmount, amountSourceOperand, transformOperand);
+ }
+ }
+
+
/**
* Parse the preset generators chunk (PGEN) and assign the parsed generators to their
* preset.
@@ -494,10 +547,11 @@ private void parsePresetZoneGenerators (final RIFFChunk chunk, final Sf2PresetZo
final int firstGenerator = zone.getFirstGenerator ();
final int numberOfGenerators = zone.getNumberOfGenerators ();
- for (int index = firstGenerator; index < firstGenerator + numberOfGenerators; index++)
+ for (int index = 0; index < numberOfGenerators; index++)
{
- final int generator = chunk.twoBytesAsInt (LENGTH_PGEN * index);
- final int value = chunk.twoBytesAsInt (LENGTH_PGEN * index + 2);
+ final int offset = (firstGenerator + index) * LENGTH_PGEN;
+ final int generator = chunk.twoBytesAsInt (offset);
+ final int value = chunk.twoBytesAsInt (offset + 2);
zone.addGenerator (generator, value);
}
@@ -570,8 +624,10 @@ private void parseInstrumentZones (final RIFFChunk chunk) throws ParseException
{
final int offset = (firstZoneIndex + zoneCounter) * LENGTH_IBAG;
final int generatorIndex = chunk.twoBytesAsInt (offset);
- final int nextGeneratorIndex = chunk.twoBytesAsInt ((firstZoneIndex + zoneCounter + 1) * LENGTH_IBAG);
- instrument.addZone (new Sf2InstrumentZone (generatorIndex, nextGeneratorIndex - generatorIndex, chunk.twoBytesAsInt (offset + 2)));
+ final int modulatorIndex = chunk.twoBytesAsInt (offset + 2);
+ final int nextGeneratorIndex = chunk.twoBytesAsInt (offset + LENGTH_IBAG);
+ final int nextModulatorIndex = chunk.twoBytesAsInt (offset + LENGTH_IBAG + 2);
+ instrument.addZone (new Sf2InstrumentZone (generatorIndex, nextGeneratorIndex - generatorIndex, modulatorIndex, nextModulatorIndex - modulatorIndex));
}
}
}
@@ -582,6 +638,53 @@ private void parseInstrumentZones (final RIFFChunk chunk) throws ParseException
}
+ /**
+ * Parse the instrument modulators chunk (IMOD) and assign the parsed modulators to their
+ * instrument.
+ *
+ * @param chunk The chunk to parse
+ * @throws ParseException Error if the chunk is unsound
+ */
+ private void parseInstrumentModulators (final RIFFChunk chunk) throws ParseException
+ {
+ // Check for sound PMOD structure
+ final long size = chunk.getSize ();
+ if (size % LENGTH_IMOD > 0)
+ throw new ParseException (Functions.getMessage ("IDS_NOTIFY_ERR_BROKEN_INSTRUMENT_MODULATORS"));
+
+ for (int i = 0; i < Sf2File.this.instruments.size () - 1; i++)
+ {
+ final Sf2Instrument instrument = Sf2File.this.instruments.get (i);
+ for (int zoneIndex = 0; zoneIndex < instrument.getZoneCount (); zoneIndex++)
+ this.parseInstrumentZoneModulators (chunk, instrument.getZone (zoneIndex));
+ }
+ }
+
+
+ /**
+ * Parse all modulators of a instrument zone.
+ *
+ * @param chunk The chunk to parse
+ * @param zone The zone
+ */
+ private void parseInstrumentZoneModulators (final RIFFChunk chunk, final Sf2InstrumentZone zone)
+ {
+ final int firstModulator = zone.getFirstModulator ();
+ final int numberOfModulators = zone.getNumberOfModulators ();
+
+ for (int index = 0; index < numberOfModulators; index++)
+ {
+ final int offset = (firstModulator + index) * LENGTH_IMOD;
+ final int sourceModulator = chunk.twoBytesAsInt (offset);
+ final int destinationGenerator = chunk.twoBytesAsInt (offset + 2);
+ final int modAmount = chunk.twoBytesAsInt (offset + 4);
+ final int amountSourceOperand = chunk.twoBytesAsInt (offset + 6);
+ final int transformOperand = chunk.twoBytesAsInt (offset + 8);
+ zone.addModulator (sourceModulator, destinationGenerator, modAmount, amountSourceOperand, transformOperand);
+ }
+ }
+
+
/**
* Parse the instrument generators chunk (IGEN) and assign the parsed generators to their
* instrument.
@@ -616,10 +719,11 @@ private void parseInstrumentZoneGenerators (final RIFFChunk chunk, final Sf2Inst
final int firstGenerator = zone.getFirstGenerator ();
final int numberOfGenerators = zone.getNumberOfGenerators ();
- for (int index = firstGenerator; index < firstGenerator + numberOfGenerators; index++)
+ for (int index = 0; index < numberOfGenerators; index++)
{
- final int generator = chunk.twoBytesAsInt (LENGTH_IGEN * index);
- final int value = chunk.twoBytesAsInt (LENGTH_IGEN * index + 2);
+ final int offset = (firstGenerator + index) * LENGTH_IGEN;
+ final int generator = chunk.twoBytesAsInt (offset);
+ final int value = chunk.twoBytesAsInt (offset + 2);
zone.addGenerator (generator, value);
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2InstrumentZone.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2InstrumentZone.java
index ffd46d4..28cfaa7 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2InstrumentZone.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2InstrumentZone.java
@@ -23,10 +23,11 @@ public class Sf2InstrumentZone extends AbstractZone
* @param firstGenerator Index to the first generator of the zone in the IGEN list
* @param numberOfGenerators The number of generators in this zone
* @param firstModulator Index to the first modulator of the zone in the IMOD list
+ * @param numberOfModulators The number of modulators of this zone
*/
- public Sf2InstrumentZone (final int firstGenerator, final int numberOfGenerators, final int firstModulator)
+ public Sf2InstrumentZone (final int firstGenerator, final int numberOfGenerators, final int firstModulator, final int numberOfModulators)
{
- super (firstGenerator, numberOfGenerators, firstModulator);
+ super (firstGenerator, numberOfGenerators, firstModulator, numberOfModulators);
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2Modulator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2Modulator.java
new file mode 100644
index 0000000..f483975
--- /dev/null
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2Modulator.java
@@ -0,0 +1,160 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2021
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.sampleconverter.file.sf2;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+/**
+ * A SF2 modulator.
+ *
+ * @author Jürgen Moßgraber
+ */
+public class Sf2Modulator
+{
+ /** The ID for a Pitch Bend modulator. */
+ public static final Integer MODULATOR_PITCH_BEND = Integer.valueOf (14);
+
+ private static final Map MODULATOR_NAMES = new HashMap<> ();
+ static
+ {
+ /**
+ * No controller is to be used. The output of this controller module should be treated as if
+ * its value were set to ‘1’. It should not be a means to turn off a modulator.
+ */
+ MODULATOR_NAMES.put (Integer.valueOf (0), "No Controller");
+ /**
+ * The controller source to be used is the velocity value which is sent from the MIDI
+ * note-on command which generated the given sound.
+ */
+ MODULATOR_NAMES.put (Integer.valueOf (2), "Note-On Velocity");
+ /**
+ * The controller source to be used is the key number value which was sent from the MIDI
+ * note-on command which generated the given sound.
+ */
+ MODULATOR_NAMES.put (Integer.valueOf (3), "Note-On Key Number");
+ /**
+ * The controller source to be used is the poly-pressure amount that is sent from the MIDI
+ * poly-pressure command.
+ */
+ MODULATOR_NAMES.put (Integer.valueOf (10), "Poly Pressure");
+ /**
+ * The controller source to be used is the channel pressure amount that is sent from the
+ * MIDI channel-pressure command.
+ */
+ MODULATOR_NAMES.put (Integer.valueOf (13), "Channel Pressure");
+ /**
+ * The controller source to be used is the pitch wheel amount which is sent from the MIDI
+ * pitch wheel command.
+ */
+ MODULATOR_NAMES.put (MODULATOR_PITCH_BEND, "Pitch Wheel");
+ /**
+ * The controller source to be used is the pitch wheel sensitivity amount which is sent from
+ * the MIDI RPN 0 pitch wheel sensitivity command.
+ */
+ MODULATOR_NAMES.put (Integer.valueOf (16), "Pitch Wheel Sensitivity");
+ /**
+ * The controller source is the output of another modulator. This is NOT SUPPORTED as an
+ * Amount Source.
+ */
+ MODULATOR_NAMES.put (Integer.valueOf (127), "Link");
+ }
+
+ private final int controllerSource;
+ private final int destinationGenerator;
+ private final int modAmount;
+ private final int amountSourceOperand;
+ private final int transformOperand;
+
+
+ /**
+ * Constructor.
+ *
+ * @param sourceModulator The ID of the source modulator
+ * @param destinationGenerator The destination of the modulator
+ * @param modAmount A signed value indicating the degree to which the source modulates the
+ * destination
+ * @param amountSourceOperand Indicates the degree to which the source modulates the destination
+ * is to be controlled by the specified modulation source
+ * @param transformOperand Indicates that a transform of the specified type will be applied to
+ * the modulation source before application to the modulator
+ */
+ public Sf2Modulator (final int sourceModulator, final int destinationGenerator, final int modAmount, final int amountSourceOperand, final int transformOperand)
+ {
+ this.controllerSource = sourceModulator & 0x7F;
+ this.destinationGenerator = destinationGenerator;
+ this.modAmount = modAmount;
+ this.amountSourceOperand = amountSourceOperand;
+ this.transformOperand = transformOperand;
+ }
+
+
+ /**
+ * Get the ID of the controller source.
+ *
+ * @return The controller source
+ */
+ public int getControllerSource ()
+ {
+ return this.controllerSource;
+ }
+
+
+ /**
+ * Format all parameters into a string.
+ *
+ * @return The formatted string
+ */
+ public String printInfo ()
+ {
+ final StringBuilder sb = new StringBuilder ();
+
+ sb.append (" - Modulator: " + getModulatorName (this.controllerSource));
+ sb.append (" - Destination Generator: " + Generator.getGeneratorName (this.destinationGenerator) + " : " + this.modAmount + "\n");
+ sb.append (" - Amount Source Operand: " + getModulatorName (this.amountSourceOperand) + "\n");
+
+ return sb.toString ();
+ }
+
+
+ private static String getModulatorName (final int modulatorID)
+ {
+ return MODULATOR_NAMES.getOrDefault (Integer.valueOf (modulatorID), "Unknown");
+ }
+
+
+ /**
+ * Get the destination generator to be modulated.
+ *
+ * @return The destination generator
+ */
+ public int getDestinationGenerator ()
+ {
+ return this.destinationGenerator;
+ }
+
+
+ /**
+ * Get the modulation amount.
+ *
+ * @return The modulation amount
+ */
+ public int getModulationAmount ()
+ {
+ return this.modAmount;
+ }
+
+
+ /**
+ * Get the transformation operand.
+ *
+ * @return The transform operand
+ */
+ public int getTransformOperand ()
+ {
+ return this.transformOperand;
+ }
+}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2PresetZone.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2PresetZone.java
index b929d78..4260ce7 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2PresetZone.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2PresetZone.java
@@ -23,12 +23,13 @@ public class Sf2PresetZone extends AbstractZone
* Constructor.
*
* @param firstGenerator Index to the first generator of the zone in the PGEN list
- * @param numberOfGenerators The number of generators in this zone
+ * @param numberOfGenerators The number of generators of this zone
* @param firstModulator Index to the first modulator of the zone in the PMOD list
+ * @param numberOfModulators The number of modulators of this zone
*/
- public Sf2PresetZone (final int firstGenerator, final int numberOfGenerators, final int firstModulator)
+ public Sf2PresetZone (final int firstGenerator, final int numberOfGenerators, final int firstModulator, final int numberOfModulators)
{
- super (firstGenerator, numberOfGenerators, firstModulator);
+ super (firstGenerator, numberOfGenerators, firstModulator, numberOfModulators);
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCFilter.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCFilter.java
new file mode 100644
index 0000000..cfaaef6
--- /dev/null
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCFilter.java
@@ -0,0 +1,140 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2021
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.sampleconverter.format.akai;
+
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultFilter;
+
+
+/**
+ * MPC extension to a filters' settings.
+ *
+ * @author Jürgen Moßgraber
+ */
+public class MPCFilter extends DefaultFilter
+{
+ private static final int MAX = 30;
+
+ private static final FilterType [] FILTER_TYPES = new FilterType [MAX];
+ private static final int [] FILTER_POLES = new int [MAX];
+
+ static
+ {
+ // Low-pass 1 Pole
+ FILTER_TYPES[1] = FilterType.LOW_PASS;
+ FILTER_POLES[1] = 1;
+
+ // Low-pass 2 Pole
+ FILTER_TYPES[2] = FilterType.LOW_PASS;
+ FILTER_POLES[2] = 2;
+
+ // Low-pass 4 Pole
+ FILTER_TYPES[3] = FilterType.LOW_PASS;
+ FILTER_POLES[3] = 4;
+
+ // Low-pass 6 Pole
+ FILTER_TYPES[4] = FilterType.LOW_PASS;
+ FILTER_POLES[4] = 6;
+
+ // Low-pass 8 Pole
+ FILTER_TYPES[5] = FilterType.LOW_PASS;
+ FILTER_POLES[5] = 8;
+
+ // High-pass 1 Pole
+ FILTER_TYPES[6] = FilterType.HIGH_PASS;
+ FILTER_POLES[6] = 1;
+
+ // High-pass 2 Pole
+ FILTER_TYPES[7] = FilterType.HIGH_PASS;
+ FILTER_POLES[7] = 2;
+
+ // High-pass 4 Pole
+ FILTER_TYPES[8] = FilterType.HIGH_PASS;
+ FILTER_POLES[8] = 4;
+
+ // High-pass 6 Pole
+ FILTER_TYPES[9] = FilterType.HIGH_PASS;
+ FILTER_POLES[9] = 6;
+
+ // High-pass 8 Pole
+ FILTER_TYPES[10] = FilterType.HIGH_PASS;
+ FILTER_POLES[10] = 8;
+
+ // Bandpass 2 Pole
+ FILTER_TYPES[11] = FilterType.BAND_PASS;
+ FILTER_POLES[11] = 2;
+
+ // Bandpass 4 Pole
+ FILTER_TYPES[12] = FilterType.BAND_PASS;
+ FILTER_POLES[12] = 4;
+
+ // Bandpass 6 Pole
+ FILTER_TYPES[13] = FilterType.BAND_PASS;
+ FILTER_POLES[13] = 6;
+
+ // Bandpass 8 Pole
+ FILTER_TYPES[14] = FilterType.BAND_PASS;
+ FILTER_POLES[14] = 8;
+
+ // Band-stop 2 Pole
+ FILTER_TYPES[15] = FilterType.BAND_REJECTION;
+ FILTER_POLES[15] = 2;
+
+ // Band-stop 4 Pole
+ FILTER_TYPES[16] = FilterType.BAND_REJECTION;
+ FILTER_POLES[16] = 4;
+
+ // Band-stop 6 Pole
+ FILTER_TYPES[17] = FilterType.BAND_REJECTION;
+ FILTER_POLES[17] = 6;
+
+ // Band-stop 8 Pole
+ FILTER_TYPES[18] = FilterType.BAND_REJECTION;
+ FILTER_POLES[18] = 8;
+
+ // MPC Low-pass
+ FILTER_TYPES[29] = FilterType.LOW_PASS;
+ FILTER_POLES[29] = 4;
+ }
+
+
+ /**
+ * Constructor.
+ *
+ * @param id The index of the MPC filter
+ * @param cutoff The cutoff frequency
+ * @param resonance The resonance
+ */
+ public MPCFilter (final int id, final double cutoff, final double resonance)
+ {
+ super (null, 0, cutoff * MAX_FREQUENCY, resonance * 40.0);
+
+ if (id >= MAX)
+ return;
+
+ this.type = FILTER_TYPES[id];
+ this.poles = FILTER_POLES[id];
+ }
+
+
+ /**
+ * Get the index of the MPC filter depending on the given filter type and number of poles.
+ *
+ * @param filter The filter for which to get the index
+ * @return The index or 0 if it could not be mapped
+ */
+ public static int getFilterIndex (final IFilter filter)
+ {
+ final FilterType type = filter.getType ();
+ final int poles = filter.getPoles ();
+ for (int index = 1; index < MAX; index++)
+ {
+ if (FILTER_TYPES[index] == type && FILTER_POLES[index] == poles)
+ return index;
+ }
+ return 0;
+ }
+}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupCreator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupCreator.java
index cef5fe8..8dd5635 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupCreator.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupCreator.java
@@ -8,10 +8,11 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.creator.AbstractCreator;
import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
import de.mossgrabers.sampleconverter.util.XMLUtils;
import org.w3c.dom.Document;
@@ -149,7 +150,7 @@ private Optional createMetadata (final IMultisampleSource multisampleSou
{
for (final ISampleMetadata sampleMetadata: velocityLayer.getSampleMetadata ())
{
- final Optional keygroupOpt = getKeygroup (keygroupsMap, sampleMetadata, document, instrumentsElement);
+ final Optional keygroupOpt = getKeygroup (keygroupsMap, sampleMetadata, document, instrumentsElement, multisampleSource.getGlobalFilter ());
if (keygroupOpt.isEmpty ())
{
this.notifier.logError ("IDS_MPC_MORE_THAN_4_LAYERS", Integer.toString (sampleMetadata.getKeyLow ()), Integer.toString (sampleMetadata.getKeyHigh ()), Integer.toString (sampleMetadata.getVelocityLow ()), Integer.toString (sampleMetadata.getVelocityHigh ()));
@@ -193,7 +194,13 @@ private static Element createProgramElement (final Document document, final IMul
programElement.appendChild (document.createElement (MPCKeygroupTag.PROGRAM_PADS + APP_VERSION));
// Pitchbend 2 semitones up/down
- XMLUtils.addTextElement (document, programElement, MPCKeygroupTag.PROGRAM_PITCHBEND_RANGE, "0.160000");
+ final List layers = getNonEmptyLayers (multisampleSource.getLayers ());
+ if (!layers.isEmpty ())
+ {
+ final int bendUp = Math.abs (layers.get (0).getSampleMetadata ().get (0).getBendUp ());
+ final double bendUpValue = bendUp == 0 ? 0.16 : bendUp / 1200.0;
+ XMLUtils.addTextElement (document, programElement, MPCKeygroupTag.PROGRAM_PITCHBEND_RANGE, formatDouble (bendUpValue, 3));
+ }
// Vibrato on Modulation Wheel
XMLUtils.addTextElement (document, programElement, MPCKeygroupTag.PROGRAM_WHEEL_TO_LFO, "1.000000");
@@ -257,7 +264,7 @@ private static Element createLayerElement (final Document document, final int la
XMLUtils.addTextElement (document, layerElement, MPCKeygroupTag.LAYER_OFFSET, "0");
XMLUtils.addTextElement (document, layerElement, MPCKeygroupTag.LAYER_SLICE_START, Integer.toString (sampleMetadata.getStart ()));
- final List loops = sampleMetadata.getLoops ();
+ final List loops = sampleMetadata.getLoops ();
if (loops.isEmpty ())
{
XMLUtils.addTextElement (document, layerElement, MPCKeygroupTag.LAYER_SLICE_END, Integer.toString (sampleMetadata.getStop ()));
@@ -266,7 +273,7 @@ private static Element createLayerElement (final Document document, final int la
}
// Format can store only 1 loop
- final SampleLoop sampleLoop = loops.get (0);
+ final ISampleLoop sampleLoop = loops.get (0);
XMLUtils.addTextElement (document, layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP_START, Integer.toString (sampleLoop.getStart ()));
XMLUtils.addTextElement (document, layerElement, MPCKeygroupTag.LAYER_SLICE_END, Integer.toString (sampleLoop.getEnd ()));
XMLUtils.addTextElement (document, layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP, sampleMetadata.isReversed () ? "3" : "1");
@@ -290,7 +297,7 @@ private static Element createLfoElement (final Document document)
}
- private static Optional getKeygroup (final Map> keygroupsMap, final ISampleMetadata sampleMetadata, final Document document, final Element instrumentsElement)
+ private static Optional getKeygroup (final Map> keygroupsMap, final ISampleMetadata sampleMetadata, final Document document, final Element instrumentsElement, final Optional optFilter)
{
final int keyLow = sampleMetadata.getKeyLow ();
final int keyHigh = sampleMetadata.getKeyHigh ();
@@ -323,6 +330,29 @@ private static Optional getKeygroup (final Map>
final Element instrumentElement = document.createElement ("Instrument");
instrumentElement.setAttribute ("number", Integer.toString (calcInstrumentNumber (keygroupsMap)));
instrumentsElement.appendChild (instrumentElement);
+
+ if (optFilter.isPresent ())
+ {
+ final IFilter filter = optFilter.get ();
+ XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_TYPE, Integer.toString (MPCFilter.getFilterIndex (filter)));
+ XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_CUTOFF, formatDouble (normalizeFrequency (filter.getCutoff ()), 2));
+ XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_RESONANCE, formatDouble (Math.min (40, filter.getResonance ()) / 40.0, 2));
+
+ final int envelopeDepth = filter.getEnvelopeDepth ();
+ // Only positive modulation values are supported with MPC
+ if (envelopeDepth > 0)
+ {
+ XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_ENV_AMOUNT, formatDouble (envelopeDepth / (double) IFilter.MAX_ENVELOPE_DEPTH, 2));
+
+ final IEnvelope filterEnvelope = filter.getEnvelope ();
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_ATTACK, filterEnvelope.getAttack (), 0, 30, 0);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_HOLD, filterEnvelope.getHold (), 0, 30, 0);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_DECAY, filterEnvelope.getDecay (), 0, 30, 0);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_SUSTAIN, filterEnvelope.getSustain (), 0, 1, 1);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_RELEASE, filterEnvelope.getRelease (), 0, 30, 0.63);
+ }
+ }
+
XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_LOW_NOTE, Integer.toString (keyLow));
XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_HIGH_NOTE, Integer.toString (keyHigh));
XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_IGNORE_BASE_NOTE, sampleMetadata.getKeyTracking () == 0 ? "True" : "False");
@@ -334,6 +364,21 @@ private static Optional getKeygroup (final Map>
setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_SUSTAIN, amplitudeEnvelope.getSustain (), 0, 1, 1);
setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_RELEASE, amplitudeEnvelope.getRelease (), 0, 30, 0.63);
+ final int pitchDepth = sampleMetadata.getPitchEnvelopeDepth ();
+ // Only positive modulation values are supported with MPC
+ if (pitchDepth > 0)
+ {
+ final double mpcPitchDepth = clamp (pitchDepth, -3600, 3600) / 3600.0 / 2.0 + 0.5;
+ XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_ENV_AMOUNT, formatDouble (mpcPitchDepth, 2));
+
+ final IEnvelope pitchEnvelope = sampleMetadata.getPitchEnvelope ();
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_ATTACK, pitchEnvelope.getAttack (), 0, 30, 0);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_HOLD, pitchEnvelope.getHold (), 0, 30, 0);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_DECAY, pitchEnvelope.getDecay (), 0, 30, 0);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_SUSTAIN, pitchEnvelope.getSustain (), 0, 1, 1);
+ setEnvelopeAttribute (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_RELEASE, pitchEnvelope.getRelease (), 0, 30, 0.63);
+ }
+
XMLUtils.addTextElement (document, instrumentElement, MPCKeygroupTag.INSTRUMENT_ZONE_PLAY, ZonePlay.from (sampleMetadata.getPlayLogic ()).getID ());
instrumentElement.appendChild (createLfoElement (document));
final Element layersElement = document.createElement ("Layers");
@@ -349,6 +394,19 @@ private static Optional getKeygroup (final Map>
}
+ private static double normalizeFrequency (final double cutoff)
+ {
+ final double val = log2 (cutoff) / log2 (IFilter.MAX_FREQUENCY);
+ return Math.min (1, Math.max (0, val));
+ }
+
+
+ private static double log2 (final double value)
+ {
+ return Math.log (value) / Math.log (2);
+ }
+
+
private static int calcInstrumentNumber (final Map> keygroupsMap)
{
int count = 1;
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupDetectorTask.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupDetectorTask.java
index 42595b5..9e8703c 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupDetectorTask.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupDetectorTask.java
@@ -8,13 +8,14 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.detector.AbstractDetectorTask;
import de.mossgrabers.sampleconverter.core.detector.MultisampleSource;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
-import de.mossgrabers.sampleconverter.core.model.VelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleLoop;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultVelocityLayer;
import de.mossgrabers.sampleconverter.file.FileUtils;
import de.mossgrabers.sampleconverter.ui.IMetadataConfig;
import de.mossgrabers.sampleconverter.util.TagDetector;
@@ -165,8 +166,7 @@ private List parseDescription (final File file, final Docume
if (instrumentsElement == null)
return Collections.emptyList ();
- final String numKeygroupsStr = XMLUtils.getChildElementContent (programElement, MPCKeygroupTag.PROGRAM_NUM_KEYGROUPS);
- final int numKeygroups = numKeygroupsStr == null || numKeygroupsStr.isBlank () ? 128 : Integer.parseInt (numKeygroupsStr);
+ final int numKeygroups = XMLUtils.getChildElementIntegerContent (programElement, MPCKeygroupTag.PROGRAM_NUM_KEYGROUPS, 128);
final Element [] instrumentElements = XMLUtils.getChildElementsByName (instrumentsElement, MPCKeygroupTag.INSTRUMENTS_INSTRUMENT);
final List velocityLayers = this.parseVelocityLayers (file.getParentFile (), numKeygroups, instrumentElements, isDrum);
@@ -175,6 +175,21 @@ private List parseDescription (final File file, final Docume
this.applyPadNoteMap (programElement, velocityLayers);
multisampleSource.setVelocityLayers (velocityLayers);
+
+ final double pitchBendRange = XMLUtils.getChildElementDoubleContent (programElement, MPCKeygroupTag.PROGRAM_PITCHBEND_RANGE, 0);
+ if (pitchBendRange != 0)
+ {
+ final int pitchBend = (int) Math.round (pitchBendRange * 1200.0);
+ for (final IVelocityLayer layer: velocityLayers)
+ {
+ for (final ISampleMetadata sample: layer.getSampleMetadata ())
+ {
+ sample.setBendUp (pitchBend);
+ sample.setBendDown (-pitchBend);
+ }
+ }
+ }
+
return Collections.singletonList (multisampleSource);
}
@@ -203,16 +218,12 @@ private List parseVelocityLayers (final File basePath, final int
int keyHigh = instrumentNumber;
if (!isDrum)
{
- final String lowNoteStr = XMLUtils.getChildElementContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_LOW_NOTE);
- final String highNoteStr = XMLUtils.getChildElementContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_HIGH_NOTE);
- keyLow = lowNoteStr == null ? 0 : Integer.parseInt (lowNoteStr);
- keyHigh = highNoteStr == null ? 0 : Integer.parseInt (highNoteStr);
+ keyLow = XMLUtils.getChildElementIntegerContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_LOW_NOTE, 0);
+ keyHigh = XMLUtils.getChildElementIntegerContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_HIGH_NOTE, 0);
}
- final String velStartStr = XMLUtils.getChildElementContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_VEL_START);
- final String velEndStr = XMLUtils.getChildElementContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_VEL_END);
- final int velStart = velStartStr == null ? 0 : Integer.parseInt (velStartStr);
- final int velEnd = velEndStr == null ? 0 : Integer.parseInt (velEndStr);
+ final int velStart = XMLUtils.getChildElementIntegerContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_VEL_START, 0);
+ final int velEnd = XMLUtils.getChildElementIntegerContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_VEL_END, 0);
PlayLogic zonePlay;
try
@@ -232,11 +243,19 @@ private List parseVelocityLayers (final File basePath, final int
final String oneShotStr = XMLUtils.getChildElementContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_ONE_SHOT);
final boolean isOneShot = oneShotStr == null || MPCKeygroupTag.TRUE.equalsIgnoreCase (oneShotStr);
- final double attack = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_ATTACK, 0, 30, 0);
- final double hold = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_HOLD, 0, 30, 0);
- final double decay = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_DECAY, 0, 30, 0);
- final double sustain = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_SUSTAIN, 0, 1, 1);
- final double release = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_RELEASE, 0, 30, 0.63);
+ final IFilter filter = parseFilter (instrumentElement);
+
+ final double volumeAttack = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_ATTACK, 0, 30, 0);
+ final double volumeHold = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_HOLD, 0, 30, 0);
+ final double volumeDecay = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_DECAY, 0, 30, 0);
+ final double volumeSustain = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_SUSTAIN, 0, 1, 1);
+ final double volumeRelease = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_VOLUME_RELEASE, 0, 30, 0.63);
+
+ final double pitchAttack = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_ATTACK, 0, 30, 0);
+ final double pitchHold = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_HOLD, 0, 30, 0);
+ final double pitchDecay = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_DECAY, 0, 30, 0);
+ final double pitchSustain = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_SUSTAIN, 0, 1, 1);
+ final double pitchRelease = getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_RELEASE, 0, 30, 0.63);
final Element layersElement = XMLUtils.getChildElementByName (instrumentElement, MPCKeygroupTag.INSTRUMENT_LAYERS);
if (layersElement != null)
@@ -250,16 +269,32 @@ private List parseVelocityLayers (final File basePath, final int
samples.add (sampleMetadata);
final IEnvelope amplitudeEnvelope = sampleMetadata.getAmplitudeEnvelope ();
- amplitudeEnvelope.setAttack (attack);
- amplitudeEnvelope.setHold (hold);
- amplitudeEnvelope.setDecay (decay);
- amplitudeEnvelope.setSustain (sustain);
- amplitudeEnvelope.setRelease (release);
+ amplitudeEnvelope.setAttack (volumeAttack);
+ amplitudeEnvelope.setHold (volumeHold);
+ amplitudeEnvelope.setDecay (volumeDecay);
+ amplitudeEnvelope.setSustain (volumeSustain);
+ amplitudeEnvelope.setRelease (volumeRelease);
+
+ final double pitchEnvAmount = XMLUtils.getChildElementDoubleContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_PITCH_ENV_AMOUNT, 0.5);
+ if (pitchEnvAmount != 0.5)
+ {
+ final int cents = (int) Math.min (3600, Math.max (-3600, Math.round ((pitchEnvAmount - 0.5) * 2.0 * 3600.0)));
+ sampleMetadata.setPitchEnvelopeDepth (cents);
+
+ final IEnvelope pitchEnvelope = sampleMetadata.getPitchEnvelope ();
+ pitchEnvelope.setAttack (pitchAttack);
+ pitchEnvelope.setHold (pitchHold);
+ pitchEnvelope.setDecay (pitchDecay);
+ pitchEnvelope.setSustain (pitchSustain);
+ pitchEnvelope.setRelease (pitchRelease);
+ }
// No loop if it is a one-shot
if (!isOneShot)
parseLoop (layerElement, sampleMetadata);
+ sampleMetadata.setFilter (filter);
+
this.readMissingData (isDrum, isOneShot, sampleMetadata);
}
}
@@ -269,10 +304,43 @@ private List parseVelocityLayers (final File basePath, final int
}
+ /**
+ * Parse the filter settings from the instrument element.
+ *
+ * @param instrumentElement The instrument element
+ * @return The filter or null
+ */
+ private static IFilter parseFilter (final Element instrumentElement)
+ {
+ final int filterID = XMLUtils.getChildElementIntegerContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_TYPE, -1);
+ if (filterID <= 0)
+ return null;
+ final double cutoff = XMLUtils.getChildElementDoubleContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_CUTOFF, 1);
+ final double resonance = XMLUtils.getChildElementDoubleContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_RESONANCE, 0);
+ final MPCFilter filter = new MPCFilter (filterID, cutoff, resonance);
+ if (filter.getType () == null)
+ return null;
+
+ final double filterAmount = XMLUtils.getChildElementDoubleContent (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_ENV_AMOUNT, 0);
+ if (filterAmount > 0)
+ {
+ filter.setEnvelopeDepth ((int) Math.round (filterAmount * IFilter.MAX_ENVELOPE_DEPTH));
+
+ final IEnvelope filterEnvelope = filter.getEnvelope ();
+ filterEnvelope.setAttack (getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_ATTACK, 0, 30, 0));
+ filterEnvelope.setHold (getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_HOLD, 0, 30, 0));
+ filterEnvelope.setDecay (getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_DECAY, 0, 30, 0));
+ filterEnvelope.setSustain (getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_SUSTAIN, 0, 1, 1));
+ filterEnvelope.setRelease (getEnvelopeAttribute (instrumentElement, MPCKeygroupTag.INSTRUMENT_FILTER_RELEASE, 0, 30, 0.63));
+ }
+ return filter;
+ }
+
+
/**
* Parse the loop settings from the layer element.
*
- * @param layerElement THe layer element
+ * @param layerElement The layer element
* @param basePath The path where the XPM file is located, this is the base path for samples
* @param keyLow The lower key of the sample
* @param keyHigh The upper key of the sample
@@ -322,13 +390,8 @@ private DefaultSampleMetadata parseSampleData (final Element layerElement, final
sampleMetadata.setTune (Double.parseDouble (pitchStr));
else
{
- final String tuneCoarseStr = XMLUtils.getChildElementContent (layerElement, MPCKeygroupTag.LAYER_COARSE_TUNE);
- double pitch = 0;
- if (tuneCoarseStr != null && !tuneCoarseStr.isBlank ())
- pitch = Double.parseDouble (tuneCoarseStr);
- final String tuneFineStr = XMLUtils.getChildElementContent (layerElement, MPCKeygroupTag.LAYER_FINE_TUNE);
- if (tuneFineStr != null && !tuneFineStr.isBlank ())
- pitch += Double.parseDouble (tuneFineStr);
+ double pitch = XMLUtils.getChildElementDoubleContent (layerElement, MPCKeygroupTag.LAYER_COARSE_TUNE, 0);
+ pitch += XMLUtils.getChildElementDoubleContent (layerElement, MPCKeygroupTag.LAYER_FINE_TUNE, 0);
sampleMetadata.setTune (pitch);
}
@@ -389,11 +452,7 @@ private void readMissingData (final boolean isDrum, final boolean isOneShot, fin
private static void parseLoop (final Element layerElement, final DefaultSampleMetadata sampleMetadata)
{
// There might be no loop, forward or reverse
- final String sliceLoopStr = XMLUtils.getChildElementContent (layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP);
- if (sliceLoopStr == null)
- return;
-
- final int sliceLoop = Integer.parseInt (sliceLoopStr);
+ final int sliceLoop = XMLUtils.getChildElementIntegerContent (layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP, -1);
if (sliceLoop <= 0)
return;
@@ -401,15 +460,12 @@ private static void parseLoop (final Element layerElement, final DefaultSampleMe
if (sliceLoop == 3)
sampleMetadata.setReversed (true);
- final SampleLoop sampleLoop = new SampleLoop ();
- final String sliceLoopStartStr = XMLUtils.getChildElementContent (layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP_START);
- if (sliceLoopStartStr != null && !sliceLoopStartStr.isBlank ())
- sampleLoop.setStart (Integer.parseInt (sliceLoopStartStr));
+ final DefaultSampleLoop sampleLoop = new DefaultSampleLoop ();
+ final int sliceLoopStart = XMLUtils.getChildElementIntegerContent (layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP_START, -1);
+ if (sliceLoopStart >= 0)
+ sampleLoop.setStart (sliceLoopStart);
sampleLoop.setEnd (sampleMetadata.getStop ());
-
- final String sliceLoopCrossFadeLengthStr = XMLUtils.getChildElementContent (layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP_CROSSFADE);
- if (sliceLoopCrossFadeLengthStr != null && !sliceLoopCrossFadeLengthStr.isBlank ())
- sampleLoop.setCrossfade (Double.parseDouble (sliceLoopCrossFadeLengthStr));
+ sampleLoop.setCrossfade (XMLUtils.getChildElementDoubleContent (layerElement, MPCKeygroupTag.LAYER_SLICE_LOOP_CROSSFADE, 0));
sampleMetadata.getLoops ().add (sampleLoop);
}
@@ -430,7 +486,7 @@ private static List groupIntoLayers (final List new VelocityLayer ());
+ final IVelocityLayer velocityLayer = layerMap.computeIfAbsent (id, key -> new DefaultVelocityLayer ());
if (velocityLayer.getName () == null)
{
@@ -467,9 +523,9 @@ private void applyPadNoteMap (final Element programElement, final List= 0)
+ padNoteMap.put (Integer.valueOf (padNumber), Integer.valueOf (note));
}
for (final IVelocityLayer layer: velocityLayers)
@@ -509,10 +565,7 @@ private static double convertGain (final double volume)
private static double getEnvelopeAttribute (final Element element, final String attribute, final double minimum, final double maximum, final double defaultValue)
{
- final String content = XMLUtils.getChildElementContent (element, attribute);
- if (content.isBlank ())
- return defaultValue;
- final double value = Double.parseDouble (content);
+ final double value = XMLUtils.getChildElementDoubleContent (element, attribute, defaultValue);
return value < 0 ? defaultValue : denormalizeValue (value, minimum, maximum);
}
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupTag.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupTag.java
index e7060d9..e350bea 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupTag.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCKeygroupTag.java
@@ -15,151 +15,186 @@ public class MPCKeygroupTag
// Elements
/** The root element. */
- public static final String ROOT = "MPCVObject";
+ public static final String ROOT = "MPCVObject";
/** The program element. */
- public static final String ROOT_PROGRAM = "Program";
+ public static final String ROOT_PROGRAM = "Program";
/** The version element. */
- public static final String ROOT_VERSION = "Version";
+ public static final String ROOT_VERSION = "Version";
/** The file version element. */
- public static final String VERSION_FILE_VERSION = "File_Version";
+ public static final String VERSION_FILE_VERSION = "File_Version";
/** The program name element. */
- public static final String PROGRAM_NAME = "ProgramName";
+ public static final String PROGRAM_NAME = "ProgramName";
/** The program instrument element. */
- public static final String PROGRAM_INSTRUMENTS = "Instruments";
+ public static final String PROGRAM_INSTRUMENTS = "Instruments";
/** The program pad note map element. */
- public static final String PROGRAM_PAD_NOTE_MAP = "PadNoteMap";
+ public static final String PROGRAM_PAD_NOTE_MAP = "PadNoteMap";
/** The program number of keygroups element. */
- public static final String PROGRAM_NUM_KEYGROUPS = "KeygroupNumKeygroups";
+ public static final String PROGRAM_NUM_KEYGROUPS = "KeygroupNumKeygroups";
/** The program pads setup element. */
- public static final String PROGRAM_PADS = "ProgramPads-";
+ public static final String PROGRAM_PADS = "ProgramPads-";
/** The program keygroup pitch bend range element. */
- public static final String PROGRAM_PITCHBEND_RANGE = "KeygroupPitchBendRange";
+ public static final String PROGRAM_PITCHBEND_RANGE = "KeygroupPitchBendRange";
/** The program keygroup wheel to LFO element. */
- public static final String PROGRAM_WHEEL_TO_LFO = "KeygroupWheelToLfo";
+ public static final String PROGRAM_WHEEL_TO_LFO = "KeygroupWheelToLfo";
/** The instruments instrument element. */
- public static final String INSTRUMENTS_INSTRUMENT = "Instrument";
+ public static final String INSTRUMENTS_INSTRUMENT = "Instrument";
/** The low note element of the instrument element. */
- public static final String INSTRUMENT_LOW_NOTE = "LowNote";
+ public static final String INSTRUMENT_LOW_NOTE = "LowNote";
/** The high note element of the instrument element. */
- public static final String INSTRUMENT_HIGH_NOTE = "HighNote";
+ public static final String INSTRUMENT_HIGH_NOTE = "HighNote";
/** The velocity start element of the instrument element. */
- public static final String INSTRUMENT_VEL_START = "VelStart";
+ public static final String INSTRUMENT_VEL_START = "VelStart";
/** The velocity end element of the instrument element. */
- public static final String INSTRUMENT_VEL_END = "VelEnd";
+ public static final String INSTRUMENT_VEL_END = "VelEnd";
/** The ignore base note element of the instrument element. */
- public static final String INSTRUMENT_IGNORE_BASE_NOTE = "IgnoreBaseNote";
+ public static final String INSTRUMENT_IGNORE_BASE_NOTE = "IgnoreBaseNote";
/** The zone play element of the instrument element. */
- public static final String INSTRUMENT_ZONE_PLAY = "ZonePlay";
+ public static final String INSTRUMENT_ZONE_PLAY = "ZonePlay";
/** The one-shot element of the instrument element. */
- public static final String INSTRUMENT_ONE_SHOT = "OneShot";
+ public static final String INSTRUMENT_ONE_SHOT = "OneShot";
/** The layers element of the instrument element. */
- public static final String INSTRUMENT_LAYERS = "Layers";
+ public static final String INSTRUMENT_LAYERS = "Layers";
+
+ /** The filter type element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_TYPE = "FilterType";
+ /** The filter cutoff element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_CUTOFF = "Cutoff";
+ /** The filter resonance element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_RESONANCE = "Resonance";
+
+ /** The filter envelope amount element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_ENV_AMOUNT = "FilterEnvAmt";
+
+ /** The filter attack element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_ATTACK = "FilterAttack";
+ /** The filter hold element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_HOLD = "FilterHold";
+ /** The filter decay element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_DECAY = "FilterDecay";
+ /** The filter sustain element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_SUSTAIN = "FilterSustain";
+ /** The filter release element of the instrument element. */
+ public static final String INSTRUMENT_FILTER_RELEASE = "FilterRelease";
/** The volume attack element of the instrument element. */
- public static final String INSTRUMENT_VOLUME_ATTACK = "VolumeAttack";
+ public static final String INSTRUMENT_VOLUME_ATTACK = "VolumeAttack";
/** The volume hold element of the instrument element. */
- public static final String INSTRUMENT_VOLUME_HOLD = "VolumeHold";
+ public static final String INSTRUMENT_VOLUME_HOLD = "VolumeHold";
/** The volume decay element of the instrument element. */
- public static final String INSTRUMENT_VOLUME_DECAY = "VolumeDecay";
+ public static final String INSTRUMENT_VOLUME_DECAY = "VolumeDecay";
/** The volume sustain element of the instrument element. */
- public static final String INSTRUMENT_VOLUME_SUSTAIN = "VolumeSustain";
+ public static final String INSTRUMENT_VOLUME_SUSTAIN = "VolumeSustain";
/** The volume release element of the instrument element. */
- public static final String INSTRUMENT_VOLUME_RELEASE = "VolumeRelease";
+ public static final String INSTRUMENT_VOLUME_RELEASE = "VolumeRelease";
+
+ /** The pitch attack element of the instrument element. */
+ public static final String INSTRUMENT_PITCH_ATTACK = "PitchAttack";
+ /** The pitch hold element of the instrument element. */
+ public static final String INSTRUMENT_PITCH_HOLD = "PitchHold";
+ /** The pitch decay element of the instrument element. */
+ public static final String INSTRUMENT_PITCH_DECAY = "PitchDecay";
+ /** The pitch sustain element of the instrument element. */
+ public static final String INSTRUMENT_PITCH_SUSTAIN = "PitchSustain";
+ /** The pitch release element of the instrument element. */
+ public static final String INSTRUMENT_PITCH_RELEASE = "PitchRelease";
+
+ /** The pitch envelope amount element of the instrument element. */
+ public static final String INSTRUMENT_PITCH_ENV_AMOUNT = "PitchEnvAmount";
/** The layer element of the layers element. */
- public static final String LAYERS_LAYER = "Layer";
+ public static final String LAYERS_LAYER = "Layer";
/** The sample name element of the layer element. */
- public static final String LAYER_SAMPLE_NAME = "SampleName";
+ public static final String LAYER_SAMPLE_NAME = "SampleName";
/** The active element of the layer element. */
- public static final String LAYER_ACTIVE = "Active";
+ public static final String LAYER_ACTIVE = "Active";
/** The volume element of the layer element. */
- public static final String LAYER_VOLUME = "Volume";
+ public static final String LAYER_VOLUME = "Volume";
/** The panorama element of the layer element. */
- public static final String LAYER_PAN = "Pan";
+ public static final String LAYER_PAN = "Pan";
/** The pitch element of the layer element. */
- public static final String LAYER_PITCH = "Pitch";
+ public static final String LAYER_PITCH = "Pitch";
/** The coarse tune element of the layer element. */
- public static final String LAYER_COARSE_TUNE = "TuneCoarse";
+ public static final String LAYER_COARSE_TUNE = "TuneCoarse";
/** The fine tune element of the layer element. */
- public static final String LAYER_FINE_TUNE = "TuneFine";
+ public static final String LAYER_FINE_TUNE = "TuneFine";
/** The root note element of the layer element. */
- public static final String LAYER_ROOT_NOTE = "RootNote";
+ public static final String LAYER_ROOT_NOTE = "RootNote";
/** The key track element of the layer element. */
- public static final String LAYER_KEY_TRACK = "KeyTrack";
+ public static final String LAYER_KEY_TRACK = "KeyTrack";
/** The sample start element of the layer element. */
- public static final String LAYER_SAMPLE_START = "SampleStart";
+ public static final String LAYER_SAMPLE_START = "SampleStart";
/** The sample end element of the layer element. */
- public static final String LAYER_SAMPLE_END = "SampleEnd";
+ public static final String LAYER_SAMPLE_END = "SampleEnd";
/** The loop start element of the layer element. */
- public static final String LAYER_LOOP_START = "LoopStart";
+ public static final String LAYER_LOOP_START = "LoopStart";
/** The loop end element of the layer element. */
- public static final String LAYER_LOOP_END = "LoopEnd";
+ public static final String LAYER_LOOP_END = "LoopEnd";
/** The loop crossfade element of the layer element. */
- public static final String LAYER_LOOP_CROSSFADE = "LoopCrossfadeLength";
+ public static final String LAYER_LOOP_CROSSFADE = "LoopCrossfadeLength";
/** The loop tune element of the layer element. */
- public static final String LAYER_LOOP_TUNE = "LoopTune";
+ public static final String LAYER_LOOP_TUNE = "LoopTune";
/** The pitch randomization element of the layer element. */
- public static final String LAYER_PITCH_RANDOM = "PitchRandom";
+ public static final String LAYER_PITCH_RANDOM = "PitchRandom";
/** The volume randomization element of the layer element. */
- public static final String LAYER_VOLUME_RANDOM = "VolumeRandom";
+ public static final String LAYER_VOLUME_RANDOM = "VolumeRandom";
/** The panorama randomization element of the layer element. */
- public static final String LAYER_PAN_RANDOM = "PanRandom";
+ public static final String LAYER_PAN_RANDOM = "PanRandom";
/** The offset randomization element of the layer element. */
- public static final String LAYER_OFFSET_RANDOM = "OffsetRandom";
+ public static final String LAYER_OFFSET_RANDOM = "OffsetRandom";
/** The sample file element of the layer element. */
- public static final String LAYER_SAMPLE_FILE = "SampleFile";
+ public static final String LAYER_SAMPLE_FILE = "SampleFile";
/** The slice index element of the layer element. */
- public static final String LAYER_SLICE_INDEX = "SliceIndex";
+ public static final String LAYER_SLICE_INDEX = "SliceIndex";
/** The direction element of the layer element. */
- public static final String LAYER_DIRECTION = "Direction";
+ public static final String LAYER_DIRECTION = "Direction";
/** The offset element of the layer element. */
- public static final String LAYER_OFFSET = "Offset";
+ public static final String LAYER_OFFSET = "Offset";
/** The slice start element of the layer element. */
- public static final String LAYER_SLICE_START = "SliceStart";
+ public static final String LAYER_SLICE_START = "SliceStart";
/** The slice end element of the layer element. */
- public static final String LAYER_SLICE_END = "SliceEnd";
+ public static final String LAYER_SLICE_END = "SliceEnd";
/** The slice loop element of the layer element. */
- public static final String LAYER_SLICE_LOOP = "SliceLoop";
+ public static final String LAYER_SLICE_LOOP = "SliceLoop";
/** The slice loop start element of the layer element. */
- public static final String LAYER_SLICE_LOOP_START = "SliceLoopStart";
+ public static final String LAYER_SLICE_LOOP_START = "SliceLoopStart";
/** The slice loop crossfade element of the layer element. */
- public static final String LAYER_SLICE_LOOP_CROSSFADE = "SliceLoopCrossFadeLength";
+ public static final String LAYER_SLICE_LOOP_CROSSFADE = "SliceLoopCrossFadeLength";
/** The slice tail position element of the layer element. */
- public static final String LAYER_SLICE_TAIL_POSITION = "SliceTailPosition";
+ public static final String LAYER_SLICE_TAIL_POSITION = "SliceTailPosition";
/** The slice tail length element of the layer element. */
- public static final String LAYER_SLICE_TAIL_LENGTH = "SliceTailLength";
+ public static final String LAYER_SLICE_TAIL_LENGTH = "SliceTailLength";
/** The pad note element of the pad note map element. */
- public static final String PAD_NOTE_MAP_PAD_NOTE = "PadNote";
+ public static final String PAD_NOTE_MAP_PAD_NOTE = "PadNote";
/** The pad note element of the pad note map element. */
- public static final String PAD_NOTE_NOTE = "Note";
+ public static final String PAD_NOTE_NOTE = "Note";
///////////////////////////////////////////////////////
// Attributes
/** The type attribute of the program element. */
- public static final String PROGRAM_TYPE = "type";
+ public static final String PROGRAM_TYPE = "type";
/** The number attribute of the instrument element. */
- public static final String INSTRUMENT_NUMBER = "number";
+ public static final String INSTRUMENT_NUMBER = "number";
/** The number attribute of the pad note element. */
- public static final String PAD_NOTE_NUMBER = "number";
+ public static final String PAD_NOTE_NUMBER = "number";
/** The program type keygroup. */
- public static final String TYPE_KEYGROUP = "Keygroup";
+ public static final String TYPE_KEYGROUP = "Keygroup";
/** The program type drum. */
- public static final String TYPE_DRUM = "Drum";
+ public static final String TYPE_DRUM = "Drum";
/** The true value. */
- public static final String TRUE = "True";
+ public static final String TRUE = "True";
/**
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/ZonePlay.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/ZonePlay.java
index 4a1626d..6332cb2 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/ZonePlay.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/ZonePlay.java
@@ -4,7 +4,7 @@
package de.mossgrabers.sampleconverter.format.akai;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
/**
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleCreator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleCreator.java
index 962316d..424407e 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleCreator.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleCreator.java
@@ -7,11 +7,11 @@
import de.mossgrabers.sampleconverter.core.IMultisampleSource;
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.creator.AbstractCreator;
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.LoopType;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
+import de.mossgrabers.sampleconverter.core.model.enumeration.LoopType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
import de.mossgrabers.sampleconverter.util.XMLUtils;
import org.w3c.dom.Document;
@@ -192,10 +192,10 @@ private static void createSample (final Document document, final Element multisa
/////////////////////////////////////////////////////
// Loops
- final List loops = info.getLoops ();
+ final List loops = info.getLoops ();
if (!loops.isEmpty ())
{
- final SampleLoop sampleLoop = loops.get (0);
+ final ISampleLoop sampleLoop = loops.get (0);
final String type = sampleLoop.getType () == LoopType.ALTERNATING ? "ping-pong" : "loop";
final Element loopElement = XMLUtils.addElement (document, sampleElement, "loop");
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleDetectorTask.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleDetectorTask.java
index 7ae0408..f995230 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleDetectorTask.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/bitwig/BitwigMultisampleDetectorTask.java
@@ -8,12 +8,12 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.detector.AbstractDetectorTask;
import de.mossgrabers.sampleconverter.core.detector.MultisampleSource;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.LoopType;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
-import de.mossgrabers.sampleconverter.core.model.VelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.enumeration.LoopType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleLoop;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultVelocityLayer;
import de.mossgrabers.sampleconverter.util.XMLUtils;
import org.w3c.dom.Document;
@@ -153,7 +153,7 @@ private List parseDescription (final File multiSampleFile, f
final String k = groupElement.getAttribute ("name");
final String layerName = k.isBlank () ? "Velocity Layer " + (groupCounter + 1) : k;
- indexedVelocityLayers.put (Integer.valueOf (groupCounter), new VelocityLayer (layerName));
+ indexedVelocityLayers.put (Integer.valueOf (groupCounter), new DefaultVelocityLayer (layerName));
groupCounter++;
}
else
@@ -163,7 +163,7 @@ private List parseDescription (final File multiSampleFile, f
}
}
// Additional layer for potentially un-grouped samples
- indexedVelocityLayers.put (Integer.valueOf (-1), new VelocityLayer ());
+ indexedVelocityLayers.put (Integer.valueOf (-1), new DefaultVelocityLayer ());
// Parse (deprecated) layer tag
final Node [] layerNodes = XMLUtils.getChildrenByName (top, BitwigMultisampleTag.LAYER);
@@ -175,7 +175,7 @@ private List parseDescription (final File multiSampleFile, f
final String k = layerElement.getAttribute ("name");
final String layerName = k == null || k.isBlank () ? "Velocity Layer " + (groupCounter + 1) : k;
- indexedVelocityLayers.put (Integer.valueOf (groupCounter), new VelocityLayer (layerName));
+ indexedVelocityLayers.put (Integer.valueOf (groupCounter), new DefaultVelocityLayer (layerName));
groupCounter++;
// Parse all samples of the layer
@@ -263,7 +263,7 @@ private void parseSample (final File zipFile, final Map
this.checkChildTags (BitwigMultisampleTag.SAMPLE, BitwigMultisampleTag.SAMPLE_TAGS, XMLUtils.getChildElements (sampleElement));
final int groupIndex = XMLUtils.getIntegerAttribute (sampleElement, BitwigMultisampleTag.GROUP, -1);
- final IVelocityLayer velocityLayer = indexedVelocityLayers.computeIfAbsent (Integer.valueOf (groupIndex), groupIdx -> new VelocityLayer ("Velocity layer " + (groupIdx.intValue () + 1)));
+ final IVelocityLayer velocityLayer = indexedVelocityLayers.computeIfAbsent (Integer.valueOf (groupIndex), groupIdx -> new DefaultVelocityLayer ("Velocity layer " + (groupIdx.intValue () + 1)));
final String filename = sampleElement.getAttribute ("file");
if (filename == null || filename.isBlank ())
@@ -272,7 +272,7 @@ private void parseSample (final File zipFile, final Map
return;
}
- final DefaultSampleMetadata sampleMetadata = new DefaultSampleMetadata (zipFile, filename);
+ final DefaultSampleMetadata sampleMetadata = new DefaultSampleMetadata (zipFile, new File (filename));
sampleMetadata.setStart ((int) Math.round (XMLUtils.getDoubleAttribute (sampleElement, "sample-start", -1)));
sampleMetadata.setStop ((int) Math.round (XMLUtils.getDoubleAttribute (sampleElement, "sample-stop", -1)));
@@ -326,7 +326,7 @@ else if ("false".equals (attribute))
final String attribute = loopElement.getAttribute ("mode");
if (attribute != null)
{
- final SampleLoop loop = new SampleLoop ();
+ final DefaultSampleLoop loop = new DefaultSampleLoop ();
switch (attribute)
{
default:
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerCreator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerCreator.java
index 0feaa54..8ffa1eb 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerCreator.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerCreator.java
@@ -8,10 +8,12 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.creator.AbstractCreator;
import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
import de.mossgrabers.sampleconverter.ui.tools.BasicConfig;
import de.mossgrabers.sampleconverter.ui.tools.panel.BoxPanel;
import de.mossgrabers.sampleconverter.util.XMLUtils;
@@ -47,8 +49,6 @@
public class DecentSamplerCreator extends AbstractCreator
{
private static final String MOD_AMP = "amp";
- private static final String MOD_EFFECT = "effect";
- private static final String EFFECTS_EFFECT = "effect";
private static final String FOLDER_POSTFIX = "Samples";
private boolean isOutputFormatLibrary;
@@ -64,6 +64,8 @@ public class DecentSamplerCreator extends AbstractCreator
private CheckBox addReverbBox;
private CheckBox makeMonophonicBox;
+ private int seqPosition = 1;
+
/**
* Constructor.
@@ -222,19 +224,34 @@ private Optional createMetadata (final String folderName, final IMultisa
final Element groupsElement = XMLUtils.addElement (document, multisampleElement, DecentSamplerTag.GROUPS);
final List velocityLayers = getNonEmptyLayers (multisampleSource.getLayers ());
+ boolean hasRoundRobin = false;
+
IEnvelope amplitudeEnvelope = null;
if (!velocityLayers.isEmpty ())
{
final ISampleMetadata sampleMetadata = velocityLayers.get (0).getSampleMetadata ().get (0);
amplitudeEnvelope = sampleMetadata.getAmplitudeEnvelope ();
addVolumeEnvelope (amplitudeEnvelope, groupsElement);
+
+ final PlayLogic playLogic = sampleMetadata.getPlayLogic ();
+ hasRoundRobin = playLogic != PlayLogic.ALWAYS;
+ if (hasRoundRobin)
+ groupsElement.setAttribute (DecentSamplerTag.SEQ_MODE, "round_robin");
}
this.createUI (document, multisampleElement, amplitudeEnvelope);
+ // Add all layers
+
for (final IVelocityLayer layer: velocityLayers)
{
final Element groupElement = XMLUtils.addElement (document, groupsElement, DecentSamplerTag.GROUP);
+ if (hasRoundRobin)
+ {
+ groupElement.setAttribute (DecentSamplerTag.SEQ_POSITION, Integer.toString (this.seqPosition));
+ this.seqPosition++;
+ }
+
final String name = layer.getName ();
if (name != null && !name.isBlank ())
groupElement.setAttribute ("name", name);
@@ -244,7 +261,7 @@ private Optional createMetadata (final String folderName, final IMultisa
}
this.makeMonophonic (document, multisampleElement, groupsElement);
- this.createEffects (document, multisampleElement);
+ this.createEffects (document, multisampleElement, multisampleSource);
try
{
@@ -278,7 +295,7 @@ private void makeMonophonic (final Document document, final Element multisampleE
* @param groupElement The element where to add the sample information
* @param info Where to get the sample info from
*/
- private static void createSample (final Document document, final String folderName, final Element groupElement, final ISampleMetadata info)
+ private void createSample (final Document document, final String folderName, final Element groupElement, final ISampleMetadata info)
{
/////////////////////////////////////////////////////
// Sample element and attributes
@@ -286,7 +303,7 @@ private static void createSample (final Document document, final String folderNa
final Element sampleElement = XMLUtils.addElement (document, groupElement, DecentSamplerTag.SAMPLE);
final Optional filename = info.getUpdatedFilename ();
if (filename.isPresent ())
- sampleElement.setAttribute (DecentSamplerTag.PATH, new StringBuilder ().append (folderName).append ('/').append (filename.get ()).toString ());
+ sampleElement.setAttribute (DecentSamplerTag.PATH, formatFileName (folderName, filename.get ()));
final double gain = info.getGain ();
if (gain != 0)
@@ -302,10 +319,6 @@ private static void createSample (final Document document, final String folderNa
// No info.isReversed ()
- final PlayLogic playLogic = info.getPlayLogic ();
- if (playLogic != PlayLogic.ALWAYS)
- sampleElement.setAttribute (DecentSamplerTag.SEQ_MODE, "round_robin");
-
/////////////////////////////////////////////////////
// Key & Velocity attributes
@@ -323,11 +336,11 @@ private static void createSample (final Document document, final String folderNa
/////////////////////////////////////////////////////
// Loops
- final List loops = info.getLoops ();
+ final List loops = info.getLoops ();
if (!loops.isEmpty ())
{
- final SampleLoop sampleLoop = loops.get (0);
+ final ISampleLoop sampleLoop = loops.get (0);
sampleElement.setAttribute (DecentSamplerTag.LOOP_ENABLED, "true");
XMLUtils.setDoubleAttribute (sampleElement, DecentSamplerTag.LOOP_START, check (sampleLoop.getStart (), 0), 3);
XMLUtils.setDoubleAttribute (sampleElement, DecentSamplerTag.LOOP_END, check (sampleLoop.getEnd (), stop), 3);
@@ -371,25 +384,46 @@ private boolean isOutputFormatLibrary ()
*
* @param document The XML document
* @param rootElement Where to add the effect elements
+ * @param multisampleSource The multi-sample
*/
- private void createEffects (final Document document, final Element rootElement)
+ private void createEffects (final Document document, final Element rootElement, final IMultisampleSource multisampleSource)
{
- if (this.addFilterBox.isSelected () || this.addReverbBox.isSelected ())
+ final Optional optFilter = multisampleSource.getGlobalFilter ();
+
+ final boolean lowPassFilterIsPresent = optFilter.isPresent () && optFilter.get ().getType () == FilterType.LOW_PASS;
+ final boolean hasFilter = this.addFilterBox.isSelected () || lowPassFilterIsPresent;
+ final boolean hasReverb = this.addReverbBox.isSelected ();
+
+ if (hasFilter || hasReverb)
{
- final Element effectsElement = XMLUtils.addElement (document, rootElement, "effects");
+ final Element effectsElement = XMLUtils.addElement (document, rootElement, DecentSamplerTag.EFFECTS);
- if (this.addFilterBox.isSelected ())
+ if (hasFilter)
{
- final Element effectElement = XMLUtils.addElement (document, effectsElement, EFFECTS_EFFECT);
- effectElement.setAttribute ("type", "lowpass_4pl");
- effectElement.setAttribute ("resonance", "0.5");
- effectElement.setAttribute ("frequency", "22000");
+ final Element filterElement = XMLUtils.addElement (document, effectsElement, DecentSamplerTag.EFFECTS_EFFECT);
+ filterElement.setAttribute ("type", "lowpass_4pl");
+ if (lowPassFilterIsPresent)
+ {
+ // Note: this might not be a 4 pole low-pass but better than no filter...
+ final IFilter filter = optFilter.get ();
+ // Note: Resonance is in the range [0..1] but it is not documented what value 1
+ // represents. Therefore, we assume 40dB maximum and a linear range (could also
+ // be logarithmic).
+ final double resonance = Math.min (40, filter.getResonance ());
+ filterElement.setAttribute ("resonance", formatDouble (resonance / 40.0, 3));
+ filterElement.setAttribute ("frequency", formatDouble (filter.getCutoff (), 2));
+ }
+ else
+ {
+ filterElement.setAttribute ("resonance", "0.5");
+ filterElement.setAttribute ("frequency", "22000");
+ }
}
- if (this.addReverbBox.isSelected ())
+ if (hasReverb)
{
- final Element effectElement = XMLUtils.addElement (document, effectsElement, EFFECTS_EFFECT);
- effectElement.setAttribute ("type", "reverb");
+ final Element reverbElement = XMLUtils.addElement (document, effectsElement, DecentSamplerTag.EFFECTS_EFFECT);
+ reverbElement.setAttribute ("type", "reverb");
}
}
}
@@ -413,22 +447,22 @@ private void createUI (final Document document, final Element root, final IEnvel
if (this.addFilterBox.isSelected ())
{
knobElement = createKnob (document, tabElement, 0, 0, "Filter Cutoff", 22000, 22000);
- createBinding (document, knobElement, MOD_EFFECT, "FX_FILTER_FREQUENCY");
+ createBinding (document, knobElement, DecentSamplerTag.MOD_EFFECT, "FX_FILTER_FREQUENCY");
knobElement = createKnob (document, tabElement, 100, 0, "Filter Resonance", 2, 0.01);
- createBinding (document, knobElement, MOD_EFFECT, "FX_FILTER_RESONANCE");
+ createBinding (document, knobElement, DecentSamplerTag.MOD_EFFECT, "FX_FILTER_RESONANCE");
}
if (this.addReverbBox.isSelected ())
{
knobElement = createKnob (document, tabElement, 200, 0, "Reverb Wet Level", 1000, 0);
- Element bindingElement = createBinding (document, knobElement, MOD_EFFECT, "FX_REVERB_WET_LEVEL");
+ Element bindingElement = createBinding (document, knobElement, DecentSamplerTag.MOD_EFFECT, "FX_REVERB_WET_LEVEL");
bindingElement.setAttribute ("position", "1");
bindingElement.setAttribute ("translation", "linear");
bindingElement.setAttribute ("translationOutputMax", "1");
bindingElement.setAttribute ("translationOutputMin", "0.0");
knobElement = createKnob (document, tabElement, 300, 0, "Reverb Room Size", 1000, 0);
- bindingElement = createBinding (document, knobElement, MOD_EFFECT, "FX_REVERB_ROOM_SIZE");
+ bindingElement = createBinding (document, knobElement, DecentSamplerTag.MOD_EFFECT, "FX_REVERB_ROOM_SIZE");
bindingElement.setAttribute ("position", "1");
bindingElement.setAttribute ("translation", "linear");
bindingElement.setAttribute ("translationOutputMax", "1");
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerDetectorTask.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerDetectorTask.java
index 0332098..035ada7 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerDetectorTask.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerDetectorTask.java
@@ -8,12 +8,15 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.detector.AbstractDetectorTask;
import de.mossgrabers.sampleconverter.core.detector.MultisampleSource;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
-import de.mossgrabers.sampleconverter.core.model.VelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultFilter;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleLoop;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultVelocityLayer;
import de.mossgrabers.sampleconverter.file.FileUtils;
import de.mossgrabers.sampleconverter.ui.IMetadataConfig;
import de.mossgrabers.sampleconverter.util.TagDetector;
@@ -212,10 +215,38 @@ private List parseMetadataFile (final File multiSampleFile,
multisampleSource.setVelocityLayers (velocityLayers);
+ parseEffects (top, multisampleSource);
+
return Collections.singletonList (multisampleSource);
}
+ /**
+ * Parse the effects on the top level.
+ *
+ * @param top The top element
+ * @param multisampleSource The multisample to fill
+ */
+ private static void parseEffects (final Element top, final MultisampleSource multisampleSource)
+ {
+ final Element effectsElement = XMLUtils.getChildElementByName (top, DecentSamplerTag.EFFECTS);
+ if (effectsElement == null)
+ return;
+
+ for (final Element effectElement: XMLUtils.getChildElementsByName (top, DecentSamplerTag.EFFECTS_EFFECT))
+ {
+ final String effectType = effectElement.getAttribute ("type");
+ if ("lowpass_4pl".equals (effectType))
+ {
+ final double frequency = XMLUtils.getDoubleAttribute (effectElement, "frequency", IFilter.MAX_FREQUENCY);
+ final double resonance = XMLUtils.getDoubleAttribute (effectElement, "resonance", 0);
+ multisampleSource.setGlobalFilter (new DefaultFilter (FilterType.LOW_PASS, 4, frequency, resonance));
+ return;
+ }
+ }
+ }
+
+
/**
* Parses all velocity layers (groups).
*
@@ -240,7 +271,7 @@ private List parseVelocityLayers (final Element top, final Strin
final String k = groupElement.getAttribute (DecentSamplerTag.GROUP_NAME);
final String layerName = k == null || k.isBlank () ? "Velocity Layer " + groupCounter : k;
- final VelocityLayer velocityLayer = new VelocityLayer (layerName);
+ final DefaultVelocityLayer velocityLayer = new DefaultVelocityLayer (layerName);
final double groupVolumeOffset = parseVolume (groupElement, DecentSamplerTag.VOLUME);
double groupTuningOffset = XMLUtils.getDoubleAttribute (groupElement, DecentSamplerTag.GROUP_TUNING, 0);
@@ -272,7 +303,7 @@ private List parseVelocityLayers (final Element top, final Strin
* @param groupVolumeOffset The volume offset
* @param tuningOffset The tuning offset
*/
- private void parseVelocityLayer (final VelocityLayer velocityLayer, final Element groupElement, final String basePath, final File libraryFile, final double groupVolumeOffset, final double tuningOffset)
+ private void parseVelocityLayer (final DefaultVelocityLayer velocityLayer, final Element groupElement, final String basePath, final File libraryFile, final double groupVolumeOffset, final double tuningOffset)
{
for (final Element sampleElement: XMLUtils.getChildElementsByName (groupElement, DecentSamplerTag.SAMPLE))
{
@@ -289,22 +320,22 @@ private void parseVelocityLayer (final VelocityLayer velocityLayer, final Elemen
}
final DefaultSampleMetadata sampleMetadata;
+ final File sampleFile = new File (basePath, sampleName);
if (libraryFile == null)
{
- final File sampleFile = new File (basePath, sampleName);
if (!this.checkSampleFile (sampleFile))
return;
sampleMetadata = new DefaultSampleMetadata (sampleFile);
}
else
- sampleMetadata = new DefaultSampleMetadata (libraryFile, new File (basePath, sampleName).getPath ().replace ('\\', '/'));
+ sampleMetadata = new DefaultSampleMetadata (libraryFile, sampleFile);
sampleMetadata.setStart ((int) Math.round (XMLUtils.getDoubleAttribute (sampleElement, DecentSamplerTag.START, -1)));
sampleMetadata.setStop ((int) Math.round (XMLUtils.getDoubleAttribute (sampleElement, DecentSamplerTag.END, -1)));
sampleMetadata.setGain (groupVolumeOffset + parseVolume (sampleElement, DecentSamplerTag.VOLUME));
- sampleMetadata.setTune ((tuningOffset + XMLUtils.getDoubleAttribute (sampleElement, DecentSamplerTag.TUNING, 0)) * 100.0);
+ sampleMetadata.setTune ((tuningOffset + XMLUtils.getDoubleAttribute (sampleElement, DecentSamplerTag.TUNING, 0)));
- final String zoneLogic = sampleElement.getAttribute (DecentSamplerTag.SEQ_MODE);
+ final String zoneLogic = this.currentGroupsElement.getAttribute (DecentSamplerTag.SEQ_MODE);
sampleMetadata.setPlayLogic (zoneLogic != null && "round_robin".equals (zoneLogic) ? PlayLogic.ROUND_ROBIN : PlayLogic.ALWAYS);
sampleMetadata.setKeyTracking (XMLUtils.getDoubleAttribute (sampleElement, DecentSamplerTag.PITCH_KEY_TRACK, 1));
@@ -323,7 +354,7 @@ private void parseVelocityLayer (final VelocityLayer velocityLayer, final Elemen
if (loopStart >= 0 || loopEnd > 0 || loopCrossfade > 0)
{
- final SampleLoop loop = new SampleLoop ();
+ final DefaultSampleLoop loop = new DefaultSampleLoop ();
loop.setStart (loopStart);
loop.setEnd (loopEnd);
final int loopLength = loopEnd - loopStart;
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerTag.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerTag.java
index 38e4f64..81f3612 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerTag.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/decentsampler/DecentSamplerTag.java
@@ -20,6 +20,13 @@ public class DecentSamplerTag
/** The root tag. */
public static final String DECENTSAMPLER = "DecentSampler";
+ /** The effects tag. */
+ public static final String EFFECTS = "effects";
+ /** The modulation effects tag. */
+ public static final String MOD_EFFECT = "effect";
+ /** The effects tag. */
+ public static final String EFFECTS_EFFECT = "effect";
+
/** The user interface tag. */
public static final String UI = "ui";
/** The tabulator tag. */
@@ -41,6 +48,8 @@ public class DecentSamplerTag
public static final String GROUP = "group";
/** The sample tag. */
public static final String SAMPLE = "sample";
+ /** The sequence mode tag. */
+ public static final String SEQ_MODE = "seqMode";
/** The global tuning attribute. */
public static final String GROUP_TUNING = "groupTuning";
@@ -57,8 +66,8 @@ public class DecentSamplerTag
public static final String END = "end";
/** The tuning tag sample attribute. */
public static final String TUNING = "tuning";
- /** The sequence mode tag sample attribute. */
- public static final String SEQ_MODE = "seqMode";
+ /** The sequence position tag sample attribute. */
+ public static final String SEQ_POSITION = "seqPosition";
/** The root note tag sample attribute. */
public static final String ROOT_NOTE = "rootNote";
/** The pitch key tracking tag sample attribute. */
@@ -91,7 +100,7 @@ public class DecentSamplerTag
public static final String AMP_ENV_RELEASE = "release";
/** The supported top level tags. */
- public static final Set TOP_LEVEL_TAGS = Set.of (GROUPS);
+ public static final Set TOP_LEVEL_TAGS = Set.of (EFFECTS, UI, GROUPS);
/** The supported group tags. */
public static final Set GROUP_TAGS = Set.of (SAMPLE);
/** The supported sample tags. */
@@ -103,9 +112,9 @@ public class DecentSamplerTag
static
{
ATTRIBUTES.put (DECENTSAMPLER, Collections.emptySet ());
- ATTRIBUTES.put (GROUPS, Set.of (GLOBAL_TUNING, AMP_ENV_ATTACK, AMP_ENV_DECAY, AMP_ENV_SUSTAIN, AMP_ENV_RELEASE));
- ATTRIBUTES.put (GROUP, Set.of (GROUP_NAME, GROUP_TUNING, TUNING, VOLUME, AMP_ENV_ATTACK, AMP_ENV_DECAY, AMP_ENV_SUSTAIN, AMP_ENV_RELEASE));
- ATTRIBUTES.put (SAMPLE, Set.of (PATH, ROOT_NOTE, LO_NOTE, HI_NOTE, LO_VEL, HI_VEL, START, END, TUNING, VOLUME, PITCH_KEY_TRACK, LOOP_START, LOOP_END, LOOP_CROSSFADE, LOOP_ENABLED, SEQ_MODE, AMP_ENV_ATTACK, AMP_ENV_DECAY, AMP_ENV_SUSTAIN, AMP_ENV_RELEASE));
+ ATTRIBUTES.put (GROUPS, Set.of (GLOBAL_TUNING, SEQ_MODE, AMP_ENV_ATTACK, AMP_ENV_DECAY, AMP_ENV_SUSTAIN, AMP_ENV_RELEASE));
+ ATTRIBUTES.put (GROUP, Set.of (GROUP_NAME, SEQ_POSITION, GROUP_TUNING, TUNING, VOLUME, AMP_ENV_ATTACK, AMP_ENV_DECAY, AMP_ENV_SUSTAIN, AMP_ENV_RELEASE));
+ ATTRIBUTES.put (SAMPLE, Set.of (PATH, ROOT_NOTE, LO_NOTE, HI_NOTE, LO_VEL, HI_VEL, START, END, TUNING, VOLUME, PITCH_KEY_TRACK, LOOP_START, LOOP_END, LOOP_CROSSFADE, LOOP_ENABLED, AMP_ENV_ATTACK, AMP_ENV_DECAY, AMP_ENV_SUSTAIN, AMP_ENV_RELEASE));
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleCreator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleCreator.java
index cf97b42..5fc00ca 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleCreator.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleCreator.java
@@ -7,9 +7,9 @@
import de.mossgrabers.sampleconverter.core.IMultisampleSource;
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.creator.AbstractCreator;
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
import java.io.ByteArrayOutputStream;
import java.io.File;
@@ -187,7 +187,7 @@ private static void writeSampleParameters (final ByteArrayOutputStream sampleOut
write7bitNumberLSB (sampleOutput, start);
}
- final List loops = sample.getLoops ();
+ final List loops = sample.getLoops ();
if (!loops.isEmpty ())
{
final int loopStart = loops.get (0).getStart ();
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleDetectorTask.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleDetectorTask.java
index f95547d..91725b0 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleDetectorTask.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/korgmultisample/KorgmultisampleDetectorTask.java
@@ -8,11 +8,11 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.detector.AbstractDetectorTask;
import de.mossgrabers.sampleconverter.core.detector.MultisampleSource;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
-import de.mossgrabers.sampleconverter.core.model.VelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultVelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleLoop;
import de.mossgrabers.sampleconverter.exception.FormatException;
import de.mossgrabers.sampleconverter.file.FileUtils;
@@ -108,7 +108,7 @@ private List parseFile (final InputStream in, final File fil
final MultisampleSource multisampleSource = new MultisampleSource (file, parts, name, this.subtractPaths (this.sourceFolder, file));
final List velocityLayers = new ArrayList<> ();
// There is only one layer (no velocity zones)
- final VelocityLayer velocityLayer = new VelocityLayer ("Layer");
+ final DefaultVelocityLayer velocityLayer = new DefaultVelocityLayer ("Layer");
velocityLayers.add (velocityLayer);
int id;
@@ -291,7 +291,7 @@ private static int parseSampleParameters (final ISampleMetadata sample, final In
if (!oneShot)
{
- final SampleLoop loop = new SampleLoop ();
+ final DefaultSampleLoop loop = new DefaultSampleLoop ();
loop.setStart (loopStart);
loop.setEnd (sample.getStop ());
sample.addLoop (loop);
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2DetectorTask.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2DetectorTask.java
index a5fbf4c..633113c 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2DetectorTask.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2DetectorTask.java
@@ -9,16 +9,20 @@
import de.mossgrabers.sampleconverter.core.detector.AbstractDetectorTask;
import de.mossgrabers.sampleconverter.core.detector.MultisampleSource;
import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
-import de.mossgrabers.sampleconverter.core.model.VelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultFilter;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleLoop;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultVelocityLayer;
import de.mossgrabers.sampleconverter.exception.ParseException;
import de.mossgrabers.sampleconverter.file.FileUtils;
import de.mossgrabers.sampleconverter.file.sf2.Generator;
import de.mossgrabers.sampleconverter.file.sf2.Sf2File;
import de.mossgrabers.sampleconverter.file.sf2.Sf2Instrument;
import de.mossgrabers.sampleconverter.file.sf2.Sf2InstrumentZone;
+import de.mossgrabers.sampleconverter.file.sf2.Sf2Modulator;
import de.mossgrabers.sampleconverter.file.sf2.Sf2Preset;
import de.mossgrabers.sampleconverter.file.sf2.Sf2PresetZone;
import de.mossgrabers.sampleconverter.file.sf2.Sf2SampleDescriptor;
@@ -30,6 +34,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.function.Consumer;
@@ -113,7 +118,7 @@ private List parseSF2File (final File sourceFile, final Sf2F
generators.setPresetZoneGenerators (zone.getGenerators ());
final Sf2Instrument instrument = zone.getInstrument ();
- final VelocityLayer layer = new VelocityLayer (instrument.getName ());
+ final DefaultVelocityLayer layer = new DefaultVelocityLayer (instrument.getName ());
for (int instrumentZoneIndex = 0; instrumentZoneIndex < instrument.getZoneCount (); instrumentZoneIndex++)
{
@@ -124,7 +129,9 @@ private List parseSF2File (final File sourceFile, final Sf2F
continue;
}
generators.setInstrumentZoneGenerators (instrZone.getGenerators ());
- layer.addSampleMetadata (createSampleMetadata (instrZone.getSample (), generators));
+ final Sf2SampleMetadata sampleMetadata = createSampleMetadata (instrZone.getSample (), generators);
+ parseModulators (sampleMetadata, zone, instrZone);
+ layer.addSampleMetadata (sampleMetadata);
}
layers.add (layer);
@@ -141,6 +148,24 @@ private List parseSF2File (final File sourceFile, final Sf2F
}
+ private static void parseModulators (final Sf2SampleMetadata sampleMetadata, final Sf2PresetZone zone, final Sf2InstrumentZone instrZone)
+ {
+ Optional modulator = instrZone.getModulator (Sf2Modulator.MODULATOR_PITCH_BEND);
+ if (modulator.isEmpty ())
+ modulator = zone.getModulator (Sf2Modulator.MODULATOR_PITCH_BEND);
+ if (!modulator.isEmpty ())
+ {
+ final Sf2Modulator sf2Modulator = modulator.get ();
+ if (sf2Modulator.getDestinationGenerator () == Generator.FINE_TUNE)
+ {
+ final int amount = sf2Modulator.getModulationAmount ();
+ sampleMetadata.setBendUp (amount);
+ sampleMetadata.setBendDown (-amount);
+ }
+ }
+ }
+
+
/**
* SF2 contains only mono files. Combine them to stereo, if setup as split-stereo or (only)
* panned left/right. If it is a pure mono file (not panned) leave it as it is.
@@ -380,32 +405,75 @@ private static Sf2SampleMetadata createSampleMetadata (final Sf2SampleDescriptor
// Set loop, if any
if ((generators.getUnsignedValue (Generator.SAMPLE_MODES).intValue () & 1) > 0)
{
- final SampleLoop sampleLoop = new SampleLoop ();
+ final DefaultSampleLoop sampleLoop = new DefaultSampleLoop ();
sampleLoop.setStart ((int) (sample.getStartloop () - sampleStart));
sampleLoop.setEnd ((int) (sample.getEndloop () - sampleStart));
sampleMetadata.addLoop (sampleLoop);
}
// Gain
-
final int initialAttenuation = generators.getSignedValue (Generator.INITIAL_ATTENUATION).intValue ();
if (initialAttenuation > 0)
sampleMetadata.setGain (-initialAttenuation / 10.0);
// Volume envelope
final IEnvelope amplitudeEnvelope = sampleMetadata.getAmplitudeEnvelope ();
- final Integer delay = generators.getSignedValue (Generator.VOL_ENV_DELAY);
- final Integer attack = generators.getSignedValue (Generator.VOL_ENV_ATTACK);
- final Integer hold = generators.getSignedValue (Generator.VOL_ENV_HOLD);
- final Integer decay = generators.getSignedValue (Generator.VOL_ENV_DECAY);
- final Integer release = generators.getSignedValue (Generator.VOL_ENV_RELEASE);
- amplitudeEnvelope.setDelay (convertEnvelopeTime (delay));
- amplitudeEnvelope.setAttack (convertEnvelopeTime (attack));
- amplitudeEnvelope.setHold (convertEnvelopeTime (hold));
- amplitudeEnvelope.setDecay (convertEnvelopeTime (decay));
- amplitudeEnvelope.setRelease (convertEnvelopeTime (release));
- final Integer sustain = generators.getSignedValue (Generator.VOL_ENV_SUSTAIN);
- amplitudeEnvelope.setSustain (convertEnvelopeVolume (sustain));
+ amplitudeEnvelope.setDelay (convertEnvelopeTime (generators.getSignedValue (Generator.VOL_ENV_DELAY)));
+ amplitudeEnvelope.setAttack (convertEnvelopeTime (generators.getSignedValue (Generator.VOL_ENV_ATTACK)));
+ amplitudeEnvelope.setHold (convertEnvelopeTime (generators.getSignedValue (Generator.VOL_ENV_HOLD)));
+ amplitudeEnvelope.setDecay (convertEnvelopeTime (generators.getSignedValue (Generator.VOL_ENV_DECAY)));
+ amplitudeEnvelope.setRelease (convertEnvelopeTime (generators.getSignedValue (Generator.VOL_ENV_RELEASE)));
+ amplitudeEnvelope.setSustain (convertEnvelopeVolume (generators.getSignedValue (Generator.VOL_ENV_SUSTAIN)));
+
+ // Filter settings
+ final Integer initialCutoffValue = generators.getSignedValue (Generator.INITIAL_FILTER_CUTOFF);
+ if (initialCutoffValue != null)
+ {
+ final int initialCutoff = initialCutoffValue.intValue ();
+ if (initialCutoff >= 1500 && initialCutoff < 13500)
+ {
+ // Convert cents to Hertz: f2 is the minimum supported frequency, cents is always a
+ // relation of two frequencies, 1200 cents are one octave:
+ // cents = 1200 * log2 (f1 / f2), f2 = 8.176 => f1 = f2 * 2^(cents / 1200)
+ final double frequency = 8.176 * Math.pow (2, initialCutoff / 1200.0);
+
+ double resonance = 0;
+ final Integer initialResonanceValue = generators.getSignedValue (Generator.INITIAL_FILTER_RESONANCE);
+ if (initialResonanceValue != null)
+ {
+ final int initialResonance = initialResonanceValue.intValue ();
+ if (initialResonance > 0 && initialResonance < 960)
+ resonance = initialResonance / 100.0;
+ }
+
+ final IFilter filter = new DefaultFilter (FilterType.LOW_PASS, 2, frequency, resonance);
+ filter.setEnvelopeDepth (generators.getSignedValue (Generator.MOD_ENV_TO_FILTER_CUTOFF).intValue ());
+ if (filter.getEnvelopeDepth () != 0)
+ {
+ final IEnvelope filterEnvelope = filter.getEnvelope ();
+ filterEnvelope.setDelay (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_DELAY)));
+ filterEnvelope.setAttack (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_ATTACK)));
+ filterEnvelope.setHold (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_HOLD)));
+ filterEnvelope.setDecay (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_DECAY)));
+ filterEnvelope.setRelease (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_RELEASE)));
+ filterEnvelope.setSustain (convertEnvelopeVolume (generators.getSignedValue (Generator.MOD_ENV_SUSTAIN)));
+ }
+
+ sampleMetadata.setFilter (filter);
+
+ sampleMetadata.setPitchEnvelopeDepth (generators.getSignedValue (Generator.MOD_ENV_TO_PITCH).intValue ());
+ if (sampleMetadata.getPitchEnvelopeDepth () != 0)
+ {
+ final IEnvelope pitchEnvelope = sampleMetadata.getPitchEnvelope ();
+ pitchEnvelope.setDelay (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_DELAY)));
+ pitchEnvelope.setAttack (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_ATTACK)));
+ pitchEnvelope.setHold (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_HOLD)));
+ pitchEnvelope.setDecay (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_DECAY)));
+ pitchEnvelope.setRelease (convertEnvelopeTime (generators.getSignedValue (Generator.MOD_ENV_RELEASE)));
+ pitchEnvelope.setSustain (convertEnvelopeVolume (generators.getSignedValue (Generator.MOD_ENV_SUSTAIN)));
+ }
+ }
+ }
return sampleMetadata;
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2SampleMetadata.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2SampleMetadata.java
index 02a2b87..546da41 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2SampleMetadata.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sf2/Sf2SampleMetadata.java
@@ -4,7 +4,7 @@
package de.mossgrabers.sampleconverter.format.sf2;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.file.sf2.Sf2SampleDescriptor;
import de.mossgrabers.sampleconverter.file.wav.DataChunk;
import de.mossgrabers.sampleconverter.file.wav.WaveFile;
@@ -34,7 +34,7 @@ public class Sf2SampleMetadata extends DefaultSampleMetadata
*/
public Sf2SampleMetadata (final Sf2SampleDescriptor sample, final Integer panorama)
{
- super (sample.getName ());
+ super (sample.getName (), null, null, null);
this.sample = sample;
this.rightSample = sample;
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzCreator.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzCreator.java
index a3c27c2..b1d9e6f 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzCreator.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzCreator.java
@@ -8,11 +8,13 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.creator.AbstractCreator;
import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.LoopType;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.LoopType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
import java.io.File;
import java.io.FileWriter;
@@ -32,20 +34,27 @@
*/
public class SfzCreator extends AbstractCreator
{
- private static final char LINE_FEED = '\n';
- private static final String FOLDER_POSTFIX = " Samples";
- private static final String SFZ_HEADER = """
+ private static final char LINE_FEED = '\n';
+ private static final String FOLDER_POSTFIX = " Samples";
+ private static final String SFZ_HEADER = """
/////////////////////////////////////////////////////////////////////////////
////
""";
- private static final String COMMENT_PREFIX = "//// ";
+ private static final String COMMENT_PREFIX = "//// ";
+
+ private static final Map FILTER_TYPE_MAP = new EnumMap<> (FilterType.class);
+ private static final Map LOOP_TYPE_MAP = new EnumMap<> (LoopType.class);
- private static final Map LOOP_TYPE_MAPPER = new EnumMap<> (LoopType.class);
static
{
- LOOP_TYPE_MAPPER.put (LoopType.FORWARD, "forward");
- LOOP_TYPE_MAPPER.put (LoopType.BACKWARDS, "backward");
- LOOP_TYPE_MAPPER.put (LoopType.ALTERNATING, "alternate");
+ FILTER_TYPE_MAP.put (FilterType.LOW_PASS, "lpf");
+ FILTER_TYPE_MAP.put (FilterType.HIGH_PASS, "hpf");
+ FILTER_TYPE_MAP.put (FilterType.BAND_PASS, "bpf");
+ FILTER_TYPE_MAP.put (FilterType.BAND_REJECTION, "brf");
+
+ LOOP_TYPE_MAP.put (LoopType.FORWARD, "forward");
+ LOOP_TYPE_MAP.put (LoopType.BACKWARDS, "backward");
+ LOOP_TYPE_MAP.put (LoopType.ALTERNATING, "alternate");
}
@@ -98,7 +107,7 @@ public void create (final File destinationFolder, final IMultisampleSource multi
* @param multisampleSource The multi-sample
* @return The XML structure
*/
- private static String createMetadata (final String safeSampleFolderName, final IMultisampleSource multisampleSource)
+ private String createMetadata (final String safeSampleFolderName, final IMultisampleSource multisampleSource)
{
final StringBuilder sb = new StringBuilder (SFZ_HEADER);
@@ -120,7 +129,7 @@ private static String createMetadata (final String safeSampleFolderName, final I
sb.append ('<').append (SfzHeader.GLOBAL).append (">").append (LINE_FEED);
if (name != null && !name.isBlank ())
- sb.append (SfzOpcode.GLOBAL_LABEL).append ('=').append (name).append (LINE_FEED);
+ addAttribute (sb, SfzOpcode.GLOBAL_LABEL, name, true);
for (final IVelocityLayer layer: multisampleSource.getLayers ())
{
@@ -139,9 +148,9 @@ private static String createMetadata (final String safeSampleFolderName, final I
sb.append (LINE_FEED).append ('<').append (SfzHeader.GROUP).append (">").append (LINE_FEED);
final String layerName = layer.getName ();
if (layerName != null && !layerName.isBlank ())
- sb.append (SfzOpcode.GROUP_LABEL).append ('=').append (layerName).append (LINE_FEED);
+ addAttribute (sb, SfzOpcode.GROUP_LABEL, layerName, true);
if (sequence > 0)
- sb.append (SfzOpcode.SEQ_LENGTH).append ('=').append (sequence).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.SEQ_LENGTH, sequence, true);
sequence = 1;
for (final ISampleMetadata info: sampleMetadata)
@@ -164,17 +173,17 @@ private static String createMetadata (final String safeSampleFolderName, final I
* @param info Where to get the sample info from
* @param sequenceNumber The number in the sequence for round-robin playback
*/
- private static void createSample (final String safeSampleFolderName, final StringBuilder sb, final ISampleMetadata info, final int sequenceNumber)
+ private void createSample (final String safeSampleFolderName, final StringBuilder sb, final ISampleMetadata info, final int sequenceNumber)
{
sb.append ("\n<").append (SfzHeader.REGION).append (">\n");
final Optional filename = info.getUpdatedFilename ();
if (filename.isPresent ())
- sb.append (SfzOpcode.SAMPLE).append ('=').append (safeSampleFolderName).append ('\\').append (filename.get ()).append (LINE_FEED);
+ addAttribute (sb, SfzOpcode.SAMPLE, formatFileName (safeSampleFolderName, filename.get ()), true);
if (info.isReversed ())
- sb.append (SfzOpcode.DIRECTION).append ("=reverse").append (LINE_FEED);
+ addAttribute (sb, SfzOpcode.DIRECTION, "reverse", true);
if (info.getPlayLogic () == PlayLogic.ROUND_ROBIN)
- sb.append (SfzOpcode.SEQ_POSITION).append ('=').append (sequenceNumber).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.SEQ_POSITION, sequenceNumber, true);
////////////////////////////////////////////////////////////
// Key range
@@ -185,20 +194,27 @@ private static void createSample (final String safeSampleFolderName, final Strin
if (keyRoot == keyLow && keyLow == keyHigh)
{
// Pitch and range are the same, use single key attribute
- sb.append (SfzOpcode.KEY).append ('=').append (keyRoot).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.KEY, keyRoot, true);
}
else
{
- sb.append (SfzOpcode.PITCH_KEY_CENTER).append ('=').append (keyRoot).append (LINE_FEED);
- sb.append (SfzOpcode.LO_KEY).append ('=').append (check (keyLow, 0)).append (' ').append (SfzOpcode.HI_KEY).append ('=').append (check (keyHigh, 127)).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.PITCH_KEY_CENTER, keyRoot, true);
+ addIntegerAttribute (sb, SfzOpcode.LO_KEY, check (keyLow, 0), false);
+ addIntegerAttribute (sb, SfzOpcode.HI_KEY, check (keyHigh, 127), true);
}
final int crossfadeLow = info.getNoteCrossfadeLow ();
if (crossfadeLow > 0)
- sb.append (SfzOpcode.XF_IN_LO_KEY).append ('=').append (Math.max (0, keyLow - crossfadeLow)).append (' ').append (SfzOpcode.XF_IN_HI_KEY).append ('=').append (keyLow).append (LINE_FEED);
+ {
+ addIntegerAttribute (sb, SfzOpcode.XF_IN_LO_KEY, Math.max (0, keyLow - crossfadeLow), false);
+ addIntegerAttribute (sb, SfzOpcode.XF_IN_HI_KEY, keyLow, true);
+ }
final int crossfadeHigh = info.getNoteCrossfadeHigh ();
if (crossfadeHigh > 0)
- sb.append (SfzOpcode.XF_OUT_LO_KEY).append ('=').append (keyHigh).append (' ').append (SfzOpcode.XF_OUT_HI_KEY).append ('=').append (Math.min (127, keyHigh + crossfadeHigh)).append (LINE_FEED);
+ {
+ addIntegerAttribute (sb, SfzOpcode.XF_OUT_LO_KEY, keyHigh, false);
+ addIntegerAttribute (sb, SfzOpcode.XF_OUT_HI_KEY, Math.min (127, keyHigh + crossfadeHigh), true);
+ }
////////////////////////////////////////////////////////////
// Velocity
@@ -206,70 +222,129 @@ private static void createSample (final String safeSampleFolderName, final Strin
final int velocityLow = info.getVelocityLow ();
final int velocityHigh = info.getVelocityHigh ();
if (velocityLow > 1)
- sb.append (SfzOpcode.LO_VEL).append ('=').append (velocityLow).append (velocityHigh == 127 ? LINE_FEED : ' ');
+ addIntegerAttribute (sb, SfzOpcode.LO_VEL, velocityLow, velocityHigh == 127);
if (velocityHigh > 0 && velocityHigh < 127)
- sb.append (SfzOpcode.HI_VEL).append ('=').append (velocityHigh).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.HI_VEL, velocityHigh, true);
final int crossfadeVelocityLow = info.getVelocityCrossfadeLow ();
if (crossfadeVelocityLow > 0)
- sb.append (SfzOpcode.XF_IN_LO_VEL).append ('=').append (Math.max (0, velocityLow - crossfadeVelocityLow)).append (" ").append (SfzOpcode.XF_IN_HI_VEL).append ('=').append (velocityLow).append (LINE_FEED);
+ {
+ addIntegerAttribute (sb, SfzOpcode.XF_IN_LO_VEL, Math.max (0, velocityLow - crossfadeVelocityLow), false);
+ addIntegerAttribute (sb, SfzOpcode.XF_IN_HI_VEL, velocityLow, true);
+ }
+
final int crossfadeVelocityHigh = info.getVelocityCrossfadeHigh ();
if (crossfadeVelocityHigh > 0)
- sb.append (SfzOpcode.XF_OUT_LO_VEL).append ('=').append (velocityHigh).append (" ").append (SfzOpcode.XF_OUT_HI_VEL).append ('=').append (Math.min (127, velocityHigh + crossfadeVelocityHigh)).append (LINE_FEED);
+ {
+ addIntegerAttribute (sb, SfzOpcode.XF_OUT_LO_VEL, velocityHigh, false);
+ addIntegerAttribute (sb, SfzOpcode.XF_OUT_HI_VEL, Math.min (127, velocityHigh + crossfadeVelocityHigh), true);
+ }
////////////////////////////////////////////////////////////
// Start, end, tune, volume
final int start = info.getStart ();
if (start >= 0)
- sb.append (SfzOpcode.OFFSET).append ('=').append (start).append (' ');
+
+ addIntegerAttribute (sb, SfzOpcode.OFFSET, start, false);
final int end = info.getStop ();
if (end >= 0)
- sb.append (SfzOpcode.END).append ('=').append (end).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.END, end, true);
final double tune = info.getTune ();
if (tune != 0)
- sb.append (SfzOpcode.TUNE).append ('=').append (Math.round (tune * 100)).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.TUNE, (int) Math.round (tune * 100), true);
final int keyTracking = (int) Math.round (info.getKeyTracking () * 100.0);
if (keyTracking != 100)
- sb.append (SfzOpcode.PITCH_KEYTRACK).append ('=').append (keyTracking).append (LINE_FEED);
+ addIntegerAttribute (sb, SfzOpcode.PITCH_KEYTRACK, keyTracking, true);
createVolume (sb, info);
+ ////////////////////////////////////////////////////////////
+ // Pitch Bend / Envelope
+
+ final int bendUp = info.getBendUp ();
+ if (bendUp != 0)
+ addIntegerAttribute (sb, SfzOpcode.BEND_UP, bendUp, true);
+ final int bendDown = info.getBendDown ();
+ if (bendDown != 0)
+ addIntegerAttribute (sb, SfzOpcode.BEND_DOWN, bendDown, true);
+
+ final StringBuilder envelopeStr = new StringBuilder ();
+
+ final int envelopeDepth = info.getPitchEnvelopeDepth ();
+ if (envelopeDepth > 0)
+ {
+ sb.append (SfzOpcode.PITCHEG_DEPTH).append ('=').append (info.getPitchEnvelopeDepth ()).append (LINE_FEED);
+
+ final IEnvelope pitchEnvelope = info.getPitchEnvelope ();
+
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.PITCHEG_DELAY, pitchEnvelope.getDelay ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.PITCHEG_ATTACK, pitchEnvelope.getAttack ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.PITCHEG_HOLD, pitchEnvelope.getHold ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.PITCHEG_DECAY, pitchEnvelope.getDecay ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.PITCHEG_RELEASE, pitchEnvelope.getRelease ());
+
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.PITCHEG_START, pitchEnvelope.getStart () * 100.0);
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.PITCHEG_SUSTAIN, pitchEnvelope.getSustain () * 100.0);
+
+ if (envelopeStr.length () > 0)
+ sb.append (envelopeStr).append (LINE_FEED);
+ }
+
////////////////////////////////////////////////////////////
// Sample Loop
- final List loops = info.getLoops ();
+ createLoops (sb, info);
+
+ ////////////////////////////////////////////////////////////
+ // Filter
+
+ createFilter (sb, info);
+ }
+
+
+ /**
+ * Create the loop info.
+ *
+ * @param sb Where to add the XML code
+ * @param info Where to get the sample info from
+ */
+ private static void createLoops (final StringBuilder sb, final ISampleMetadata info)
+ {
+ final List loops = info.getLoops ();
if (loops.isEmpty ())
- sb.append (SfzOpcode.LOOP_MODE).append ("=no_loop ");
- else
{
- final SampleLoop sampleLoop = loops.get (0);
- // SFZ currently only supports forward looping
- sb.append (SfzOpcode.LOOP_MODE).append ("=loop_continuous ");
- final String type = LOOP_TYPE_MAPPER.get (sampleLoop.getType ());
- // No need to write the default value
- if (!"forward".equals (type))
- sb.append (SfzOpcode.LOOP_TYPE).append ('=').append (type).append (' ');
- sb.append (SfzOpcode.LOOP_START).append ('=').append (sampleLoop.getStart ()).append (' ').append (SfzOpcode.LOOP_END).append ('=').append (sampleLoop.getEnd ());
-
- // Calculate the crossfade in seconds from a percentage of the loop length
- final double crossfade = sampleLoop.getCrossfade ();
- if (crossfade > 0)
+ addAttribute (sb, SfzOpcode.LOOP_MODE, "no_loop", false);
+ return;
+ }
+
+ final ISampleLoop sampleLoop = loops.get (0);
+ // SFZ currently only supports forward looping
+ addAttribute (sb, SfzOpcode.LOOP_MODE, "loop_continuous", false);
+ final String type = LOOP_TYPE_MAP.get (sampleLoop.getType ());
+ // No need to write the default value
+ if (!"forward".equals (type))
+ addAttribute (sb, SfzOpcode.LOOP_TYPE, type, false);
+ addIntegerAttribute (sb, SfzOpcode.LOOP_START, sampleLoop.getStart (), false);
+ sb.append (SfzOpcode.LOOP_END).append ('=').append (sampleLoop.getEnd ());
+
+ // Calculate the crossfade in seconds from a percentage of the loop length
+ final double crossfade = sampleLoop.getCrossfade ();
+ if (crossfade > 0)
+ {
+ final int loopLength = sampleLoop.getStart () - sampleLoop.getEnd ();
+ if (loopLength > 0)
{
- final int loopLength = sampleLoop.getStart () - sampleLoop.getEnd ();
- if (loopLength > 0)
- {
- final double loopLengthInSeconds = loopLength / (double) info.getSampleRate ();
-
- final double crossfadeInSeconds = crossfade * loopLengthInSeconds;
- sb.append (' ').append (SfzOpcode.LOOP_CROSSFADE).append ('=').append (Math.round (crossfadeInSeconds));
- }
- }
+ final double loopLengthInSeconds = loopLength / (double) info.getSampleRate ();
- sb.append (LINE_FEED);
+ final double crossfadeInSeconds = crossfade * loopLengthInSeconds;
+ sb.append (' ').append (SfzOpcode.LOOP_CROSSFADE).append ('=').append (Math.round (crossfadeInSeconds));
+ }
}
+
+ sb.append (LINE_FEED);
}
@@ -283,7 +358,7 @@ private static void createVolume (final StringBuilder sb, final ISampleMetadata
{
final double volume = sampleMetadata.getGain ();
if (volume != 0)
- sb.append (SfzOpcode.VOLUME).append ('=').append (volume).append (LINE_FEED);
+ addAttribute (sb, SfzOpcode.VOLUME, formatDouble (volume, 2), true);
final StringBuilder envelopeStr = new StringBuilder ();
@@ -303,6 +378,60 @@ private static void createVolume (final StringBuilder sb, final ISampleMetadata
}
+ /**
+ * Create the filter info.
+ *
+ * @param sb Where to add the XML code
+ * @param info Where to get the sample info from
+ */
+ private static void createFilter (final StringBuilder sb, final ISampleMetadata info)
+ {
+ final Optional optFilter = info.getFilter ();
+ if (optFilter.isEmpty ())
+ return;
+
+ final IFilter filter = optFilter.get ();
+ final String type = FILTER_TYPE_MAP.get (filter.getType ());
+ addAttribute (sb, SfzOpcode.FILTER_TYPE, type + "_" + (int) clamp (filter.getPoles (), 1, 4) + "p", false);
+ addAttribute (sb, SfzOpcode.CUTOFF, formatDouble (filter.getCutoff (), 2), false);
+ addAttribute (sb, SfzOpcode.RESONANCE, formatDouble (Math.min (40, filter.getResonance ()), 2), true);
+
+ final StringBuilder envelopeStr = new StringBuilder ();
+
+ final int envelopeDepth = filter.getEnvelopeDepth ();
+ if (envelopeDepth > 0)
+ {
+ sb.append (SfzOpcode.FILEG_DEPTH).append ('=').append (filter.getEnvelopeDepth ()).append (LINE_FEED);
+
+ final IEnvelope filterEnvelope = filter.getEnvelope ();
+
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.FILEG_DELAY, filterEnvelope.getDelay ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.FILEG_ATTACK, filterEnvelope.getAttack ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.FILEG_HOLD, filterEnvelope.getHold ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.FILEG_DECAY, filterEnvelope.getDecay ());
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.FILEG_RELEASE, filterEnvelope.getRelease ());
+
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.FILEG_START, filterEnvelope.getStart () * 100.0);
+ addEnvelopeAttribute (envelopeStr, SfzOpcode.FILEG_SUSTAIN, filterEnvelope.getSustain () * 100.0);
+
+ if (envelopeStr.length () > 0)
+ sb.append (envelopeStr).append (LINE_FEED);
+ }
+ }
+
+
+ private static void addAttribute (final StringBuilder sb, final String opcode, final String value, final boolean addLineFeed)
+ {
+ sb.append (opcode).append ('=').append (value).append (addLineFeed ? LINE_FEED : ' ');
+ }
+
+
+ private static void addIntegerAttribute (final StringBuilder sb, final String opcode, final int value, final boolean addLineFeed)
+ {
+ addAttribute (sb, opcode, Integer.toString (value), addLineFeed);
+ }
+
+
private static void addEnvelopeAttribute (final StringBuilder sb, final String opcode, final double value)
{
if (value < 0)
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzDetectorTask.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzDetectorTask.java
index f89e479..65cdcbf 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzDetectorTask.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzDetectorTask.java
@@ -8,14 +8,18 @@
import de.mossgrabers.sampleconverter.core.INotifier;
import de.mossgrabers.sampleconverter.core.detector.AbstractDetectorTask;
import de.mossgrabers.sampleconverter.core.detector.MultisampleSource;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IEnvelope;
+import de.mossgrabers.sampleconverter.core.model.IFilter;
+import de.mossgrabers.sampleconverter.core.model.ISampleLoop;
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.LoopType;
-import de.mossgrabers.sampleconverter.core.model.PlayLogic;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
-import de.mossgrabers.sampleconverter.core.model.VelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.enumeration.FilterType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.LoopType;
+import de.mossgrabers.sampleconverter.core.model.enumeration.PlayLogic;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultFilter;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleLoop;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultVelocityLayer;
import de.mossgrabers.sampleconverter.file.FileUtils;
import de.mossgrabers.sampleconverter.ui.IMetadataConfig;
import de.mossgrabers.sampleconverter.util.Pair;
@@ -45,15 +49,21 @@
*/
public class SfzDetectorTask extends AbstractDetectorTask
{
- private static final Pattern HEADER_PATTERN = Pattern.compile ("<([a-z]+)>([^<]*)", Pattern.DOTALL);
- private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile ("(\\b\\w+)=(.*?(?=\\s\\w+=|//|$))", Pattern.DOTALL);
+ private static final Pattern HEADER_PATTERN = Pattern.compile ("<([a-z]+)>([^<]*)", Pattern.DOTALL);
+ private static final Pattern ATTRIBUTE_PATTERN = Pattern.compile ("(\\b\\w+)=(.*?(?=\\s\\w+=|//|$))", Pattern.DOTALL);
+ private static final Map FILTER_TYPE_MAP = new HashMap<> ();
+ private static final Map LOOP_TYPE_MAP = new HashMap<> (3);
- private static final Map LOOP_TYPE_MAPPER = new HashMap<> (3);
static
{
- LOOP_TYPE_MAPPER.put ("forward", LoopType.FORWARD);
- LOOP_TYPE_MAPPER.put ("backward", LoopType.BACKWARDS);
- LOOP_TYPE_MAPPER.put ("alternate", LoopType.ALTERNATING);
+ FILTER_TYPE_MAP.put ("lpf", FilterType.LOW_PASS);
+ FILTER_TYPE_MAP.put ("hpf", FilterType.HIGH_PASS);
+ FILTER_TYPE_MAP.put ("bpf", FilterType.BAND_PASS);
+ FILTER_TYPE_MAP.put ("brf", FilterType.BAND_REJECTION);
+
+ LOOP_TYPE_MAP.put ("forward", LoopType.FORWARD);
+ LOOP_TYPE_MAP.put ("backward", LoopType.BACKWARDS);
+ LOOP_TYPE_MAP.put ("alternate", LoopType.ALTERNATING);
}
/** The names of notes. */
@@ -196,7 +206,6 @@ private List parseMetadataFile (final File multiSampleFile,
final String n = this.metadata.isPreferFolderName () ? this.sourceFolder.getName () : name;
final String [] parts = createPathParts (multiSampleFile.getParentFile (), this.sourceFolder, n);
- this.processedOpcodes.add (SfzOpcode.GLOBAL_LABEL);
final List velocityLayers = this.parseVelocityLayers (multiSampleFile.getParentFile (), result);
final Optional globalName = this.getAttribute (SfzOpcode.GLOBAL_LABEL);
@@ -228,7 +237,7 @@ private List parseVelocityLayers (final File basePath, final Lis
File sampleBaseFolder = basePath;
final List velocityLayers = new ArrayList<> ();
- IVelocityLayer layer = new VelocityLayer ();
+ IVelocityLayer layer = new DefaultVelocityLayer ();
for (final Pair> pair: headers)
{
final Map attributes = pair.getValue ();
@@ -263,7 +272,7 @@ private List parseVelocityLayers (final File basePath, final Lis
case SfzHeader.GROUP:
if (!layer.getSampleMetadata ().isEmpty ())
velocityLayers.add (layer);
- layer = new VelocityLayer ();
+ layer = new DefaultVelocityLayer ();
this.groupAttributes = attributes;
@@ -471,10 +480,85 @@ private void parseRegion (final ISampleMetadata sampleMetadata)
final double pitchKeytrack = this.getDoubleValue (SfzOpcode.PITCH_KEYTRACK, 100);
sampleMetadata.setKeyTracking (Math.min (100, Math.max (0, pitchKeytrack)) / 100.0);
+ sampleMetadata.setBendUp (this.getIntegerValue (SfzOpcode.BEND_UP, 0));
+ sampleMetadata.setBendDown (this.getIntegerValue (SfzOpcode.BEND_DOWN, 0));
+
+ int envelopeDepth = this.getIntegerValue (SfzOpcode.PITCHEG_DEPTH, 0);
+ if (envelopeDepth == 0)
+ envelopeDepth = this.getIntegerValue (SfzOpcode.PITCH_DEPTH, 0);
+ sampleMetadata.setPitchEnvelopeDepth (envelopeDepth);
+
+ final IEnvelope pitchEnvelope = sampleMetadata.getPitchEnvelope ();
+ pitchEnvelope.setDelay (this.getDoubleValue (SfzOpcode.PITCHEG_DELAY, SfzOpcode.PITCH_DELAY));
+ pitchEnvelope.setAttack (this.getDoubleValue (SfzOpcode.PITCHEG_ATTACK, SfzOpcode.PITCH_ATTACK));
+ pitchEnvelope.setHold (this.getDoubleValue (SfzOpcode.PITCHEG_HOLD, SfzOpcode.PITCH_HOLD));
+ pitchEnvelope.setDecay (this.getDoubleValue (SfzOpcode.PITCHEG_DECAY, SfzOpcode.PITCH_DECAY));
+ pitchEnvelope.setRelease (this.getDoubleValue (SfzOpcode.PITCHEG_RELEASE, SfzOpcode.PITCH_RELEASE));
+ final double startValue = this.getDoubleValue (SfzOpcode.PITCHEG_START, SfzOpcode.PITCH_START);
+ final double sustainValue = this.getDoubleValue (SfzOpcode.PITCHEG_SUSTAIN, SfzOpcode.PITCH_SUSTAIN);
+ pitchEnvelope.setStart (startValue < 0 ? -1 : startValue / 100.0);
+ pitchEnvelope.setSustain (sustainValue < 0 ? -1 : sustainValue / 100.0);
+
////////////////////////////////////////////////////////////
// Volume
this.parseVolume (sampleMetadata);
+
+ ////////////////////////////////////////////////////////////
+ // Filter
+
+ this.parseFilter (sampleMetadata);
+ }
+
+
+ private void parseFilter (ISampleMetadata sampleMetadata)
+ {
+ double cutoff = this.getDoubleValue (SfzOpcode.CUTOFF, -1);
+ if (cutoff < 0)
+ cutoff = IFilter.MAX_FREQUENCY;
+
+ final Optional attribute = this.getAttribute (SfzOpcode.FILTER_TYPE);
+ final String filterTypeStr = attribute.isEmpty () ? "lpf_2p" : attribute.get ();
+ if (filterTypeStr.length () < 6)
+ return;
+ FilterType filterType = FILTER_TYPE_MAP.get (filterTypeStr.substring (0, 3));
+ // Unsupported filter type?
+ if (filterType == null)
+ filterType = FilterType.LOW_PASS;
+ int poles;
+ try
+ {
+ poles = Integer.parseInt (filterTypeStr.substring (4, 5));
+ if (poles <= 0)
+ poles = 2;
+ }
+ catch (final NumberFormatException ex)
+ {
+ poles = 2;
+ }
+
+ final double resonance = this.getDoubleValue (SfzOpcode.RESONANCE, 0);
+ int envelopeDepth = this.getIntegerValue (SfzOpcode.FILEG_DEPTH, 0);
+ if (envelopeDepth == 0)
+ envelopeDepth = this.getIntegerValue (SfzOpcode.FIL_DEPTH, 0);
+
+ final IFilter filter = new DefaultFilter (filterType, poles, cutoff, resonance);
+ sampleMetadata.setFilter (filter);
+
+ filter.setEnvelopeDepth (envelopeDepth);
+
+ // Filter envelope
+ final IEnvelope filterEnvelope = filter.getEnvelope ();
+ filterEnvelope.setDelay (this.getDoubleValue (SfzOpcode.FILEG_DELAY, SfzOpcode.FIL_DELAY));
+ filterEnvelope.setAttack (this.getDoubleValue (SfzOpcode.FILEG_ATTACK, SfzOpcode.FIL_ATTACK));
+ filterEnvelope.setHold (this.getDoubleValue (SfzOpcode.FILEG_HOLD, SfzOpcode.FIL_HOLD));
+ filterEnvelope.setDecay (this.getDoubleValue (SfzOpcode.FILEG_DECAY, SfzOpcode.FIL_DECAY));
+ filterEnvelope.setRelease (this.getDoubleValue (SfzOpcode.FILEG_RELEASE, SfzOpcode.FIL_RELEASE));
+
+ final double startValue = this.getDoubleValue (SfzOpcode.FILEG_START, SfzOpcode.FIL_START);
+ final double sustainValue = this.getDoubleValue (SfzOpcode.FILEG_SUSTAIN, SfzOpcode.FIL_SUSTAIN);
+ filterEnvelope.setStart (startValue < 0 ? -1 : startValue / 100.0);
+ filterEnvelope.setSustain (sustainValue < 0 ? -1 : sustainValue / 100.0);
}
@@ -485,7 +569,7 @@ private void parseRegion (final ISampleMetadata sampleMetadata)
*/
private void parseLoop (final ISampleMetadata sampleMetadata)
{
- final SampleLoop loop = new SampleLoop ();
+ final DefaultSampleLoop loop = new DefaultSampleLoop ();
final Optional loopMode = this.getAttribute (SfzOpcode.LOOP_MODE);
if (loopMode.isPresent ())
@@ -503,7 +587,7 @@ private void parseLoop (final ISampleMetadata sampleMetadata)
final Optional loopType = this.getAttribute (SfzOpcode.LOOP_TYPE);
if (loopType.isPresent ())
{
- final LoopType type = LOOP_TYPE_MAPPER.get (loopType.get ());
+ final LoopType type = LOOP_TYPE_MAP.get (loopType.get ());
if (type != null)
loop.setType (type);
}
@@ -569,9 +653,9 @@ private void readMissingValues (final DefaultSampleMetadata sampleMetadata)
// Read loop and root key if necessary. If loop was not explicitly
// deactivated, there is a loop present, which might need to read the
// parameters from the WAV file
- List loops = sampleMetadata.getLoops ();
+ List loops = sampleMetadata.getLoops ();
boolean readLoops = false;
- SampleLoop oldLoop = null;
+ ISampleLoop oldLoop = null;
if (!loops.isEmpty ())
{
oldLoop = loops.get (0);
@@ -587,7 +671,7 @@ private void readMissingValues (final DefaultSampleMetadata sampleMetadata)
// The null check is not necessary but otherwise we get an Eclipse warning
if (oldLoop != null && !loops.isEmpty ())
{
- final SampleLoop newLoop = loops.get (0);
+ final ISampleLoop newLoop = loops.get (0);
final int oldStart = oldLoop.getStart ();
if (oldStart >= 0)
@@ -621,6 +705,7 @@ private Set diffOpcodes ()
if (!this.processedOpcodes.contains (attribute))
unsupported.add (attribute);
});
+ this.allOpcodes.clear ();
return unsupported;
}
@@ -688,17 +773,31 @@ private int getIntegerValue (final String key1, final String key2)
* @return The value or -1 if not found or is not an integer
*/
private int getIntegerValue (final String key)
+ {
+ return getIntegerValue (key, -1);
+ }
+
+
+ /**
+ * Get the attribute integer value for the given key. The value is searched starting from region
+ * upwards to group, master and finally global.
+ *
+ * @param key The key of the value to lookup
+ * @param defaultValue The value to return if the key is not present or cannot be read
+ * @return The value or -1 if not found or is not an integer
+ */
+ private int getIntegerValue (final String key, final int defaultValue)
{
final Optional value = this.getAttribute (key);
if (value.isEmpty ())
- return -1;
+ return defaultValue;
try
{
return Integer.parseInt (value.get ());
}
catch (final NumberFormatException ex)
{
- return -1;
+ return defaultValue;
}
}
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzOpcode.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzOpcode.java
index 4ce3816..d4bef99 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzOpcode.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/sfz/SfzOpcode.java
@@ -89,6 +89,12 @@ public class SfzOpcode
public static final String PITCH = "pitch";
/** SFZ v1. Defines how much the pitch changes with every note. */
public static final String PITCH_KEYTRACK = "pitch_keytrack";
+
+ /** SFZ v1. The pitch bend up in cents (-9600 to 9600). */
+ public static final String BEND_UP = "bend_up";
+ /** SFZ v1. The pitch bend down in cents (-9600 to 9600). */
+ public static final String BEND_DOWN = "bend_down";
+
/** SFZ v1. The volume for the region, in decibels. */
public static final String VOLUME = "volume";
@@ -136,6 +142,87 @@ public class SfzOpcode
/** Cakewalk alias. The EG release time. */
public static final String AMP_RELEASE = "amp_release";
+ ////////////////////////////////////////////////////////////////
+ // Filter opcodes
+
+ /** SFZ v1. The cutoff frequency (Hz) of the 1st filter specified in Hertz. */
+ public static final String CUTOFF = "cutoff";
+ /** SFZ v1. The filter cutoff resonance value, in decibels. */
+ public static final String RESONANCE = "resonance";
+ /** SFZ v1. The type of filter. */
+ public static final String FILTER_TYPE = "fil_type";
+
+ /** SFZ v1. The filter EG depth. */
+ public static final String FILEG_DEPTH = "fileg_depth";
+ /** Cakewalk alias. The filter EG depth. */
+ public static final String FIL_DEPTH = "fil_depth";
+
+ /** SFZ v1. The EG delay time. */
+ public static final String FILEG_DELAY = "fileg_delay";
+ /** SFZ v1. The EG envelope start level. */
+ public static final String FILEG_START = "fileg_start";
+ /** SFZ v1. The EG attack time. */
+ public static final String FILEG_ATTACK = "fileg_attack";
+ /** SFZ v1. The EG hold time. */
+ public static final String FILEG_HOLD = "fileg_hold";
+ /** SFZ v1. The EG decay time. */
+ public static final String FILEG_DECAY = "fileg_decay";
+ /** SFZ v1. The EG envelope sustain level. */
+ public static final String FILEG_SUSTAIN = "fileg_sustain";
+ /** SFZ v1. The EG release time. */
+ public static final String FILEG_RELEASE = "fileg_release";
+ /** Cakewalk alias. The EG delay time. */
+ public static final String FIL_DELAY = "fil_delay";
+ /** Cakewalk alias. The EG envelope start level. */
+ public static final String FIL_START = "fil_start";
+ /** Cakewalk alias. The EG attack time. */
+ public static final String FIL_ATTACK = "fil_attack";
+ /** Cakewalk alias. The EG hold time. */
+ public static final String FIL_HOLD = "fil_hold";
+ /** Cakewalk alias. The EG decay time. */
+ public static final String FIL_DECAY = "fil_decay";
+ /** Cakewalk alias. The EG envelope sustain level. */
+ public static final String FIL_SUSTAIN = "fil_sustain";
+ /** Cakewalk alias. The EG release time. */
+ public static final String FIL_RELEASE = "fil_release";
+
+ ////////////////////////////////////////////////////////////////
+ // Pitch opcodes
+
+ /** SFZ v1. The filter pitch EG depth. */
+ public static final String PITCHEG_DEPTH = "pitcheg_depth";
+ /** Cakewalk alias. The filter pitch EG depth. */
+ public static final String PITCH_DEPTH = "pitch_depth";
+
+ /** SFZ v1. The pitch EG delay time. */
+ public static final String PITCHEG_DELAY = "pitcheg_delay";
+ /** SFZ v1. The pitch EG envelope start level. */
+ public static final String PITCHEG_START = "pitcheg_start";
+ /** SFZ v1. The pitch EG attack time. */
+ public static final String PITCHEG_ATTACK = "pitcheg_attack";
+ /** SFZ v1. The pitch EG hold time. */
+ public static final String PITCHEG_HOLD = "pitcheg_hold";
+ /** SFZ v1. The pitch EG decay time. */
+ public static final String PITCHEG_DECAY = "pitcheg_decay";
+ /** SFZ v1. The pitch EG envelope sustain level. */
+ public static final String PITCHEG_SUSTAIN = "pitcheg_sustain";
+ /** SFZ v1. The pitch EG release time. */
+ public static final String PITCHEG_RELEASE = "pitcheg_release";
+ /** Cakewalk alias. The pitch EG delay time. */
+ public static final String PITCH_DELAY = "pitch_delay";
+ /** Cakewalk alias. The pitch EG envelope start level. */
+ public static final String PITCH_START = "pitch_start";
+ /** Cakewalk alias. The pitch EG attack time. */
+ public static final String PITCH_ATTACK = "pitch_attack";
+ /** Cakewalk alias. The pitch EG hold time. */
+ public static final String PITCH_HOLD = "pitch_hold";
+ /** Cakewalk alias. The pitch EG decay time. */
+ public static final String PITCH_DECAY = "pitch_decay";
+ /** Cakewalk alias. The pitch EG envelope sustain level. */
+ public static final String PITCH_SUSTAIN = "pitch_sustain";
+ /** Cakewalk alias. The pitch EG release time. */
+ public static final String PITCH_RELEASE = "pitch_release";
+
/**
* Private constructor for utility class.
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavKeyMapping.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavKeyMapping.java
index 9169dd4..64538ca 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavKeyMapping.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavKeyMapping.java
@@ -6,7 +6,7 @@
import de.mossgrabers.sampleconverter.core.model.ISampleMetadata;
import de.mossgrabers.sampleconverter.core.model.IVelocityLayer;
-import de.mossgrabers.sampleconverter.core.model.VelocityLayer;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultVelocityLayer;
import de.mossgrabers.sampleconverter.exception.CombinationNotPossibleException;
import de.mossgrabers.sampleconverter.exception.MultisampleException;
import de.mossgrabers.sampleconverter.exception.NoteNotDetectedException;
@@ -245,7 +245,7 @@ private static List orderLayers (final Map {
- final IVelocityLayer velocityLayer = new VelocityLayer (new ArrayList<> (layer));
+ final IVelocityLayer velocityLayer = new DefaultVelocityLayer (new ArrayList<> (layer));
if (isAscending)
reorderedSampleMetadata.add (velocityLayer);
else
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavSampleMetadata.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavSampleMetadata.java
index 0d310ee..dae7850 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavSampleMetadata.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/wav/WavSampleMetadata.java
@@ -4,9 +4,9 @@
package de.mossgrabers.sampleconverter.format.wav;
-import de.mossgrabers.sampleconverter.core.model.DefaultSampleMetadata;
-import de.mossgrabers.sampleconverter.core.model.LoopType;
-import de.mossgrabers.sampleconverter.core.model.SampleLoop;
+import de.mossgrabers.sampleconverter.core.model.enumeration.LoopType;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleLoop;
+import de.mossgrabers.sampleconverter.core.model.implementation.DefaultSampleMetadata;
import de.mossgrabers.sampleconverter.exception.CombinationNotPossibleException;
import de.mossgrabers.sampleconverter.exception.CompressionNotSupportedException;
import de.mossgrabers.sampleconverter.exception.ParseException;
@@ -17,6 +17,7 @@
import de.mossgrabers.sampleconverter.ui.tools.Functions;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -61,18 +62,19 @@ public WavSampleMetadata (final File file) throws IOException
* Constructor for a sample stored in a ZIP file.
*
* @param zipFile The ZIP file which contains the WAV files
- * @param filename The name of the samples' file in the ZIP file
+ * @param zipEntry The relative path in the ZIP where the file is stored
* @throws IOException Could not read the file
*/
- public WavSampleMetadata (final File zipFile, final String filename) throws IOException
+ public WavSampleMetadata (final File zipFile, final File zipEntry) throws IOException
{
- super (zipFile, filename);
+ super (zipFile, zipEntry);
try (final ZipFile zf = new ZipFile (this.zipFile))
{
- final ZipEntry entry = zf.getEntry (this.filename);
+ final String path = this.zipEntry.getPath ().replace ('\\', '/');
+ final ZipEntry entry = zf.getEntry (path);
if (entry == null)
- throw new IOException (Functions.getMessage ("IDS_NOTIFY_ERR_FILE_NOT_FOUND_IN_ZIP", this.filename));
+ throw new FileNotFoundException (Functions.getMessage ("IDS_NOTIFY_ERR_FILE_NOT_FOUND_IN_ZIP", path));
try (final InputStream in = zf.getInputStream (entry))
{
this.waveFile = new WaveFile (in, true);
@@ -121,7 +123,7 @@ private void readFromChunks () throws IOException
this.tune = Math.max (0, Math.min (1, midiPitchFraction * 0.5 / 0x80000000));
sampleChunk.getLoops ().forEach (sampleLoop -> {
- final SampleLoop loop = new SampleLoop ();
+ final DefaultSampleLoop loop = new DefaultSampleLoop ();
switch (sampleLoop.getType ())
{
default:
diff --git a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/util/XMLUtils.java b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/util/XMLUtils.java
index 10264da..8dd8f7b 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/util/XMLUtils.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/util/XMLUtils.java
@@ -161,6 +161,58 @@ public static String getChildElementContent (final Node parent, final String nam
}
+ /**
+ * Returns the text content interpreted as an integer of a sub-element of a node with the name
+ * 'name' or null if not found.
+ *
+ * @param parent The parent node of the sub-element to lookup
+ * @param name The tag-name of the sub-element
+ * @param defaultValue The default value to return if the element is not present or does not
+ * contain a valid integer
+ * @return The sub-elements' integer content or null
+ */
+ public static int getChildElementIntegerContent (final Node parent, final String name, final int defaultValue)
+ {
+ final String content = getChildElementContent (parent, name);
+ if (content.isBlank ())
+ return defaultValue;
+ try
+ {
+ return Integer.parseInt (content);
+ }
+ catch (final NumberFormatException ex)
+ {
+ return defaultValue;
+ }
+ }
+
+
+ /**
+ * Returns the text content interpreted as an integer of a sub-element of a node with the name
+ * 'name' or null if not found.
+ *
+ * @param parent The parent node of the sub-element to lookup
+ * @param name The tag-name of the sub-element
+ * @param defaultValue The default value to return if the element is not present or does not
+ * contain a valid double
+ * @return The sub-elements' integer content or null
+ */
+ public static double getChildElementDoubleContent (final Node parent, final String name, final double defaultValue)
+ {
+ final String content = getChildElementContent (parent, name);
+ if (content.isBlank ())
+ return defaultValue;
+ try
+ {
+ return Double.parseDouble (content);
+ }
+ catch (final NumberFormatException ex)
+ {
+ return defaultValue;
+ }
+ }
+
+
/**
* Returns the sub-nodes of a node with the name 'name'.
*
diff --git a/src/main/java/de/mossgrabers/sampleconverter/module-info.java b/src/main/java/de/mossgrabers/sampleconverter/module-info.java
index c3a1c3d..a2f05f2 100644
--- a/src/main/java/de/mossgrabers/sampleconverter/module-info.java
+++ b/src/main/java/de/mossgrabers/sampleconverter/module-info.java
@@ -20,6 +20,7 @@
exports de.mossgrabers.sampleconverter.ui.tools.panel;
exports de.mossgrabers.sampleconverter.core;
exports de.mossgrabers.sampleconverter.core.model;
+ exports de.mossgrabers.sampleconverter.core.model.enumeration;
exports de.mossgrabers.sampleconverter.util;
exports de.mossgrabers.sampleconverter.format.bitwig;
exports de.mossgrabers.sampleconverter.format.sfz;
diff --git a/src/main/resources/Strings.properties b/src/main/resources/Strings.properties
index 914ec61..89b22f3 100644
--- a/src/main/resources/Strings.properties
+++ b/src/main/resources/Strings.properties
@@ -1,4 +1,4 @@
-TITLE=ConvertWithMoss 4.5
+TITLE=ConvertWithMoss 4.6
##################################################################################
#
@@ -45,9 +45,11 @@ IDS_NOTIFY_ERR_PARSER=Could not instantiate XML parser/writer.\n
IDS_NOTIFY_LINE_FEED=\n
IDS_NOTIFY_ERR_BROKEN_PRESET=Structurally unsound preset section.\n
IDS_NOTIFY_ERR_BROKEN_PRESET_ZONE=Structurally unsound preset zone section.\n
+IDS_NOTIFY_ERR_BROKEN_PRESET_MODULATORS=Structurally unsound preset modulators section.\n
IDS_NOTIFY_ERR_BROKEN_PRESET_GENERATORS=Structurally unsound preset generators section.\n
IDS_NOTIFY_ERR_BROKEN_INST=Structurally unsound instrument section.\n
IDS_NOTIFY_ERR_BROKEN_INSTRUMENT_ZONE=Structurally unsound instrument zone section.\n
+IDS_NOTIFY_ERR_BROKEN_INSTRUMENT_MODULATORS=Structurally unsound instrument modulators section.\n
IDS_NOTIFY_ERR_BROKEN_INSTRUMENT_GENERATORS=Structurally unsound instrument generators section.\n
IDS_NOTIFY_ERR_UNSUPPORTED_SAMPLE_TYPE=Linked samples and samples located in a ROM are not supported.\n
IDS_NOTIFY_ERR_BROKEN_SAMPLE_HEADER=Structurally unsound sample header section.\n
@@ -57,6 +59,7 @@ IDS_NOTIFY_ERR_DIFFERENT_SAMPLE_PITCH=Left and right samples do not have the sam
IDS_NOTIFY_ERR_DIFFERENT_SAMPLE_RATE=Left and right samples do not have the same sample rate: %1 %2\n
IDS_NOTIFY_ERR_DIFFERENT_SAMPLE_LENGTH=Warning: Left and right samples do not have the same length: %1 (%2) %3 (%4)\n
IDS_NOTIFY_ERR_DIFFERENT_LOOP_LENGTH=Warning: Left and right loop do not have the same loop start/end: %1 %2 (%3:%4/%5:%6)\n
+IDS_NOTIFY_ERR_FILENAME=Filename of sample must not be null and not contain slashes: %1
IDS_MPC_MORE_THAN_4_LAYERS=Round-robin keygroup can only contain up to 4 layers (Range: %1 - %2, Velocity: %3 - %4).\n
IDS_MPC_MORE_THAN_128_KEYGROUPS=More than 128 keygroups present (%1). This might cause issues when loading the program.\n