From 66af027d39ebbe052dc7f5a48f03104845b0a6b6 Mon Sep 17 00:00:00 2001 From: git-moss Date: Sun, 26 Dec 2021 14:55:54 +0100 Subject: [PATCH] * 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. --- README.md | 11 + pom.xml | 2 +- .../core/IMultisampleSource.java | 19 ++ .../core/creator/AbstractCreator.java | 28 ++ .../core/detector/AbstractDetectorTask.java | 2 +- .../core/detector/MultisampleSource.java | 55 +++- .../sampleconverter/core/model/Envelope.java | 133 ---------- .../core/model/EnvelopeAccess.java | 23 -- .../core/model/IEnvelopeAccess.java | 24 ++ .../sampleconverter/core/model/IFilter.java | 77 ++++++ .../core/model/ISampleLoop.java | 2 + .../core/model/ISampleMetadata.java | 54 +++- .../core/model/enumeration/FilterType.java | 22 ++ .../model/{ => enumeration}/LoopType.java | 2 +- .../model/{ => enumeration}/PlayLogic.java | 2 +- .../implementation/AbstractEnvelope.java | 53 ++++ .../model/implementation/DefaultEnvelope.java | 188 ++++++++++++++ .../model/implementation/DefaultFilter.java | 147 +++++++++++ .../DefaultSampleLoop.java} | 7 +- .../DefaultSampleMetadata.java | 157 +++++++---- .../DefaultVelocityLayer.java} | 13 +- .../file/sf2/AbstractZone.java | 55 +++- .../sampleconverter/file/sf2/Generator.java | 62 +++-- .../sampleconverter/file/sf2/Sf2File.java | 128 ++++++++- .../file/sf2/Sf2InstrumentZone.java | 5 +- .../file/sf2/Sf2Modulator.java | 160 ++++++++++++ .../file/sf2/Sf2PresetZone.java | 7 +- .../format/akai/MPCFilter.java | 140 ++++++++++ .../format/akai/MPCKeygroupCreator.java | 72 ++++- .../format/akai/MPCKeygroupDetectorTask.java | 159 ++++++++---- .../format/akai/MPCKeygroupTag.java | 163 +++++++----- .../sampleconverter/format/akai/ZonePlay.java | 2 +- .../bitwig/BitwigMultisampleCreator.java | 10 +- .../bitwig/BitwigMultisampleDetectorTask.java | 22 +- .../decentsampler/DecentSamplerCreator.java | 90 +++++-- .../DecentSamplerDetectorTask.java | 53 +++- .../decentsampler/DecentSamplerTag.java | 21 +- .../KorgmultisampleCreator.java | 4 +- .../KorgmultisampleDetectorTask.java | 10 +- .../format/sf2/Sf2DetectorTask.java | 104 ++++++-- .../format/sf2/Sf2SampleMetadata.java | 4 +- .../format/sfz/SfzCreator.java | 245 +++++++++++++----- .../format/sfz/SfzDetectorTask.java | 141 ++++++++-- .../sampleconverter/format/sfz/SfzOpcode.java | 87 +++++++ .../format/wav/WavKeyMapping.java | 4 +- .../format/wav/WavSampleMetadata.java | 20 +- .../sampleconverter/util/XMLUtils.java | 52 ++++ .../sampleconverter/module-info.java | 1 + src/main/resources/Strings.properties | 5 +- 49 files changed, 2275 insertions(+), 572 deletions(-) delete mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/Envelope.java delete mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/EnvelopeAccess.java create mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/IFilter.java create mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/enumeration/FilterType.java rename src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/{ => enumeration}/LoopType.java (83%) rename src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/{ => enumeration}/PlayLogic.java (82%) create mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/AbstractEnvelope.java create mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultEnvelope.java create mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/implementation/DefaultFilter.java rename src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/{SampleLoop.java => implementation/DefaultSampleLoop.java} (80%) rename src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/{ => implementation}/DefaultSampleMetadata.java (66%) rename src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/core/model/{VelocityLayer.java => implementation/DefaultVelocityLayer.java} (73%) create mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/file/sf2/Sf2Modulator.java create mode 100644 src/main/java/de/mossgrabers/sampleconverter/de/mossgrabers/sampleconverter/format/akai/MPCFilter.java 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