diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef4b4ee..92ea05b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
# Changes
+## 11.2.0
+
+* New: Source and destination path stores now the last 20 selections.
+* New: Implemented loading of AIFF files since some crashed the Java Sound API.
+* DecentSampler
+ * New: Added option to create a dsbundle as output format.
+ * New: Added option to combine all detected multi-sample sources into one library or bundle.
+* Korg KMP
+ * New: Proper support for stereo files. Turns out these workstations cannot play back real stereo files, therefore, a stereo file needs to be split into 2 KMP files.
+ * New: Additionally, a KSC file is created to ease loading of stereo files.
+ * New: Added 2 options to increase the volume.
+ * New: Added option to split source groups into individual KMPs.
+ * New: Increased sample rate limit to 48kHz (was 44.1kHz).
+ * New: Improved creating unique folder names for KMP files.
+ * Fixed: Zones needed to be ordered by their upper key-limit otherwise the file did not work and could even crash the workstation.
+ * Fixed: Reverse playback state was not read correctly.
+ * Fixed: Prevent several characters in file names which could crash the workstation.
+* MPC Keygroups
+ * New: Added option to create up to 8 layers which is now supported with MPC Firmware 3.4.
+* SFZ
+ * New: Added support for reading SFZ files which reference other SFZ files with #include statements.
+ * New: Added option to (not) log unsupported SFZ opcodes. This is off by default since the warnings confused many users.
+* Soundfont 2
+ * New: Added options to add the filename and the preset number to the resulting destination file names.
+
## 11.1.0
* New: AIFF/WAV files are now lazy loaded which keeps the memory usage down.
diff --git a/README-FORMATS.md b/README-FORMATS.md
index a67639a..7ce496e 100644
--- a/README-FORMATS.md
+++ b/README-FORMATS.md
@@ -90,6 +90,7 @@ Other restrictions are:
### Destination Options
+* Limit layers to: MPC Firmware 3.4 increased the number of possible layers in a keygroup to 8. This option allows you to choose between 4 (for older firmware revisions) or 8.
* Options to write/update [WAV Chunk Information](#wav-chunk-information)
## CWITEC TX16Wx
@@ -116,9 +117,10 @@ There are no metadata fields (category, creator, etc.) specified in the format.
### Destination Options
+* Output Format: Choose to create individual presets, libraries or bundles
+* Combine all source multi-samples into one library: If Library or Bundle is selected as output format, all detected source multi-samples are combined into one library or bundle. If the options is off, one library/bundle is created for each detected source multi-sample.
+* Library Filename: Enter a name to use for the library which contains all source multi-samples. If empty, the name of the first found source is used.
* Make monophonic: Restricts the sound to 1 note, use e.g. for lead sounds.
-* Add envelope: Create 4 knobs to edit the amplitude envelope.
-* Add filter: Adds a low pass filter and creates a cutoff and resonance knob for it.
* Add reverb: Adds a reverb effect and creates two parameter knobs for it.
* Options to write/update [WAV Chunk Information](#wav-chunk-information)
@@ -173,12 +175,20 @@ The KMP/KSF format (*.KMP) was first introduced in the Korg Trinity workstation
* PA1X/PA800/PA2X/PA3X/PA4X
* Nautilus
-The format is documented in detail in the appendix of the respective parameter guides. The KMP format contains only 1 group of a multisample, which means there are only key splits but no groups. The file references several KSF files which contain the sample data for each key region.
+The format is documented in detail (more or less) in the appendix of the respective parameter guides. The KMP format contains only 1 group of a multisample, which means there are only key splits but no groups. The file references several KSF files which contain the sample data for each key region.
-Since the KMP format can only contain 1 group of a multisample, sources with multiple groups are split into several destination KMP files. Due to limitations of the format only uncompressed 8 or 16 bit samples up to 48kHz are supported. Files in other formats are automatically converted.
+Since the KMP format can only contain 1 group of a multisample, sources with multiple groups can be optionally split into several destination KMP files. Due to limitations of the format only uncompressed 8 or 16 bit samples up to 48kHz are supported. Files in other formats are automatically converted.
+
+Even if the KSF files can store stereo files, they do not work. Therefore, they need to be split into 2 KMP files. To ease the use of these 2 files an additional KSC file is created, which loads all referenced files.
There are no metadata fields (category, creator, etc.) specified in the format. Therefore, information is stored and retrieved from Broadcast Audio Extension chunks in the WAV files. If noch such chunks are present an [automatic detection](#automatic-metadata-detection) is applied.
+### Destination Options
+
+* Write group KMPs: Writes a KMP for each group in the source multi-sample. This option will be ignored for split stereo source files.
+* Enable the +12dB option: Increases the volume of each sample by +12dB. Use for low volume samples.
+* Set sample volume to +99: If enabled, sets all sample volumes to +99. Use for very low volume samples.
+
## Korg wavestate/modwave
The korgmultisample format is currently used by the Korg wavestate and modwave keyboards as well as their VST plugin siblings. Files in that format (*.korgmultisample) can be opened with the Korg Sample Builder software and transferred to the keyboard.
@@ -230,6 +240,10 @@ The SFZ file contains only the description of the multisample. The related sampl
There are no metadata fields (category, creator, etc.) specified in the format. Therefore, information is stored and retrieved from Broadcast Audio Extension chunks in the WAV files. If noch such chunks are present an [automatic detection](#automatic-metadata-detection) is applied.
+### Source Options
+
+* Log unsupported SFZ opcodes: If enabled, opcodes which are found in the source but are not used (not supported) as input for the conversion are logged.
+
### Destination Options
* Convert to FLAC format: If enabled, the sample files are converted to FLAC.
@@ -245,6 +259,11 @@ The conversion process creates one destination file for each preset found in a S
There are metadata fields for creator and some description specified in the format. However, additional information like a category is retrieved from Broadcast Audio Extension chunks in the WAV files. If noch such chunks are present an [automatic detection](#automatic-metadata-detection) is applied.
+### Source Options
+
+* Prefix with file name: If enabled, the name of the Soundfont file is added to all resulting destination files.
+* Prefix with program number: If enabled, the preset number of the preset is added to the resulting destination file.
+
## TAL Sampler
TAL-Sampler is an analog modeled synthesizer with a sampler engine as the sound source, including a modulation matrix and self-oscillating filters. Most of the presets in it's library store the sample files in an encrypted format (*.wavsmpl), this format is not supported. Only presets using plain WAV or AIFF files are supported.
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar.md5 b/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar.md5
deleted file mode 100644
index 6e7fe6a..0000000
--- a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar.md5
+++ /dev/null
@@ -1 +0,0 @@
-5ea81203e17c44529710ed6ecad31aee
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar.sha1 b/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar.sha1
deleted file mode 100644
index 640920a..0000000
--- a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar.sha1
+++ /dev/null
@@ -1 +0,0 @@
-138d997b241232eebdbc4a08ac7d985283b02ccc
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom.md5 b/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom.md5
deleted file mode 100644
index 4254e18..0000000
--- a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom.md5
+++ /dev/null
@@ -1 +0,0 @@
-bdbc2cec38783514501aa326d9034437
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom.sha1 b/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom.sha1
deleted file mode 100644
index 24cac77..0000000
--- a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom.sha1
+++ /dev/null
@@ -1 +0,0 @@
-c92b453afcb8c662d5dbc3e84486ccfdd972a435
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar
similarity index 73%
rename from maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar
rename to maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar
index a18cee9..1a6eb9f 100644
Binary files a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.jar and b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar differ
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar.md5 b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar.md5
new file mode 100644
index 0000000..b6b7da3
--- /dev/null
+++ b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar.md5
@@ -0,0 +1 @@
+09477063a6de1ac0b1c789b64824ab0d
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar.sha1 b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar.sha1
new file mode 100644
index 0000000..04429bf
--- /dev/null
+++ b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.jar.sha1
@@ -0,0 +1 @@
+f01aca241efc98a3d0334a205cebdbcfab4b1e78
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom
similarity index 92%
rename from maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom
rename to maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom
index d2aa070..8205d8a 100644
--- a/maven-local-repository/de/mossgrabers/uitools/1.5.0/uitools-1.5.0.pom
+++ b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom
@@ -7,7 +7,7 @@
de.mossgrabers
jar
UiTools
- 1.5.0
+ 1.5.1
UTF-8
@@ -25,17 +25,17 @@
org.openjfx
javafx-controls
- 23-ea+20
+ 23.0.1
org.openjfx
javafx-web
- 23-ea+20
+ 23.0.1
org.junit.jupiter
junit-jupiter-api
- 5.11.0
+ 5.11.3
test
@@ -96,7 +96,7 @@
versions-maven-plugin
2.17.1
- .*-M.*,.*-alpha.*,.*-ea.*
+ .*-M.*,.*-alpha.*,.*-beta.*,.*-ea.*
false
@@ -110,12 +110,12 @@
org.apache.maven.plugins
maven-deploy-plugin
- 3.1.2
+ 3.1.3
org.apache.maven.plugins
maven-install-plugin
- 3.1.2
+ 3.1.3
org.apache.maven.plugins
@@ -135,7 +135,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.4.0
+ 3.5.1
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom.md5 b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom.md5
new file mode 100644
index 0000000..9e1f586
--- /dev/null
+++ b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom.md5
@@ -0,0 +1 @@
+ed0bda4c5d97e01ed70c367e2dc59563
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom.sha1 b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom.sha1
new file mode 100644
index 0000000..1964726
--- /dev/null
+++ b/maven-local-repository/de/mossgrabers/uitools/1.5.1/uitools-1.5.1.pom.sha1
@@ -0,0 +1 @@
+26612119e929ed2152b630b3402c70150884dc7b
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml b/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml
index ea93e38..6c83f31 100644
--- a/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml
+++ b/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml
@@ -3,10 +3,10 @@
de.mossgrabers
uitools
- 1.5.0
+ 1.5.1
- 1.5.0
+ 1.5.1
- 20240818222008
+ 20241021142809
diff --git a/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.md5 b/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.md5
index acbf5cc..90e6e45 100644
--- a/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.md5
+++ b/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.md5
@@ -1 +1 @@
-85a866c60b2d8c7010c69b79c74db380
\ No newline at end of file
+9a1d20d6c1b747ea116c03b0fc0f9a97
\ No newline at end of file
diff --git a/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.sha1 b/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.sha1
index 9b86a7c..73b9f85 100644
--- a/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.sha1
+++ b/maven-local-repository/de/mossgrabers/uitools/maven-metadata.xml.sha1
@@ -1 +1 @@
-cb7b0d543b2816fe806449cb5206e7980a714e45
\ No newline at end of file
+015ae3e403453609eb19be94e9724088bca26c38
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ee5cce4..2e5c14b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
4.0.0
de.mossgrabers
convertwithmoss
- 11.1.0
+ 11.2.0
jar
ConvertWithMoss
@@ -34,12 +34,12 @@
org.openjfx
javafx-controls
- 23-ea+22
+ 23.0.1
org.openjfx
javafx-web
- 23-ea+22
+ 23.0.1
com.github.trilarion
@@ -54,7 +54,7 @@
uitools
de.mossgrabers
- 1.5.0
+ 1.5.1
@@ -239,7 +239,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.5.0
+ 3.5.1
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/ZoneChannels.java b/src/main/java/de/mossgrabers/convertwithmoss/core/ZoneChannels.java
new file mode 100644
index 0000000..4d31b7a
--- /dev/null
+++ b/src/main/java/de/mossgrabers/convertwithmoss/core/ZoneChannels.java
@@ -0,0 +1,77 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2024
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.convertwithmoss.core;
+
+import java.io.IOException;
+import java.util.List;
+
+import de.mossgrabers.convertwithmoss.core.model.IGroup;
+import de.mossgrabers.convertwithmoss.core.model.ISampleZone;
+
+
+/**
+ * Enumeration for different channel configurations of zones.
+ *
+ * @author Jürgen Moßgraber
+ */
+public enum ZoneChannels
+{
+ /** All zones reference mono samples. */
+ MONO,
+ /** All zones reference stereo samples. */
+ STEREO,
+ /** All zones reference mono samples which are either hard panned left or right. */
+ SPLIT_STEREO,
+ /** Mixed mono and stereo samples. */
+ MIXED;
+
+
+ /**
+ * Checks for the channel setup across all zones of the given groups.
+ *
+ * @param groups The groups to check
+ * @return The channel configuration
+ * @throws IOException Could not detect the number of channels
+ */
+ public static ZoneChannels detectChannelConfiguration (final List groups) throws IOException
+ {
+ Boolean stereo = null;
+ Boolean splitMono = null;
+
+ for (final IGroup group: groups)
+ for (final ISampleZone sampleZone: group.getSampleZones ())
+ {
+ final boolean isStereo = sampleZone.getSampleData ().getAudioMetadata ().getChannels () == 2;
+ if (stereo == null)
+ {
+ // First iteration, store if mono or stereo
+ stereo = Boolean.valueOf (isStereo);
+ // If it is already stereo, it cannot be split stereo
+ if (isStereo && splitMono == null)
+ splitMono = Boolean.FALSE;
+ continue;
+ }
+
+ // Mixed mono/stereo
+ if (stereo.booleanValue () != isStereo)
+ return ZoneChannels.MIXED;
+
+ if (isStereo)
+ continue;
+
+ // Check for split stereo which needs to be hard panned left or right
+ if (splitMono == null || splitMono.booleanValue ())
+ {
+ final double panorama = sampleZone.getPanorama ();
+ splitMono = Boolean.valueOf (panorama <= -1 || panorama >= 1);
+ }
+ }
+
+ if (stereo != null && stereo.booleanValue ())
+ return ZoneChannels.STEREO;
+
+ return splitMono != null && splitMono.booleanValue () ? ZoneChannels.SPLIT_STEREO : ZoneChannels.MONO;
+ }
+}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java
index 7bbc62f..80fcea9 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java
@@ -12,6 +12,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.attribute.FileTime;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -51,8 +52,10 @@
import de.mossgrabers.tools.ui.BasicConfig;
import de.mossgrabers.tools.ui.Functions;
import de.mossgrabers.tools.ui.control.TitledSeparator;
+import de.mossgrabers.tools.ui.panel.BasePanel;
import de.mossgrabers.tools.ui.panel.BoxPanel;
import javafx.scene.control.CheckBox;
+import javafx.scene.control.TextField;
/**
@@ -67,13 +70,19 @@ public abstract class AbstractCreator extends AbstractCoreTask implements ICreat
private static final DestinationAudioFormat DESTINATION_FORMAT = new DestinationAudioFormat ();
private static final String IDS_NOTIFY_ERR_MISSING_SAMPLE_DATA = "IDS_NOTIFY_ERR_MISSING_SAMPLE_DATA";
- private static final String FORWARD_SLASH = "/";
+ protected static final String FORWARD_SLASH = "/";
+ // Metadata constants
private static final String WRITE_BROADCAST_AUDIO_CHUNK = "WriteBroadcastAudioChunk";
private static final String WRITE_INSTRUMENT_CHUNK = "WriteInstrumentChunk";
private static final String WRITE_SAMPLE_CHUNK = "WriteSampleChunk";
private static final String REMOVE_JUNK_CHUNK = "RemoveJunkChunk";
+ // Combine into library constants
+ private static final String COMBINE_INTO_ONE_LIBRARY = "CombineIntoOneLibrary";
+ private static final String COMBINE_FILENAME = "CombineFilename";
+
+ // Metadata options
private CheckBox updateBroadcastAudioChunkBox = null;
private CheckBox updateInstrumentChunkBox = null;
private CheckBox updateSampleChunkBox = null;
@@ -83,6 +92,10 @@ public abstract class AbstractCreator extends AbstractCoreTask implements ICreat
private boolean updateSampleChunk = false;
private boolean removeJunkChunks = false;
+ // Combine into library options
+ protected CheckBox combineIntoOneLibrary = null;
+ protected TextField combinationFilename = null;
+
/**
* Constructor.
@@ -113,6 +126,56 @@ public boolean wantsMultipleFiles ()
}
+ /**
+ * Add the UI elements for the combination of several multi-sample source into 1 library.
+ *
+ * @param panel The panel to add the widgets to
+ */
+ protected void addCombineToLibraryUI (final BasePanel panel)
+ {
+ final TitledSeparator separator = panel.createSeparator ("@IDS_COMBINE_OF_SOURCES");
+ separator.getStyleClass ().add ("titled-separator-pane");
+ this.combineIntoOneLibrary = panel.createCheckBox ("@IDS_COMBINE_IN_ONE_LIBRARY");
+ this.combinationFilename = panel.createField ("@IDS_COMBINE_LIBRARY_FILENAME");
+ }
+
+
+ protected void loadCombineToLibrarySettings (final String prefix, final BasicConfig config)
+ {
+ this.combineIntoOneLibrary.setSelected (config.getBoolean (prefix + COMBINE_INTO_ONE_LIBRARY, false));
+ this.combinationFilename.setText (config.getProperty (prefix + COMBINE_FILENAME, ""));
+ }
+
+
+ protected void saveCombineToLibrarySettings (final String prefix, final BasicConfig config)
+ {
+ config.setBoolean (prefix + COMBINE_INTO_ONE_LIBRARY, this.combineIntoOneLibrary.isSelected ());
+ config.setProperty (prefix + COMBINE_FILENAME, this.combinationFilename.getText ());
+ }
+
+
+ /**
+ * Get the library name to use for combine multi-sample sources.
+ *
+ * @param multisampleSources If no name was entered, the name of the first multi-sample is used,
+ * if any
+ * @return The name
+ */
+ protected String getCombinationLibraryName (final List multisampleSources)
+ {
+ String name = multisampleSources.get (0).getName ();
+
+ if (this.wantsMultipleFiles ())
+ {
+ final String combinationName = this.combinationFilename.getText ().trim ();
+ if (combinationName.length () > 0)
+ name = combinationName;
+ }
+
+ return createSafeFilename (name);
+ }
+
+
/**
* Create a new XML document.
*
@@ -322,6 +385,7 @@ protected void zipSampleFiles (final ZipOutputStream zipOutputStream, final Stri
this.zipSamplefile (alreadyStored, zipOutputStream, zone, multisampleSource.getMetadata ().getCreationDateTime (), relativeFolderName);
}
+ this.notifyNewline ();
}
@@ -539,6 +603,7 @@ protected List writeSamples (final File sampleFolder, final IMultisampleSo
writtenFiles.add (file);
}
}
+ this.notifyNewline ();
return writtenFiles;
}
@@ -611,6 +676,22 @@ protected List writeSamples (final File sampleFolder, final IMultisampleSo
* @throws IOException Could not retrieve the current sample rate
*/
protected static void recalculateSamplePositions (final IMultisampleSource multisampleSource, final int newSampleRate) throws IOException
+ {
+ recalculateSamplePositions (multisampleSource, newSampleRate, false);
+ }
+
+
+ /**
+ * Re-calculates the sample start, stop and loop start, stop positions for the given new sample
+ * rate of all samples/zones in the given multi-sample.
+ *
+ * @param multisampleSource The multi-sample source
+ * @param newSampleRate The new sample rate
+ * @param onlyIfLarger If true, the values are only re-calculated if the sample frequency is
+ * larger than the new sample rate
+ * @throws IOException Could not retrieve the current sample rate
+ */
+ protected static void recalculateSamplePositions (final IMultisampleSource multisampleSource, final int newSampleRate, final boolean onlyIfLarger) throws IOException
{
for (final IGroup group: multisampleSource.getGroups ())
for (final ISampleZone zone: group.getSampleZones ())
@@ -618,7 +699,10 @@ protected static void recalculateSamplePositions (final IMultisampleSource multi
final ISampleData sampleData = zone.getSampleData ();
if (sampleData == null)
continue;
- final double sampleRateRatio = newSampleRate / (double) sampleData.getAudioMetadata ().getSampleRate ();
+ final int sampleRate = sampleData.getAudioMetadata ().getSampleRate ();
+ if (onlyIfLarger && sampleRate <= newSampleRate)
+ continue;
+ final double sampleRateRatio = newSampleRate / (double) sampleRate;
zone.setStart ((int) Math.round (zone.getStart () * sampleRateRatio));
zone.setStop ((int) Math.round (zone.getStop () * sampleRateRatio));
@@ -1024,16 +1108,65 @@ protected static String formatDouble (final double value)
*/
protected File createUniqueFilename (final File destinationFolder, final String sampleName, final String extension)
{
- File multiFile;
-
- int counter = 0;
- do
+ File multiFile = new File (destinationFolder, sampleName + extension);
+ int counter = 1;
+ while (multiFile.exists ())
{
counter++;
- final String prefix = counter == 1 ? "" : " (" + counter + ")";
- multiFile = new File (destinationFolder, sampleName + prefix + extension);
- } while (multiFile.exists ());
+ multiFile = new File (destinationFolder, sampleName + " (" + counter + ")" + extension);
+ }
+ return multiFile;
+ }
+
+ /**
+ * Creates a unique file name which does not already exists in the list of the given ones.
+ *
+ * @param destinationFolder The folder in which to create the file
+ * @param sampleName The name for the file
+ * @param extension The extension for the file
+ * @param existingAbsoluteFilenames The list with existing absolute file names
+ * @return A unique name
+ */
+ protected File createUniqueFilename (final File destinationFolder, final String sampleName, final String extension, final List existingAbsoluteFilenames)
+ {
+ File multiFile = new File (destinationFolder, sampleName + extension);
+ int counter = 1;
+ while (existingAbsoluteFilenames.contains (multiFile.getAbsolutePath ()))
+ {
+ counter++;
+ multiFile = new File (destinationFolder, sampleName + " (" + counter + ")" + extension);
+ }
return multiFile;
}
+
+
+ /**
+ * Creates a DOS file name with a maximum number of 8 characters. Adds numbers to make it unique
+ * among the given other file names.
+ *
+ * @param destinationFolder The target folder
+ * @param filename The filename to shorten
+ * @param extension The file extension
+ * @param createdNames Prevent conflicts with these file names
+ * @return The unique DOS file name
+ */
+ public static String createUniqueDOSFileName (final File destinationFolder, final String filename, final String extension, final Collection createdNames)
+ {
+ String dosFilename = filename.toUpperCase ().replace (' ', '_');
+ if (dosFilename.length () > 8)
+ dosFilename = dosFilename.substring (0, 8);
+
+ int counter = 1;
+ while (createdNames.contains (dosFilename) || new File (destinationFolder, dosFilename + extension).exists ())
+ {
+ counter++;
+ final String counterStr = Integer.toString (counter);
+ dosFilename = dosFilename.substring (0, 8 - counterStr.length ()) + counterStr;
+ }
+
+ createdNames.add (dosFilename);
+
+ return dosFilename;
+ }
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/detector/AbstractDetectorTask.java b/src/main/java/de/mossgrabers/convertwithmoss/core/detector/AbstractDetectorTask.java
index 76a5482..79cbb52 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/core/detector/AbstractDetectorTask.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/core/detector/AbstractDetectorTask.java
@@ -401,9 +401,7 @@ protected IFileBasedSampleData createSampleData (final File sampleFile) throws I
sampleData = new AiffFileSampleData (sampleFile);
}
else if (fileEnding.endsWith (".ncw"))
- {
sampleData = new NcwFileSampleData (sampleFile);
- }
else
{
final AudioFileFormat audioFileFormat = AudioSystem.getAudioFileFormat (sampleFile);
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/detector/IDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/core/detector/IDetector.java
index adfa8a6..7eaa7b3 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/core/detector/IDetector.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/core/detector/IDetector.java
@@ -30,7 +30,7 @@ public interface IDetector extends ICoreTask
/**
* Validate the source parameters.
- *
+ *
* @return Returns true if all parameters are valid
*/
boolean validateParameters ();
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffCommonChunk.java b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffCommonChunk.java
index d3c1418..8f50545 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffCommonChunk.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffCommonChunk.java
@@ -87,7 +87,8 @@ public long getNumSampleFrames ()
/**
- * Get the size of the sample.
+ * Get the size of the sample. This is the number of bits in each sample point. It can be any
+ * number from 1 to 32.
*
* @return The size
*/
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFile.java b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFile.java
index feed274..fd4bb70 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFile.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFile.java
@@ -127,6 +127,17 @@ public AiffCommonChunk getCommonChunk ()
}
+ /**
+ * Get the sound data chunk.
+ *
+ * @return The sound data chunk if present
+ */
+ public AiffSoundDataChunk getSoundDataChunk ()
+ {
+ return this.soundDataChunk;
+ }
+
+
/**
* Get the marker chunk.
*
@@ -244,7 +255,7 @@ private void readLocalChunks (final List localChunks) throws IOExcepti
/**
- * Check if the chunk stack is already filled from reading the WAV file. Fill it if empty.
+ * Check if the chunk stack is already filled from reading the AIFF file. Fill it if empty.
*/
private void fillChunkStack ()
{
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFileSampleData.java b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFileSampleData.java
index 581560c..01376a1 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFileSampleData.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffFileSampleData.java
@@ -27,6 +27,8 @@
import de.mossgrabers.convertwithmoss.core.model.enumeration.LoopType;
import de.mossgrabers.convertwithmoss.core.model.implementation.AbstractFileSampleData;
import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleLoop;
+import de.mossgrabers.convertwithmoss.file.wav.DataChunk;
+import de.mossgrabers.convertwithmoss.file.wav.WaveFile;
import de.mossgrabers.tools.ui.Functions;
@@ -86,6 +88,20 @@ public void writeSample (final OutputStream outputStream) throws IOException
}
catch (final UnsupportedAudioFileException ex)
{
+ final String fileEnding = this.sampleFile.getName ().toLowerCase ();
+ if (fileEnding.endsWith (".aiff") || fileEnding.endsWith (".aif"))
+ {
+ final AiffFile aiffFile = new AiffFile (this.sampleFile);
+ final AiffCommonChunk commonChunk = aiffFile.getCommonChunk ();
+ final WaveFile wavFile = new WaveFile (commonChunk.getNumChannels (), commonChunk.getSampleRate (), commonChunk.getSampleSize (), (int) commonChunk.getNumSampleFrames ());
+ final DataChunk dataChunk = wavFile.getDataChunk ();
+ final byte [] soundData = aiffFile.getSoundDataChunk ().getData ();
+ final byte [] destinationData = dataChunk.getData ();
+ System.arraycopy (soundData, 0, destinationData, 0, Math.min (destinationData.length, soundData.length));
+ wavFile.write (outputStream);
+ return;
+ }
+
throw new IOException (ex);
}
finally
@@ -105,7 +121,7 @@ public void writeSample (final OutputStream outputStream) throws IOException
@Override
public void addZoneData (final ISampleZone zone, final boolean addRootKey, final boolean addLoops) throws IOException
{
- final AiffFile aifFile = getAiffFile ();
+ final AiffFile aifFile = this.getAiffFile ();
final AiffCommonChunk commonChunk = aifFile.getCommonChunk ();
if (commonChunk == null)
return;
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffSoundDataChunk.java b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffSoundDataChunk.java
index bbcc16d..ad38ab6 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffSoundDataChunk.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/file/aiff/AiffSoundDataChunk.java
@@ -47,6 +47,8 @@ public void read (final IffChunk chunk, final int sampleDataSize) throws IOExcep
{
this.offset = StreamUtils.readUnsigned32 (in, true);
this.blockSize = StreamUtils.readUnsigned32 (in, true);
+ if (this.offset > 0)
+ in.skip (this.offset);
if (sampleDataSize < 0)
this.soundData = in.readAllBytes ();
else
@@ -55,6 +57,17 @@ public void read (final IffChunk chunk, final int sampleDataSize) throws IOExcep
}
+ /**
+ * Get the sound data.
+ *
+ * @return The sound data
+ */
+ public byte [] getSoundData ()
+ {
+ return this.soundData;
+ }
+
+
/** {@inheritDoc} */
@Override
public String infoText ()
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/riff/AbstractListChunk.java b/src/main/java/de/mossgrabers/convertwithmoss/file/riff/AbstractListChunk.java
index 0f1f081..66182c2 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/file/riff/AbstractListChunk.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/file/riff/AbstractListChunk.java
@@ -91,15 +91,13 @@ public int hashCode ()
/** {@inheritDoc} */
@Override
- public boolean equals (Object obj)
+ public boolean equals (final Object obj)
{
if (this == obj)
return true;
- if (!super.equals (obj))
+ if (!super.equals (obj) || this.getClass () != obj.getClass ())
return false;
- if (getClass () != obj.getClass ())
- return false;
- AbstractListChunk other = (AbstractListChunk) obj;
+ final AbstractListChunk other = (AbstractListChunk) obj;
return Objects.equals (this.subChunks, other.subChunks);
}
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2DataChunk.java b/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2DataChunk.java
index 15c9367..685e3e6 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2DataChunk.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2DataChunk.java
@@ -54,7 +54,7 @@ else if (riffID == RiffID.SF_SM24_ID)
*/
public byte [] getSampleData ()
{
- return this.sampleDataChunk.getData ();
+ return this.sampleDataChunk == null ? null : this.sampleDataChunk.getData ();
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2File.java b/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2File.java
index 355615f..c107604 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2File.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/file/sf2/Sf2File.java
@@ -635,7 +635,10 @@ private void parseSampleHeader (final RIFFChunk chunk) throws ParseException
final List samples = new ArrayList<> ();
for (int i = 0; i < size / LENGTH_SHDR; i++)
{
- final Sf2SampleDescriptor sampleDescriptor = new Sf2SampleDescriptor (i, this.dataChunk.getSampleData (), this.dataChunk.getSample24Data ());
+ final byte [] sampleData = this.dataChunk.getSampleData ();
+ if (sampleData == null)
+ throw new ParseException (Functions.getMessage ("IDS_NOTIFY_ERR_MISSING_SAMPLE_DATA_CHUNK"));
+ final Sf2SampleDescriptor sampleDescriptor = new Sf2SampleDescriptor (i, sampleData, this.dataChunk.getSample24Data ());
sampleDescriptor.readHeader (i * LENGTH_SHDR, chunk);
samples.add (sampleDescriptor);
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/KeyMapping.java b/src/main/java/de/mossgrabers/convertwithmoss/format/KeyMapping.java
index 7939f96..045d5a1 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/KeyMapping.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/KeyMapping.java
@@ -227,7 +227,7 @@ private List createGroups (final List sampleData,
for (final ISampleZone zone: group)
this.extractedNames.add (zone.getName ());
- final Map> noteMap = this.detectNotes (group);
+ final Map> noteMap = detectNotes (group);
final Map groupNoteMap = convertSplitStereo (noteMap, leftChannelPatterns);
sampleMetadata.put (entry.getKey (), createKeyMaps (groupNoteMap));
@@ -615,12 +615,10 @@ private static boolean checkChannelConsistency (final Map> e: noteZoneMap.entrySet ())
- {
if (channels == -1)
channels = e.getValue ().size ();
else if (channels != e.getValue ().size ())
return false;
- }
return true;
}
@@ -633,7 +631,7 @@ else if (channels != e.getValue ().size ())
* @return The key map
* @throws MultisampleException Could not detect a note map
*/
- private Map> detectNotes (final List zones) throws MultisampleException
+ private static Map> detectNotes (final List zones) throws MultisampleException
{
// First try to detect the notes from the sample chunk
try
@@ -696,7 +694,7 @@ private static List createKeyMaps (final Map
*/
private static int lookupMidiNote (final Map keyMap, final String filename)
{
- String fn = FileUtils.getNameWithoutType (new File (filename));
+ final String fn = FileUtils.getNameWithoutType (new File (filename));
final String noteArea = fn.toUpperCase (Locale.US);
int pos = -1;
@@ -715,7 +713,7 @@ private static int lookupMidiNote (final Map keyMap, final Stri
// length
final int keyLength = n.length ();
final int strLength = str.length ();
- if (pos == -1 || keyLength > strLength || (keyLength == strLength && p + keyLength > pos + strLength))
+ if (pos == -1 || keyLength > strLength || keyLength == strLength && p + keyLength > pos + strLength)
{
pos = p;
str = n;
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/TagDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/TagDetector.java
index 09923fe..9c7c7b5 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/TagDetector.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/TagDetector.java
@@ -367,6 +367,7 @@ public class TagDetector
"Harp",
"Koto",
"Lyre",
+ "Erhu",
"Oud"
});
CATEGORIES.put (CATEGORY_SNARE, new String []
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/akai/MPCKeygroupCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/akai/MPCKeygroupCreator.java
index 9b37870..f9dc6b8 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/akai/MPCKeygroupCreator.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/akai/MPCKeygroupCreator.java
@@ -32,9 +32,13 @@
import de.mossgrabers.convertwithmoss.core.model.enumeration.TriggerType;
import de.mossgrabers.tools.XMLUtils;
import de.mossgrabers.tools.ui.BasicConfig;
+import de.mossgrabers.tools.ui.Functions;
+import de.mossgrabers.tools.ui.control.TitledSeparator;
import de.mossgrabers.tools.ui.panel.BoxPanel;
import javafx.geometry.Orientation;
import javafx.scene.Node;
+import javafx.scene.control.RadioButton;
+import javafx.scene.control.ToggleGroup;
/**
@@ -46,6 +50,9 @@
*/
public class MPCKeygroupCreator extends AbstractCreator
{
+ private static final String MPC_LAYER_LIMIT_USE_8 = "MPCLayerLimitUse8";
+
+
private enum SamplePlay
{
ONE_SHOT,
@@ -54,6 +61,9 @@ private enum SamplePlay
}
+ private ToggleGroup layerLimitGroup;
+
+
/**
* Constructor.
*
@@ -70,7 +80,20 @@ public MPCKeygroupCreator (final INotifier notifier)
public Node getEditPane ()
{
final BoxPanel panel = new BoxPanel (Orientation.VERTICAL);
- this.addWavChunkOptions (panel);
+
+ panel.createSeparator ("@IDS_MPC_LAYER_LIMIT");
+
+ this.layerLimitGroup = new ToggleGroup ();
+ final RadioButton order1 = panel.createRadioButton ("@IDS_MPC_LAYER_LIMIT_4");
+ order1.setAccessibleHelp (Functions.getMessage ("IDS_MPC_LAYER_LIMIT"));
+ order1.setToggleGroup (this.layerLimitGroup);
+ final RadioButton order2 = panel.createRadioButton ("@IDS_MPC_LAYER_LIMIT_8");
+ order2.setAccessibleHelp (Functions.getMessage ("IDS_MPC_LAYER_LIMIT"));
+ order2.setToggleGroup (this.layerLimitGroup);
+
+ final TitledSeparator separator = this.addWavChunkOptions (panel);
+ separator.getStyleClass ().add ("titled-separator-pane");
+
return panel.getPane ();
}
@@ -79,6 +102,8 @@ public Node getEditPane ()
@Override
public void loadSettings (final BasicConfig config)
{
+ this.layerLimitGroup.selectToggle (this.layerLimitGroup.getToggles ().get (config.getBoolean (MPC_LAYER_LIMIT_USE_8, false) ? 1 : 0));
+
this.loadWavChunkSettings (config, "MPC");
}
@@ -87,10 +112,23 @@ public void loadSettings (final BasicConfig config)
@Override
public void saveSettings (final BasicConfig config)
{
+ config.setBoolean (MPC_LAYER_LIMIT_USE_8, this.getLayerLimit () == 8);
+
this.saveWavChunkSettings (config, "MPC");
}
+ /**
+ * Get the limit for the number of layers in key-groups.
+ *
+ * @return 8 or 4
+ */
+ private int getLayerLimit ()
+ {
+ return this.layerLimitGroup.getToggles ().get (1).isSelected () ? 8 : 4;
+ }
+
+
/** {@inheritDoc} */
@Override
public void create (final File destinationFolder, final IMultisampleSource multisampleSource) throws IOException
@@ -154,10 +192,10 @@ private Optional createMetadata (final IMultisampleSource multisampleSou
for (final ISampleZone sampleMetadata: group.getSampleZones ())
{
- final Optional keygroupOpt = getKeygroup (keygroupsMap, sampleMetadata, document, instrumentsElement, trigger);
+ final Optional keygroupOpt = this.getKeygroup (keygroupsMap, sampleMetadata, document, instrumentsElement, trigger);
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 ()));
+ this.notifier.logError ("IDS_MPC_MORE_THAN_N_LAYERS", Integer.toString (this.getLayerLimit ()), Integer.toString (sampleMetadata.getKeyLow ()), Integer.toString (sampleMetadata.getKeyHigh ()), Integer.toString (sampleMetadata.getVelocityLow ()), Integer.toString (sampleMetadata.getVelocityHigh ()));
continue;
}
@@ -297,7 +335,7 @@ private static Element createLfoElement (final Document document)
}
- private static Optional getKeygroup (final Map> keygroupsMap, final ISampleZone zone, final Document document, final Element instrumentsElement, final TriggerType trigger)
+ private Optional getKeygroup (final Map> keygroupsMap, final ISampleZone zone, final Document document, final Element instrumentsElement, final TriggerType trigger)
{
final int keyLow = limitToDefault (zone.getKeyLow (), 0);
final int keyHigh = limitToDefault (zone.getKeyHigh (), 127);
@@ -305,25 +343,27 @@ private static Optional getKeygroup (final Map>
final boolean isSequence = zone.getPlayLogic () == PlayLogic.ROUND_ROBIN;
final List keygroups = keygroupsMap.computeIfAbsent (rangeKey, key -> new ArrayList<> ());
- // Check if a keygroup exists to which the layer can be added
+ final int layerLimit = this.getLayerLimit ();
+
+ // Check if a key-group exists to which the layer can be added
for (final Keygroup keygroup: keygroups)
- // Look for velocity or sequence keygroups (type must match)
+ // Look for velocity or sequence key-groups (type must match)
if (keygroup.isSequence () == isSequence)
{
// Velocity range must match as well for sequences
if (keygroup.isSequence () && isSequence && (limitToDefault (zone.getVelocityLow (), 1) != limitToDefault (keygroup.getVelocityLow (), 1) || limitToDefault (zone.getVelocityHigh (), 127) != limitToDefault (keygroup.getVelocityHigh (), 127)))
continue;
- // Matching keygroup with free layer found
- if (keygroup.getLayerCount () < 4)
+ // Matching key-group with free layer found
+ if (keygroup.getLayerCount () < layerLimit)
return Optional.of (keygroup);
- // Can only have 1 sequence keygroup since round robin is per keygroup
+ // Can only have 1 sequence key-group since round robin is per key-group
if (isSequence)
return Optional.empty ();
}
- // No existing keygroup found, create a new one (Instrument is a keygroup)
+ // No existing key-group found, create a new one (Instrument is a key-group)
final Element instrumentElement = document.createElement ("Instrument");
instrumentElement.setAttribute ("number", Integer.toString (calcInstrumentNumber (keygroupsMap)));
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java
index 791eabe..ce9fa7b 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/decentsampler/DecentSamplerCreator.java
@@ -9,9 +9,14 @@
import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.zip.ZipOutputStream;
@@ -30,6 +35,7 @@
import de.mossgrabers.convertwithmoss.core.model.ISampleZone;
import de.mossgrabers.convertwithmoss.core.model.enumeration.PlayLogic;
import de.mossgrabers.convertwithmoss.core.model.enumeration.TriggerType;
+import de.mossgrabers.tools.FileUtils;
import de.mossgrabers.tools.XMLUtils;
import de.mossgrabers.tools.ui.BasicConfig;
import de.mossgrabers.tools.ui.Functions;
@@ -51,7 +57,35 @@
*/
public class DecentSamplerCreator extends AbstractCreator
{
- private boolean isOutputFormatLibrary;
+ private static final String LIBRARY_INFO_CONTENT = "\n\n";
+
+
+ private enum DsOutputFormat
+ {
+ PRESET,
+ LIBRARY,
+ BUNDLE
+ }
+
+
+ private class PresetResult
+ {
+ String dsPreset;
+ public File dsPresetFile;
+ String sampleFolder;
+ IMultisampleSource sampleSource;
+ }
+
+
+ private static final Map OUTPUT_FORMAT_ENDINGS = new EnumMap<> (DsOutputFormat.class);
+ static
+ {
+ OUTPUT_FORMAT_ENDINGS.put (DsOutputFormat.PRESET, ".dspreset");
+ OUTPUT_FORMAT_ENDINGS.put (DsOutputFormat.LIBRARY, ".dslibrary");
+ OUTPUT_FORMAT_ENDINGS.put (DsOutputFormat.BUNDLE, ".dsbundle");
+ }
+
+ private DsOutputFormat outputFormat = DsOutputFormat.PRESET;
private static final String DS_OUTPUT_FORMAT_LIBRARY = "DsOutputFormatPreset";
private static final String DS_OUTPUT_MAKE_MONOPHONIC = "DsOutputMakeMonophonic";
@@ -92,12 +126,16 @@ public Node getEditPane ()
final RadioButton order2 = panel.createRadioButton ("@IDS_DS_LIBRARY");
order2.setAccessibleHelp (Functions.getMessage ("IDS_DS_OUTPUT_FORMAT"));
order2.setToggleGroup (this.outputFormatGroup);
+ final RadioButton order3 = panel.createRadioButton ("@IDS_DS_BUNDLE");
+ order3.setAccessibleHelp (Functions.getMessage ("IDS_DS_OUTPUT_FORMAT"));
+ order3.setToggleGroup (this.outputFormatGroup);
- this.makeMonophonicBox = panel.createCheckBox ("@IDS_DS_MAKE_MONOPHONIC");
+ this.addCombineToLibraryUI (panel);
final TitledSeparator separator = panel.createSeparator ("@IDS_DS_USER_INTERFACE");
separator.getStyleClass ().add ("titled-separator-pane");
+ this.makeMonophonicBox = panel.createCheckBox ("@IDS_DS_MAKE_MONOPHONIC");
this.addReverbBox = panel.createCheckBox ("@IDS_DS_ADD_REVERB");
this.addWavChunkOptions (panel).getStyleClass ().add ("titled-separator-pane");
@@ -110,10 +148,11 @@ public Node getEditPane ()
@Override
public void loadSettings (final BasicConfig config)
{
- this.outputFormatGroup.selectToggle (this.outputFormatGroup.getToggles ().get (config.getBoolean (DS_OUTPUT_FORMAT_LIBRARY, true) ? 1 : 0));
+ Functions.setSelectedToggleIndex (this.outputFormatGroup, config.getInteger (DS_OUTPUT_FORMAT_LIBRARY, 0));
this.makeMonophonicBox.setSelected (config.getBoolean (DS_OUTPUT_MAKE_MONOPHONIC, false));
this.addReverbBox.setSelected (config.getBoolean (DS_OUTPUT_ADD_REVERB, true));
+ this.loadCombineToLibrarySettings ("Ds", config);
this.loadWavChunkSettings (config, "Ds");
}
@@ -122,35 +161,93 @@ public void loadSettings (final BasicConfig config)
@Override
public void saveSettings (final BasicConfig config)
{
- config.setBoolean (DS_OUTPUT_FORMAT_LIBRARY, this.isOutputFormatLibrary ());
+ config.setInteger (DS_OUTPUT_FORMAT_LIBRARY, Functions.getSelectedToggleIndex (this.outputFormatGroup));
config.setBoolean (DS_OUTPUT_MAKE_MONOPHONIC, this.makeMonophonicBox.isSelected ());
config.setBoolean (DS_OUTPUT_ADD_REVERB, this.addReverbBox.isSelected ());
+ this.saveCombineToLibrarySettings ("Ds", config);
this.saveWavChunkSettings (config, "Ds");
}
+ /** {@inheritDoc} */
+ @Override
+ public boolean wantsMultipleFiles ()
+ {
+ return this.combineIntoOneLibrary.isSelected () && this.getOutputFormat () != DsOutputFormat.PRESET;
+ }
+
+
/** {@inheritDoc} */
@Override
public void create (final File destinationFolder, final IMultisampleSource multisampleSource) throws IOException
{
- this.seqPosition = 1;
+ this.create (destinationFolder, Collections.singletonList (multisampleSource));
+ }
- this.setOutputToLibrary (this.isOutputFormatLibrary ());
- final String sampleName = createSafeFilename (multisampleSource.getName ());
- final String relativeFolderName = this.isOutputFormatLibrary ? FOLDER_POSTFIX.trim () : sampleName + FOLDER_POSTFIX;
- final Optional metadata = this.createMetadata (relativeFolderName, multisampleSource);
- if (metadata.isEmpty ())
+ /** {@inheritDoc} */
+ @Override
+ public void create (final File destinationFolder, final List multisampleSources) throws IOException
+ {
+ if (multisampleSources.isEmpty ())
return;
- final File multiFile = this.createUniqueFilename (destinationFolder, sampleName, this.isOutputFormatLibrary ? ".dslibrary" : ".dspreset");
- this.notifier.log ("IDS_NOTIFY_STORING", multiFile.getAbsolutePath ());
+ this.outputFormat = this.getOutputFormat ();
+ final boolean isPresetOutput = this.outputFormat == DsOutputFormat.PRESET;
+
+ final String extension = OUTPUT_FORMAT_ENDINGS.get (this.outputFormat);
+ final List otherOutputFiles = new ArrayList<> ();
+ final List results = new ArrayList<> ();
+ for (final IMultisampleSource multisampleSource: multisampleSources)
+ {
+ this.seqPosition = 1;
+
+ final PresetResult presetResult = new PresetResult ();
+ presetResult.sampleSource = multisampleSource;
+
+ // Make sure the file name is unique among either the files in the destination folder or
+ // inside of the library
+ String sampleName = createSafeFilename (multisampleSource.getName ());
+ presetResult.dsPresetFile = isPresetOutput ? this.createUniqueFilename (destinationFolder, sampleName, ".dspreset") : this.createUniqueFilename (destinationFolder, sampleName, ".dspreset", otherOutputFiles);
+ sampleName = FileUtils.getNameWithoutType (presetResult.dsPresetFile);
+ presetResult.sampleFolder = sampleName + FOLDER_POSTFIX;
+
+ otherOutputFiles.add (presetResult.dsPresetFile.getAbsolutePath ());
+
+ final Optional metadata = this.createPresetMetadata (presetResult.sampleFolder, multisampleSource);
+ if (metadata.isEmpty ())
+ continue;
+ presetResult.dsPreset = metadata.get ();
+ results.add (presetResult);
+ }
- if (this.isOutputFormatLibrary)
- this.storeLibrary (relativeFolderName, multisampleSource, sampleName, multiFile, metadata.get ());
+ if (this.outputFormat == DsOutputFormat.PRESET)
+ for (final PresetResult presetResult: results)
+ {
+ this.notifier.log ("IDS_NOTIFY_STORING", presetResult.dsPresetFile.getAbsolutePath ());
+ this.storePreset (destinationFolder, presetResult);
+ }
else
- this.storePreset (relativeFolderName, destinationFolder, multisampleSource, multiFile, metadata.get ());
+ {
+ final boolean storeInOneLibrary = this.wantsMultipleFiles ();
+ final String multiSampleName = this.getCombinationLibraryName (multisampleSources);
+ final File multiFile = this.createUniqueFilename (destinationFolder, multiSampleName, extension);
+ this.notifier.log ("IDS_NOTIFY_STORING", multiFile.getAbsolutePath ());
+ if (this.outputFormat == DsOutputFormat.BUNDLE)
+ {
+ if (storeInOneLibrary)
+ this.storeBundle (multiFile, results);
+ else
+ // Note method is called for each multi-source individually!
+ this.storeBundle (multiFile, Collections.singletonList (results.get (0)));
+ }
+ else if (storeInOneLibrary)
+ this.storeLibrary (multiFile, results);
+ else
+ // Note method is called for each multi-source individually!
+ this.storeLibrary (multiFile, Collections.singletonList (results.get (0)));
+ }
this.notifier.log ("IDS_NOTIFY_PROGRESS_DONE");
}
@@ -159,43 +256,69 @@ public void create (final File destinationFolder, final IMultisampleSource multi
/**
* Create a dspreset file.
*
- * @param relativeFolderName A relative path for the samples
* @param destinationFolder Where to store the preset file
- * @param multisampleSource The multi-sample to store in the library
- * @param multiFile The file of the dslibrary
- * @param metadata The dspreset metadata description file
+ * @param presetResult The preset to store
* @throws IOException Could not store the file
*/
- private void storePreset (final String relativeFolderName, final File destinationFolder, final IMultisampleSource multisampleSource, final File multiFile, final String metadata) throws IOException
+ private void storePreset (final File destinationFolder, final PresetResult presetResult) throws IOException
{
- try (final FileWriter writer = new FileWriter (multiFile, StandardCharsets.UTF_8))
+ try (final FileWriter writer = new FileWriter (presetResult.dsPresetFile, StandardCharsets.UTF_8))
{
- writer.write (metadata);
+ writer.write (presetResult.dsPreset);
}
// Store all samples
- final File sampleFolder = new File (destinationFolder, relativeFolderName);
+ final File sampleFolder = new File (destinationFolder, presetResult.sampleFolder);
safeCreateDirectory (sampleFolder);
- this.writeSamples (sampleFolder, multisampleSource);
+ this.writeSamples (sampleFolder, presetResult.sampleSource);
+ }
+
+
+ /**
+ * Create a dsbundle file.
+ *
+ * @param multiFile The file of the dsbundle
+ * @param presetResults The presets to store in the bundle
+ * @throws IOException Could not store the file
+ */
+ private void storeBundle (final File multiFile, final List presetResults) throws IOException
+ {
+ final File bundleFolder = multiFile;
+
+ // The bundle name is a directory!
+ if (!bundleFolder.mkdirs ())
+ throw new IOException (Functions.getMessage ("IDS_NOTIFY_FOLDER_COULD_NOT_BE_CREATED", multiFile.getAbsolutePath ()));
+
+ for (final PresetResult presetResult: presetResults)
+ {
+ // Add bundle sub-path
+ presetResult.dsPresetFile = new File (bundleFolder, presetResult.dsPresetFile.getName ());
+ this.storePreset (bundleFolder, presetResult);
+ }
+
+ final String libraryName = FileUtils.getNameWithoutType (multiFile);
+ Files.writeString (new File (bundleFolder, "DSLibraryInfo.xml").toPath (), LIBRARY_INFO_CONTENT.replace ("%LIBRARY_NAME%", libraryName));
}
/**
* Create a dslibrary file.
*
- * @param relativeFolderName A relative path for the samples
- * @param multisampleSource The multi-sample to store in the library
- * @param sampleName The name of the multi-sample
* @param multiFile The file of the dslibrary
- * @param metadata The dspreset metadata description file
+ * @param presetResults The presets to store in the library
* @throws IOException Could not store the file
*/
- private void storeLibrary (final String relativeFolderName, final IMultisampleSource multisampleSource, final String sampleName, final File multiFile, final String metadata) throws IOException
+ private void storeLibrary (final File multiFile, final List presetResults) throws IOException
{
+ final String libraryPath = FileUtils.getNameWithoutType (multiFile) + FORWARD_SLASH;
+
try (final ZipOutputStream zos = new ZipOutputStream (new FileOutputStream (multiFile)))
{
- AbstractCreator.zipTextFile (zos, sampleName + ".dspreset", metadata);
- this.zipSampleFiles (zos, relativeFolderName, multisampleSource);
+ for (final PresetResult presetResult: presetResults)
+ {
+ AbstractCreator.zipTextFile (zos, libraryPath + presetResult.dsPresetFile.getName (), presetResult.dsPreset);
+ this.zipSampleFiles (zos, libraryPath + presetResult.sampleFolder, presetResult.sampleSource);
+ }
}
}
@@ -207,7 +330,7 @@ private void storeLibrary (final String relativeFolderName, final IMultisampleSo
* @param multisampleSource The multi-sample
* @return The XML structure
*/
- private Optional createMetadata (final String folderName, final IMultisampleSource multisampleSource)
+ private Optional createPresetMetadata (final String folderName, final IMultisampleSource multisampleSource)
{
final Optional optionalDocument = this.createXMLDocument ();
if (optionalDocument.isEmpty ())
@@ -367,24 +490,13 @@ private static void createSample (final Document document, final String folderNa
/**
- * Set the output format to otherwise preset only.
- *
- * @param outputFormatLibrary True to output dspreset files otherwise dslibrary
- */
- public void setOutputToLibrary (final boolean outputFormatLibrary)
- {
- this.isOutputFormatLibrary = outputFormatLibrary;
- }
-
-
- /**
- * Check if the toggle setting is set to preset or library.
+ * Get the selected output format.
*
- * @return True if library
+ * @return The output format
*/
- private boolean isOutputFormatLibrary ()
+ private DsOutputFormat getOutputFormat ()
{
- return this.outputFormatGroup.getToggles ().get (1).isSelected ();
+ return DsOutputFormat.values ()[Functions.getSelectedToggleIndex (this.outputFormatGroup)];
}
@@ -486,7 +598,7 @@ private void createEffects (final Document document, final Element rootElement)
private static void createPitchModulator (final Document document, final Element modulatorsElement, final IEnvelopeModulator pitchModulator, final int groupIndex)
{
final double envelopeDepth = pitchModulator.getDepth ();
- // Only positive values allowed in Decent Sampler
+ // Only positive values allowed in DecentSampler
if (envelopeDepth <= 0)
return;
@@ -506,7 +618,8 @@ private static void createPitchModulator (final Document document, final Element
bindingElement.setAttribute ("parameter", "GROUP_TUNING");
bindingElement.setAttribute ("translation", "linear");
bindingElement.setAttribute ("translationOutputMin", "0");
- bindingElement.setAttribute ("translationOutputMax", Integer.toString (IEnvelope.MAX_ENVELOPE_DEPTH));
+ // Unit are semi-tones; maximum value is 120 semi-tones
+ bindingElement.setAttribute ("translationOutputMax", Integer.toString (IEnvelope.MAX_ENVELOPE_DEPTH / 100));
bindingElement.setAttribute ("modBehavior", "add");
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Block.java b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Block.java
index 72d4e8b..04e8dc8 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Block.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Block.java
@@ -35,6 +35,8 @@ class EXS24Block
public static final int TYPE_PARAMS = 0x04;
/** An unknown block. */
public static final int TYPE_UNKNOWN = 0x08;
+ /** Another unknown block. */
+ public static final int TYPE_UNKNOWN_2 = 0x0B;
private static final String BIG_ENDIAN_MAGIC = "SOBT";
private static final String LITTLE_ENDIAN_MAGIC = "TBOS";
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24DetectorTask.java b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24DetectorTask.java
index 7ab250b..c84fa7f 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24DetectorTask.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24DetectorTask.java
@@ -137,6 +137,11 @@ private List readEXSFile (final File file, final InputStream
// No idea what that is but it is 4 bytes long...
break;
+ case EXS24Block.TYPE_UNKNOWN_2:
+ // No idea what that is but it is 4 bytes long...
+ java.nio.file.Files.write (java.nio.file.Paths.get ("C:/Users/mos/Desktop/filename.dat"), block.content);
+ break;
+
default:
this.notifier.logError ("IDS_EXS_UNKNOWN_EXS_BLOCK_TYPE", Integer.toString (block.type));
break;
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPChannel.java b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPChannel.java
new file mode 100644
index 0000000..57a0453
--- /dev/null
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPChannel.java
@@ -0,0 +1,20 @@
+// Written by Jürgen Moßgraber - mossgrabers.de
+// (c) 2019-2024
+// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt
+
+package de.mossgrabers.convertwithmoss.format.kmp;
+
+/**
+ * Indicator for KMPFile which type of channel to write.
+ *
+ * @author Jürgen Moßgraber
+ */
+public enum KMPChannel
+{
+ /** The left channel. */
+ LEFT,
+ /** The right channel. */
+ RIGHT,
+ /** A mono channel. */
+ MONO
+}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPCreator.java
index 66ed5d7..3d19e5f 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPCreator.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPCreator.java
@@ -8,16 +8,26 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
-import java.util.Set;
import de.mossgrabers.convertwithmoss.core.IMultisampleSource;
import de.mossgrabers.convertwithmoss.core.INotifier;
+import de.mossgrabers.convertwithmoss.core.ZoneChannels;
import de.mossgrabers.convertwithmoss.core.creator.AbstractCreator;
import de.mossgrabers.convertwithmoss.core.model.IGroup;
import de.mossgrabers.convertwithmoss.core.model.ISampleZone;
-import de.mossgrabers.tools.FileUtils;
+import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultGroup;
+import de.mossgrabers.tools.StringUtils;
+import de.mossgrabers.tools.ui.BasicConfig;
+import de.mossgrabers.tools.ui.Functions;
+import de.mossgrabers.tools.ui.panel.BoxPanel;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
/**
@@ -27,6 +37,15 @@
*/
public class KMPCreator extends AbstractCreator
{
+ private static final String KMP_WRITE_GROUP_KMPS = "KMPWriteGroupKmps";
+ private static final String KMP_GAIN_PLUS_12 = "KMPGainPlus12";
+ private static final String KMP_MAXIMIZE_VOLUME = "KMPMaximizeVolume";
+
+ private CheckBox writeGroupKmps;
+ private CheckBox gainPlus12;
+ private CheckBox maximizeVolume;
+
+
/**
* Constructor.
*
@@ -38,71 +57,157 @@ public KMPCreator (final INotifier notifier)
}
+ /** {@inheritDoc} */
+ @Override
+ public Node getEditPane ()
+ {
+ final BoxPanel panel = new BoxPanel (Orientation.VERTICAL);
+ panel.createSeparator ("@IDS_KMP_OPTIONS");
+ this.writeGroupKmps = panel.createCheckBox ("@IDS_KMP_WRITE_KMP_FOR_EACH_GROUP", "@IDS_KMP_WRITE_KMP_FOR_EACH_GROUP_TOOLTIP");
+ this.gainPlus12 = panel.createCheckBox ("@IDS_KMP_GAIN_12DB");
+ this.maximizeVolume = panel.createCheckBox ("@IDS_KMP_MAXIMIZE_VOLUME", "@IDS_KMP_MAXIMIZE_VOLUME_TOOLTIP");
+ return panel.getPane ();
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void loadSettings (final BasicConfig config)
+ {
+ this.writeGroupKmps.setSelected (config.getBoolean (KMP_WRITE_GROUP_KMPS, false));
+ this.gainPlus12.setSelected (config.getBoolean (KMP_GAIN_PLUS_12, false));
+ this.maximizeVolume.setSelected (config.getBoolean (KMP_MAXIMIZE_VOLUME, false));
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void saveSettings (final BasicConfig config)
+ {
+ config.setBoolean (KMP_WRITE_GROUP_KMPS, this.writeGroupKmps.isSelected ());
+ config.setBoolean (KMP_GAIN_PLUS_12, this.gainPlus12.isSelected ());
+ config.setBoolean (KMP_MAXIMIZE_VOLUME, this.maximizeVolume.isSelected ());
+ }
+
+
/** {@inheritDoc} */
@Override
public void create (final File destinationFolder, final IMultisampleSource multisampleSource) throws IOException
{
- AbstractCreator.recalculateSamplePositions (multisampleSource, 44100);
+ AbstractCreator.recalculateSamplePositions (multisampleSource, 48000, true);
- final String sampleName = createSafeFilename (multisampleSource.getName ());
+ final String multiSampleName = createSafeFilename (multisampleSource.getName ());
// Create a sub-folder for the KMP file(s) and all samples
- final File subFolder = new File (destinationFolder, FileUtils.createDOSFileName (sampleName, new HashSet<> ()));
+ final File subFolder = new File (destinationFolder, createUniqueDOSFileName (destinationFolder, multiSampleName, "", new HashSet<> ()));
if (!subFolder.exists () && !subFolder.mkdirs ())
{
this.notifier.logError ("IDS_NOTIFY_FOLDER_COULD_NOT_BE_CREATED", subFolder.getAbsolutePath ());
return;
}
- // KMP format supports only 1 group. Therefore, create 1 KMP file for each group
+ // KMP format supports only 1 group. Therefore, either create 1 KMP file for each group or
+ // combine them into 1
final List groups = multisampleSource.getNonEmptyGroups (true);
- final boolean needsMultipleKMPs = groups.size () > 1;
- final Set createdKMPNames = new HashSet<> ();
- for (final IGroup group: groups)
+ final ZoneChannels zoneChannels = ZoneChannels.detectChannelConfiguration (groups);
+ this.notifier.log ("IDS_KMP_SOURCE_SAMPLES_FORMAT", zoneChannels.toString ());
+
+ final List createdKMPNames = new ArrayList<> ();
+ int kmpIndex = 0;
+
+ if (!this.writeGroupKmps.isSelected () || zoneChannels == ZoneChannels.SPLIT_STEREO)
{
- final ISampleZone zone = group.getSampleZones ().get (0);
- final String groupName = needsMultipleKMPs ? String.format ("%d-%s", Integer.valueOf (zone.getVelocityHigh ()), sampleName) : sampleName;
- final String kmpFileName = FileUtils.createDOSFileName (groupName, createdKMPNames) + ".KMP";
- File groupSubFolder = subFolder;
- if (needsMultipleKMPs)
- {
- groupSubFolder = new File (subFolder, kmpFileName);
- if (!groupSubFolder.exists () && !groupSubFolder.mkdirs ())
- {
- this.notifier.logError ("IDS_NOTIFY_FOLDER_COULD_NOT_BE_CREATED", groupSubFolder.getAbsolutePath ());
- return;
- }
- }
- final File multiFile = new File (groupSubFolder, kmpFileName);
- if (multiFile.exists ())
- {
- this.notifier.logError ("IDS_NOTIFY_ALREADY_EXISTS", multiFile.getAbsolutePath ());
- continue;
- }
-
- this.notifier.log ("IDS_NOTIFY_STORING", multiFile.getAbsolutePath ());
- this.storeKMP (multiFile, kmpFileName, groupName, group);
-
- this.notifier.log ("IDS_NOTIFY_PROGRESS_DONE");
+ final IGroup combinedGroup = new DefaultGroup ();
+ for (final IGroup group: groups)
+ combinedGroup.getSampleZones ().addAll (group.getSampleZones ());
+ kmpIndex = this.storeKMP (subFolder, multiSampleName, combinedGroup, zoneChannels, kmpIndex, createdKMPNames);
}
+ else
+ for (final IGroup group: groups)
+ kmpIndex = this.storeKMP (subFolder, multiSampleName, group, zoneChannels, kmpIndex, createdKMPNames);
+
+ // Write a KSC file with all created KMP files
+ final StringBuilder sb = new StringBuilder ("#KORG Script Version 1.0\r\n");
+ for (final String kmpFilename: createdKMPNames)
+ sb.append (kmpFilename).append (".KMP\r\n");
+ final File kscFile = new File (subFolder, subFolder.getName () + ".KSC");
+ this.notifier.log ("IDS_NOTIFY_STORING", kscFile.getAbsolutePath ());
+ Files.writeString (kscFile.toPath (), sb.toString ());
+ this.notifier.log ("IDS_NOTIFY_PROGRESS_DONE");
}
/**
* Create a KMP file.
*
- * @param multiFile The KMP file to create
- * @param dosFilename Classic 8.3 file name
- * @param groupName The name to use for the group
- * @param group The group to store
+ * @param subFolder The sub-folder to store to
+ * @param sampleName The KMP file to create
+ * @param group The group
+ * @param kmpIndex The group to store
+ * @param createdKMPNames The index of the KMP file
* @throws IOException Could not store the file
*/
- private void storeKMP (final File multiFile, final String dosFilename, final String groupName, final IGroup group) throws IOException
+ private int storeKMP (final File subFolder, final String sampleName, final IGroup group, final ZoneChannels zoneChannels, final int kmpIndex, final Collection createdKMPNames) throws IOException
{
- try (final OutputStream out = new FileOutputStream (multiFile))
+ final boolean gain12dB = this.gainPlus12.isSelected ();
+ final boolean maxVolume = this.maximizeVolume.isSelected ();
+ final String filename = StringUtils.rightPadSpaces (sampleName, 6);
+
+ switch (zoneChannels)
{
- final KMPFile kmpFile = new KMPFile (this.notifier, dosFilename, groupName, group);
- kmpFile.write (multiFile.getParentFile (), out);
+ default:
+ case MONO:
+ this.storeKMPChannel (subFolder, filename, sampleName, kmpIndex, KMPChannel.MONO, group, gain12dB, maxVolume, createdKMPNames);
+ return kmpIndex + 1;
+
+ case STEREO, MIXED:
+ // Write 2 KMP files for left/right
+ this.storeKMPChannel (subFolder, filename, sampleName, kmpIndex, KMPChannel.LEFT, group, gain12dB, maxVolume, createdKMPNames);
+ this.storeKMPChannel (subFolder, filename, sampleName, kmpIndex + 1, KMPChannel.RIGHT, group, gain12dB, maxVolume, createdKMPNames);
+ return kmpIndex + 2;
+
+ case SPLIT_STEREO:
+ // First split into 2 groups for left and right
+ final IGroup leftGroup = new DefaultGroup ();
+ final IGroup rightGroup = new DefaultGroup ();
+ for (final ISampleZone zone: group.getSampleZones ())
+ if (zone.getPanorama () <= -1)
+ leftGroup.addSampleZone (zone);
+ else
+ rightGroup.addSampleZone (zone);
+ this.storeKMPChannel (subFolder, filename, sampleName, kmpIndex, KMPChannel.LEFT, leftGroup, gain12dB, maxVolume, createdKMPNames);
+ this.storeKMPChannel (subFolder, filename, sampleName, kmpIndex + 1, KMPChannel.RIGHT, rightGroup, gain12dB, maxVolume, createdKMPNames);
+ return kmpIndex + 2;
}
}
+
+
+ private void storeKMPChannel (final File subFolder, final String filename, final String sampleWithGroupName, final int kmpIndex, final KMPChannel kmpChannel, final IGroup group, final boolean gain12dB, final boolean maxVolume, final Collection createdKMPNames) throws IOException
+ {
+ final String kmpFileName = createUniqueFilename (subFolder, filename, kmpIndex, createdKMPNames);
+ final File kmpFilePath = this.createFile (subFolder, kmpFileName);
+ final KMPFile kmpFile = new KMPFile (this.notifier, kmpFileName, sampleWithGroupName, group.getSampleZones (), gain12dB, maxVolume);
+
+ try (final OutputStream out = new FileOutputStream (kmpFilePath))
+ {
+ kmpFile.write (kmpFilePath.getParentFile (), out, kmpIndex, kmpChannel);
+ }
+ this.notifier.logText ("\n");
+ }
+
+
+ private static String createUniqueFilename (final File subFolder, final String filename, final int kmpIndex, final Collection createdKMPNames)
+ {
+ return createUniqueDOSFileName (subFolder, String.format ("%s%02d", filename, Integer.valueOf (kmpIndex)), ".KMP", createdKMPNames) + ".KMP";
+ }
+
+
+ private File createFile (final File subFolder, final String kmpFileName) throws IOException
+ {
+ final File file = new File (subFolder, kmpFileName);
+ if (file.exists ())
+ throw new IOException (Functions.getMessage ("IDS_NOTIFY_ALREADY_EXISTS", file.getAbsolutePath ()));
+ this.notifier.log ("IDS_NOTIFY_STORING", file.getAbsolutePath ());
+ return file;
+ }
}
\ No newline at end of file
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPDetectorTask.java b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPDetectorTask.java
index 001dcae..c3cc206 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPDetectorTask.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPDetectorTask.java
@@ -35,7 +35,7 @@ public class KMPDetectorTask extends AbstractDetectorTask
* Constructor.
*
* @param notifier The notifier
- * @param consumer The consumer that handles the detected multisample sources
+ * @param consumer The consumer that handles the detected multi-sample sources
* @param sourceFolder The top source folder for the detection
* @param metadata Additional metadata configuration parameters
*/
@@ -53,7 +53,7 @@ protected List readFile (final File sourceFile)
try (final FileInputStream stream = new FileInputStream (sourceFile))
{
- new KMPFile (this.notifier, sourceFile, group).read (stream);
+ new KMPFile (this.notifier, sourceFile, group.getSampleZones ()).read (stream);
}
catch (final IOException | ParseException ex)
{
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPFile.java
index e0a8989..204a64d 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPFile.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KMPFile.java
@@ -12,11 +12,11 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.util.Collections;
import java.util.List;
import de.mossgrabers.convertwithmoss.core.INotifier;
import de.mossgrabers.convertwithmoss.core.creator.AbstractCreator;
-import de.mossgrabers.convertwithmoss.core.model.IGroup;
import de.mossgrabers.convertwithmoss.core.model.ISampleZone;
import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleZone;
import de.mossgrabers.convertwithmoss.exception.CompressionNotSupportedException;
@@ -34,37 +34,39 @@
public class KMPFile
{
/** ID for KMP chunk. */
- private static final String KMP_MSP_ID = "MSP1";
+ private static final String KMP_MSP_ID = "MSP1";
/** ID for KMP name chunk. */
- private static final String KMP_NAME_ID = "NAME";
+ private static final String KMP_NAME_ID = "NAME";
/** ID for KMP relative parameter chunk 1. */
- private static final String KMP_REL1_ID = "RLP1";
+ private static final String KMP_REL1_ID = "RLP1";
/** ID for KMP relative parameter chunk 2. */
- private static final String KMP_REL2_ID = "RLP2";
+ private static final String KMP_REL2_ID = "RLP2";
/** ID for KMP relative parameter chunk 3. */
- private static final String KMP_REL3_ID = "RLP3";
+ private static final String KMP_REL3_ID = "RLP3";
/** ID for KMP multi-sample number chunk. */
- private static final String KMP_NUMBER_ID = "MNO1";
+ private static final String KMP_NUMBER_ID = "MNO1";
- private static final int KMP_MSP_SIZE = 18;
- private static final int KMP_NAME_SIZE = 24;
- private static final int KMP_REL1_SIZE = 18;
- private static final int KMP_REL2_SIZE = 4;
- private static final int KMP_REL3_SIZE = 6;
- private static final int KMP_NUMBER_SIZE = 4;
+ private static final int KMP_MSP_SIZE = 18;
+ private static final int KMP_NAME_SIZE = 24;
+ private static final int KMP_REL1_SIZE = 18;
+ private static final int KMP_REL2_SIZE = 4;
+ private static final int KMP_REL3_SIZE = 6;
+ private static final int KMP_NUMBER_SIZE = 4;
- private static final String SAMPLE_SKIPPED = "SKIPPEDSAMPL";
- private static final String SAMPLE_INTERNAL = "INTERNAL";
+ private static final String SAMPLE_SKIPPED = "SKIPPEDSAMPL";
+ private static final String SAMPLE_INTERNAL = "INTERNAL";
- private final INotifier notifier;
- private final File sampleFolder1;
- private final File sampleFolder2;
+ private final INotifier notifier;
+ private final File sampleFolder1;
+ private final File sampleFolder2;
- private String name;
- private int numSamples;
- private String nameLong;
+ private String name;
+ private int numSamples;
+ private String nameLong;
- private final IGroup group;
+ private final List zones;
+ private boolean gain12dB = false;
+ private boolean maxVolume = false;
/**
@@ -73,16 +75,22 @@ public class KMPFile
* @param notifier For logging errors
* @param dosFilename Classic 8.3 file name
* @param groupName The name of the group
- * @param group The group
+ * @param zones The sample zones
+ * @param gain12dB Enables the +12dB option, if true
+ * @param maxVolume Sets all sample volumes to +99, if true
*/
- public KMPFile (final INotifier notifier, final String dosFilename, final String groupName, final IGroup group)
+ public KMPFile (final INotifier notifier, final String dosFilename, final String groupName, final List zones, final boolean gain12dB, final boolean maxVolume)
{
this.notifier = notifier;
this.sampleFolder1 = null;
this.sampleFolder2 = null;
- this.group = group;
- this.numSamples = this.group.getSampleZones ().size ();
+ // Korg M3 crashes if samples are not in ascending order!
+ this.zones = sortByKeyHigh (zones);
+
+ this.numSamples = this.zones.size ();
+ this.gain12dB = gain12dB;
+ this.maxVolume = maxVolume;
this.name = dosFilename;
this.nameLong = groupName;
@@ -94,18 +102,18 @@ public KMPFile (final INotifier notifier, final String dosFilename, final String
*
* @param notifier For logging errors
* @param kmpFile The KMP file
- * @param group The group where to add the KSF zones
+ * @param zones Where to add the sample zones
* @throws IOException Could not read the file
* @throws ParseException Error parsing the chunks
*/
- public KMPFile (final INotifier notifier, final File kmpFile, final IGroup group) throws IOException, ParseException
+ public KMPFile (final INotifier notifier, final File kmpFile, final List zones) throws IOException, ParseException
{
this.notifier = notifier;
this.sampleFolder1 = kmpFile.getParentFile ();
this.sampleFolder2 = new File (this.sampleFolder1, FileUtils.getNameWithoutType (kmpFile));
- this.group = group;
+ this.zones = zones;
}
@@ -190,14 +198,14 @@ private void readMultisampleChunk (final DataInputStream in) throws IOException
in.read ();
for (int i = 0; i < this.numSamples; i++)
- this.group.addSampleZone (new DefaultSampleZone ());
+ this.zones.add (new DefaultSampleZone ());
}
private void readParameterChunk1 (final DataInputStream in) throws IOException, ParseException
{
int lowerKey = 0;
- for (final ISampleZone zone: this.group.getSampleZones ())
+ for (final ISampleZone zone: this.zones)
{
final int originalKey = in.read ();
@@ -207,7 +215,10 @@ private void readParameterChunk1 (final DataInputStream in) throws IOException,
zone.setKeyHigh (in.read ());
lowerKey = AbstractCreator.limitToDefault (zone.getKeyHigh (), 127) + 1;
zone.setTune (in.readByte () / 100.0);
- zone.setGain (in.readByte () / 100.0 * 12.0);
+
+ // Range is [-99..99] but totally unclear to what that relates in dB.
+ // Let's keep it between [0..6]dB
+ zone.setGain ((Math.clamp (in.readByte (), -99, 99) / 99.0 + 1) / 3.0);
// Panorama - unused in KMP itself, 64 is center
in.read ();
@@ -215,7 +226,8 @@ private void readParameterChunk1 (final DataInputStream in) throws IOException,
// Filter Cutoff - unused in KMP itself
in.readByte ();
- final String sampleFilename = new String (in.readNBytes (12));
+ final byte [] nBytes = in.readNBytes (12);
+ final String sampleFilename = new String (nBytes);
if (SAMPLE_SKIPPED.equals (sampleFilename))
this.notifier.logError ("IDS_KMP_ERR_SKIPPED_SAMPLE");
@@ -237,7 +249,7 @@ else if (sampleFilename.startsWith (SAMPLE_INTERNAL))
private void readParameterChunk2 (final DataInputStream in) throws IOException
{
- for (int i = 0; i < this.group.getSampleZones ().size (); i++)
+ for (int i = 0; i < this.zones.size (); i++)
{
// Transpose
in.readByte ();
@@ -274,33 +286,36 @@ private static void assertSize (final String chunk, final int dataSize, final in
/**
- * Write a KMP file.
+ * Write a KMP file. Names will get -L/-R appended for LEFT/RIGHT. KMP Index needs to be 0 for
+ * left and 1 for right (index is also on the 3rd number position of KSF name: MS001000.KSF).
*
* @param folder The folder of the KMP file
* @param outputStream Where to write the file to
+ * @param kmpIndex The index of the KMP
+ * @param kmpChannel The channel to write
* @throws IOException Could not read the file
*/
- public void write (final File folder, final OutputStream outputStream) throws IOException
+ public void write (final File folder, final OutputStream outputStream, final int kmpIndex, final KMPChannel kmpChannel) throws IOException
{
final DataOutputStream out = new DataOutputStream (outputStream);
- this.writeMultisampleChunk (out);
- writeNumberChunk (out);
- this.writeParameterChunk1 (out);
+ this.writeMultisampleChunk (out, kmpChannel);
+ writeNumberChunk (out, kmpIndex);
+ this.writeParameterChunk1 (out, kmpIndex);
this.writeParameterChunk2 (out);
- this.writeNameChunk (out);
+ this.writeNameChunk (out, kmpChannel);
this.writeParameterChunk3 (out);
- this.writeKSFZones (folder);
+ this.writeKSFZones (folder, kmpIndex, kmpChannel);
}
- private void writeMultisampleChunk (final DataOutputStream out) throws IOException
+ private void writeMultisampleChunk (final DataOutputStream out, final KMPChannel kmpChannel) throws IOException
{
out.write (KMP_MSP_ID.getBytes ());
out.writeInt (KMP_MSP_SIZE);
- out.write (StringUtils.rightPadSpaces (StringUtils.fixASCII (this.nameLong), 16).getBytes ());
+ out.write (this.createName (16, kmpChannel).getBytes ());
out.write (this.numSamples);
// useSecondStart (1 = not use) not sure what that is about
@@ -308,33 +323,32 @@ private void writeMultisampleChunk (final DataOutputStream out) throws IOExcepti
}
- private void writeNameChunk (final DataOutputStream out) throws IOException
+ private void writeNameChunk (final DataOutputStream out, final KMPChannel kmpChannel) throws IOException
{
out.write (KMP_NAME_ID.getBytes ());
out.writeInt (KMP_NAME_SIZE);
- out.write (StringUtils.rightPadSpaces (StringUtils.fixASCII (this.nameLong), 24).getBytes ());
+ out.write (this.createName (24, kmpChannel).getBytes ());
}
- private static void writeNumberChunk (final DataOutputStream out) throws IOException
+ private static void writeNumberChunk (final DataOutputStream out, final int kmpIndex) throws IOException
{
out.write (KMP_NUMBER_ID.getBytes ());
out.writeInt (KMP_NUMBER_SIZE);
// The sample number in the bank
- out.writeInt (0);
+ out.writeInt (kmpIndex);
}
- private void writeParameterChunk1 (final DataOutputStream out) throws IOException
+ private void writeParameterChunk1 (final DataOutputStream out, final int kmpIndex) throws IOException
{
out.write (KMP_REL1_ID.getBytes ());
out.writeInt (this.numSamples * KMP_REL1_SIZE);
- final List zones = this.group.getSampleZones ();
- for (int i = 0; i < zones.size (); i++)
+ for (int i = 0; i < this.zones.size (); i++)
{
- final ISampleZone zone = zones.get (i);
+ final ISampleZone zone = this.zones.get (i);
final int keyLow = AbstractCreator.limitToDefault (zone.getKeyHigh (), 0);
final int keyHigh = AbstractCreator.limitToDefault (zone.getKeyHigh (), 127);
@@ -348,7 +362,10 @@ private void writeParameterChunk1 (final DataOutputStream out) throws IOExceptio
out.write (keyHigh);
out.writeByte ((byte) Math.round (zone.getTune () * 100.0));
- out.writeByte ((byte) Math.clamp (Math.round (Math.clamp (zone.getGain (), -12, 12) / 12.0 * 100.0), -99, 99));
+
+ // Range is [-99..99] but totally unclear to what that relates in dB.
+ // Let's keep it between [0..6]dB
+ out.writeByte (this.maxVolume ? 99 : (byte) Math.clamp (Math.round (Math.clamp (zone.getGain (), 0, 6) / 3.0 - 1.0) * 99.0, 0, 99));
// Panorama - unused in KMP itself, 64 is center
out.write (64);
@@ -356,7 +373,7 @@ private void writeParameterChunk1 (final DataOutputStream out) throws IOExceptio
// Filter Cutoff - unused in KMP itself
out.writeByte (0);
- out.write (String.format ("MS%06d.KSF", Integer.valueOf (i)).getBytes ());
+ out.write (String.format ("MS%03d%03d.KSF", Integer.valueOf (kmpIndex), Integer.valueOf (i)).getBytes ());
}
}
@@ -403,16 +420,15 @@ private void writeParameterChunk3 (final DataOutputStream out) throws IOExceptio
}
- private void writeKSFZones (final File folder) throws IOException
+ private void writeKSFZones (final File folder, final int kmpIndex, final KMPChannel kmpChannel) throws IOException
{
- final List sampleMetadata = this.group.getSampleZones ();
- for (int i = 0; i < sampleMetadata.size (); i++)
+ for (int i = 0; i < this.zones.size (); i++)
{
- final ISampleZone zone = sampleMetadata.get (i);
- final String filename = String.format ("MS%06d.KSF", Integer.valueOf (i));
+ final ISampleZone zone = this.zones.get (i);
+ final String filename = String.format ("MS%03d%03d.KSF", Integer.valueOf (kmpIndex), Integer.valueOf (i));
try (final OutputStream out = new FileOutputStream (new File (folder, filename)))
{
- KSFFile.write (zone, i, out);
+ KSFFile.write (zone, i, out, this.gain12dB, kmpChannel);
this.notifier.log ("IDS_NOTIFY_PROGRESS");
if (i > 0 && i % 80 == 0)
this.notifier.log ("IDS_NOTIFY_LINE_FEED");
@@ -423,4 +439,28 @@ private void writeKSFZones (final File folder) throws IOException
}
}
}
+
+
+ private String createName (final int maxLength, final KMPChannel kmpChannel)
+ {
+ final String paddedName = StringUtils.rightPadSpaces (StringUtils.fixASCII (this.nameLong), maxLength);
+ switch (kmpChannel)
+ {
+ case LEFT:
+ return paddedName.substring (0, maxLength - 2) + "-L";
+
+ case RIGHT:
+ return paddedName.substring (0, maxLength - 2) + "-R";
+
+ default:
+ return paddedName;
+ }
+ }
+
+
+ private static List sortByKeyHigh (final List sampleZones)
+ {
+ Collections.sort (sampleZones, (o1, o2) -> Integer.compare (o1.getKeyHigh (), o2.getKeyHigh ()));
+ return sampleZones;
+ }
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KSFFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KSFFile.java
index c0e2a1b..40db025 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KSFFile.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KSFFile.java
@@ -38,10 +38,12 @@ public class KSFFile
{
8,
16
- }, 44100, false);
+ }, 48000, false);
/** ID for KSF Sample parameter chunk. */
private static final String KSF_SAMPLE_PARAM_ID = "SMP1";
+ /** ID for KSF Sample parameter 2 chunk. */
+ private static final String KSF_SAMPLE_PARAM_2_ID = "SMP2";
/** ID for KSF Sample data chunk. */
private static final String KSF_SAMPLE_DATA_ID = "SMD1";
/** ID for KSF Sample number chunk. */
@@ -115,6 +117,11 @@ public static void read (final InputStream inputStream, final ISampleZone zone)
zone.getLoops ().add (loop);
break;
+ case KSF_SAMPLE_PARAM_2_ID:
+ // M3 manual says that this is used instead of SMP1 but seems to be identical...
+ in.readNBytes (dataSize);
+ break;
+
case KSF_SAMPLE_DATA_ID:
if (dataSize < KSF_SAMPLE_DATA_SIZE)
throw new ParseException (Functions.getMessage ("IDS_KMP_WRONG_CHUNK_LENGTH", id, Integer.toString (dataSize), Integer.toString (KSF_SAMPLE_DATA_SIZE)));
@@ -123,12 +130,21 @@ public static void read (final InputStream inputStream, final ISampleZone zone)
sampleRate = in.readInt ();
- // Attributes byte combines several settings
+ // Attributes byte combines several settings:
+ // Bit 1 (LSB): 1 = +12 dB play-back; 0 = 0 dB play-back (if un-compressed)
+ // Bit 1-4: Compression ID, if compressed
+ // Bit 5 (0x10): 1 = compressed, 0 = not compressed
+ // Bit 6 (0x20): 1 = not use 2nd start, 0 = do use 2nd start
+ // Bit 7 (0x40): 1 = reverse, 0 = forward
+ // Bit 8 (0x80): 1 = loop off, 0 = loop on
final int attributes = in.read ();
+ if ((attributes & 1) > 0)
+ System.out.println ("Boosted!");
+
if ((attributes & 0x10) > 0)
throw new ParseException (Functions.getMessage ("IDS_KMP_COMPRESSED_DATA_NOT_SUPPORTED"));
// Not used: attributes & 0x20 = 1: Not Use 2nd Start 0: Use It
- zone.setReversed ((attributes & 0x40) == 1);
+ zone.setReversed ((attributes & 0x40) > 0);
if ((attributes & 0x80) > 0)
zone.getLoops ().clear ();
@@ -183,32 +199,37 @@ public static void read (final InputStream inputStream, final ISampleZone zone)
* @param sampleZone The zone which contains the sample to store in a KSF file
* @param sampleIndex The index of the sample
* @param outputStream Where to write the file to
+ * @param gain12dB Enables the +12dB option, if true
+ * @param kmpChannel The KMP channel to write
* @throws IOException Could not write the file
* @throws ParseException If source wave files are broken
* @throws CompressionNotSupportedException If source wave files are compressed
*/
- public static void write (final ISampleZone sampleZone, final int sampleIndex, final OutputStream outputStream) throws IOException, ParseException, CompressionNotSupportedException
+ public static void write (final ISampleZone sampleZone, final int sampleIndex, final OutputStream outputStream, final boolean gain12dB, final KMPChannel kmpChannel) throws IOException, ParseException, CompressionNotSupportedException
{
final DataOutputStream out = new DataOutputStream (outputStream);
out.write (KSF_SAMPLE_PARAM_ID.getBytes ());
out.writeInt (KSF_SAMPLE_PARAM_SIZE);
- final String name = sampleZone.getName ();
+ final String name = createSafeFilename (sampleZone.getName ());
out.write (pad (name, 16).getBytes ());
- out.writeInt (sampleZone.getStart ());
out.writeInt (sampleZone.getStart ());
final List loops = sampleZone.getLoops ();
if (loops.isEmpty ())
{
+ // 2nd start
+ out.writeInt (0);
out.writeInt (0);
out.writeInt (sampleZone.getStop ());
}
else
{
final ISampleLoop loop = loops.get (0);
+ // 2nd start - no idea, but identical to loop start
+ out.writeInt (loop.getStart ());
out.writeInt (loop.getStart ());
out.writeInt (loop.getEnd ());
}
@@ -225,18 +246,24 @@ public static void write (final ISampleZone sampleZone, final int sampleIndex, f
out.write (KSF_SAMPLE_DATA_ID.getBytes ());
- // Convert the file to be a 8 or 16 bit WAV file with a maximum of 44.1kHz
+ // Convert the file to be a 8 or 16 bit WAV file with a maximum of 48kHz
final WaveFile waveFile = AudioFileUtils.convertToWav (sampleZone.getSampleData (), DESTINATION_FORMAT);
final FormatChunk formatChunk = waveFile.getFormatChunk ();
final DataChunk dataChunk = waveFile.getDataChunk ();
final byte [] data = dataChunk.getData ();
final int numSamples = dataChunk.calculateLength (formatChunk);
- out.writeInt (KSF_SAMPLE_DATA_SIZE + data.length);
+ // Only write left or right channel
+ int dataLength = data.length;
+ final boolean isStereo = formatChunk.getNumberOfChannels () == 2;
+ if (isStereo)
+ dataLength = dataLength / 2;
+
+ out.writeInt (KSF_SAMPLE_DATA_SIZE + dataLength);
out.writeInt (formatChunk.getSampleRate ());
- // Attributes byte combines several settings
- int attributes = 0;
+ // Attributes byte combines several settings; 1 = boost +12dB
+ int attributes = gain12dB ? 1 : 0;
// Not used: attributes & 0x20 = 1: Not Use 2nd Start 0: Use It
attributes |= 0x20;
if (sampleZone.isReversed ())
@@ -248,7 +275,8 @@ public static void write (final ISampleZone sampleZone, final int sampleIndex, f
// loopTune (–99…+99 cents) not supported
out.writeByte (0);
- out.write (formatChunk.getNumberOfChannels ());
+ // Number of channels is always 1
+ out.write (1);
// 8/16
final int bits = formatChunk.getSignificantBitsPerSample ();
if (bits != 8 && bits != 16)
@@ -258,14 +286,27 @@ public static void write (final ISampleZone sampleZone, final int sampleIndex, f
out.writeInt (numSamples);
if (bits == 8)
- out.write (data);
+ {
+ if (kmpChannel == KMPChannel.MONO || !isStereo)
+ out.write (data);
+ else
+ {
+ final int start = kmpChannel == KMPChannel.RIGHT && isStereo ? 1 : 0;
+ for (int i = start; i < data.length; i += 2)
+ out.write (data[i]);
+ }
+ }
else
- // Flip bytes
- for (int i = 0; i < data.length; i += 2)
+ {
+ final int start = kmpChannel == KMPChannel.RIGHT && isStereo ? 2 : 0;
+ final int step = kmpChannel == KMPChannel.MONO || !isStereo ? 2 : 4;
+ for (int i = start; i < data.length; i += step)
{
+ // Flip bytes
out.write (data[i + 1]);
out.write (data[i]);
}
+ }
//////////////////////////////////////
// KSF_SAMPLE_NAME_ID
@@ -300,4 +341,11 @@ private static void flipBytes (final byte [] data)
data[i + 1] = temp;
}
}
+
+
+ private static String createSafeFilename (final String filename)
+ {
+ final String name = filename.replaceAll ("[\\\\/:*?\"<>|&\\.#]", "_").trim ();
+ return name.length () == 1 ? "NAME" + name : name;
+ }
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010DetectorTask.java b/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010DetectorTask.java
index dd31001..5bae982 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010DetectorTask.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010DetectorTask.java
@@ -164,7 +164,7 @@ private IMultisampleSource parseAggregatedMultisample (final File multiSampleFil
for (final Element sampleElement: sampleElements)
{
- final Optional optZone = createSampleZone (multisampleSource, sampleElement, basePath);
+ final Optional optZone = this.createSampleZone (multisampleSource, sampleElement, basePath);
if (optZone.isPresent ())
group.addSampleZone (optZone.get ());
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileDetectorTask.java b/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileDetectorTask.java
index bd3b34b..0533b24 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileDetectorTask.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileDetectorTask.java
@@ -137,7 +137,7 @@ protected List readFile (final File folder)
/**
* Detect metadata, order samples and finally create the multi-sample.
- *
+ *
* @param sampleFileType The sample file type
* @param folder The folder which contains the sample files
* @param sampleData The detected sample files
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileType.java b/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileType.java
index 1d66006..23ffd4f 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileType.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/samplefile/SampleFileType.java
@@ -14,7 +14,7 @@ public interface SampleFileType
{
/**
* Get the name to display for this type.
- *
+ *
* @return The name, e.g. "WAV"
*/
String getName ();
@@ -22,7 +22,7 @@ public interface SampleFileType
/**
* Get the endings to look for.
- *
+ *
* @return The endings, e.g. '.wav'
*/
String [] getFileEndings ();
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java
index 636b551..a66761d 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Detector.java
@@ -10,6 +10,12 @@
import de.mossgrabers.convertwithmoss.core.IMultisampleSource;
import de.mossgrabers.convertwithmoss.core.INotifier;
import de.mossgrabers.convertwithmoss.core.detector.AbstractDetectorWithMetadataPane;
+import de.mossgrabers.tools.ui.BasicConfig;
+import de.mossgrabers.tools.ui.panel.BoxPanel;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ScrollPane;
/**
@@ -19,6 +25,13 @@
*/
public class Sf2Detector extends AbstractDetectorWithMetadataPane
{
+ private static final String ADD_FILE_NAME_TAG = "AddFileName";
+ private static final String ADD_PROGRAM_NUMBER_TAG = "AddProgramNumber";
+
+ private CheckBox addFileName;
+ private CheckBox addProgramNumber;
+
+
/**
* Constructor.
*
@@ -34,6 +47,55 @@ public Sf2Detector (final INotifier notifier)
@Override
public void detect (final File folder, final Consumer consumer)
{
- this.startDetection (new Sf2DetectorTask (this.notifier, consumer, folder, this.metadataPane));
+ this.startDetection (new Sf2DetectorTask (this.notifier, consumer, folder, this.metadataPane, this.addFileName.isSelected (), this.addProgramNumber.isSelected ()));
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public Node getEditPane ()
+ {
+ final BoxPanel panel = new BoxPanel (Orientation.VERTICAL);
+
+ ////////////////////////////////////////////////////////////
+ // Naming
+
+ panel.createSeparator ("@IDS_SF2_NAMING");
+
+ this.addFileName = panel.createCheckBox ("@IDS_SF2_NAMING_ADD_FILE_NAME");
+ this.addProgramNumber = panel.createCheckBox ("@IDS_SF2_NAMING_ADD_PROGRAM_NUMBER");
+
+ ////////////////////////////////////////////////////////////
+ // Metadata
+
+ this.metadataPane.addTo (panel);
+ this.metadataPane.getSeparator ().getStyleClass ().add ("titled-separator-pane");
+
+ final ScrollPane scrollPane = new ScrollPane (panel.getPane ());
+ scrollPane.fitToWidthProperty ().set (true);
+ scrollPane.fitToHeightProperty ().set (true);
+ return scrollPane;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void saveSettings (final BasicConfig config)
+ {
+ this.metadataPane.saveSettings (config);
+
+ config.setBoolean (this.prefix + ADD_FILE_NAME_TAG, this.addFileName.isSelected ());
+ config.setBoolean (this.prefix + ADD_PROGRAM_NUMBER_TAG, this.addProgramNumber.isSelected ());
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void loadSettings (final BasicConfig config)
+ {
+ this.metadataPane.loadSettings (config);
+
+ this.addFileName.setSelected (config.getBoolean (this.prefix + ADD_FILE_NAME_TAG, false));
+ this.addProgramNumber.setSelected (config.getBoolean (this.prefix + ADD_PROGRAM_NUMBER_TAG, false));
}
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2DetectorTask.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2DetectorTask.java
index 3818878..68a1d61 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2DetectorTask.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2DetectorTask.java
@@ -51,6 +51,10 @@
*/
public class Sf2DetectorTask extends AbstractDetectorTask
{
+ private final boolean addFileName;
+ private final boolean addProgramNumber;
+
+
/**
* Constructor.
*
@@ -58,10 +62,15 @@ public class Sf2DetectorTask extends AbstractDetectorTask
* @param consumer The consumer that handles the detected multi-sample sources
* @param sourceFolder The top source folder for the detection
* @param metadataConfig Additional metadata configuration parameters
+ * @param addFileName If true, add the filename to all multi-sample names
+ * @param addProgramNumber If true, add the program number to all multi-sample names
*/
- public Sf2DetectorTask (final INotifier notifier, final Consumer consumer, final File sourceFolder, final IMetadataConfig metadataConfig)
+ public Sf2DetectorTask (final INotifier notifier, final Consumer consumer, final File sourceFolder, final IMetadataConfig metadataConfig, final boolean addFileName, final boolean addProgramNumber)
{
super (notifier, consumer, sourceFolder, metadataConfig, ".sf2");
+
+ this.addFileName = addFileName;
+ this.addProgramNumber = addProgramNumber;
}
@@ -107,6 +116,8 @@ private List parseSF2File (final File sourceFile, final Sf2F
// Little workaround for not set names...
if ("NewInstr".equals (presetName))
presetName = parts[0];
+ if (this.addFileName || this.addProgramNumber)
+ presetName = this.addPrefixes (presetName, preset.getProgramNumber (), FileUtils.getNameWithoutType (sourceFile));
final String mappingName = AudioFileUtils.subtractPaths (this.sourceFolder, sourceFile) + " : " + presetName;
final DefaultMultisampleSource source = new DefaultMultisampleSource (sourceFile, parts, presetName, mappingName);
@@ -161,6 +172,17 @@ private List parseSF2File (final File sourceFile, final Sf2F
}
+ private String addPrefixes (final String presetName, final int programNumber, final String sf2FileName)
+ {
+ final StringBuilder sb = new StringBuilder ();
+ if (this.addFileName)
+ sb.append (sf2FileName).append (" - ");
+ if (this.addProgramNumber)
+ sb.append (String.format ("%03d", Integer.valueOf (programNumber))).append (" - ");
+ return sb.append (presetName).toString ();
+ }
+
+
private static void parseModulators (final ISampleZone zone, final Sf2PresetZone sf2Zone, final Sf2InstrumentZone instrZone)
{
for (final Sf2Modulator sf2Modulator: getModulators (sf2Zone, instrZone, Sf2Modulator.MODULATOR_PITCH_BEND))
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java
index 0256dcc..4106218 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetector.java
@@ -10,6 +10,12 @@
import de.mossgrabers.convertwithmoss.core.IMultisampleSource;
import de.mossgrabers.convertwithmoss.core.INotifier;
import de.mossgrabers.convertwithmoss.core.detector.AbstractDetectorWithMetadataPane;
+import de.mossgrabers.tools.ui.BasicConfig;
+import de.mossgrabers.tools.ui.panel.BoxPanel;
+import javafx.geometry.Orientation;
+import javafx.scene.Node;
+import javafx.scene.control.CheckBox;
+import javafx.scene.control.ScrollPane;
/**
@@ -19,6 +25,11 @@
*/
public class SfzDetector extends AbstractDetectorWithMetadataPane
{
+ private static final String LOG_OPCODES = "LogSupportedOpcodes";
+
+ private CheckBox logUnsupportedOpcodes;
+
+
/**
* Constructor.
*
@@ -30,10 +41,56 @@ public SfzDetector (final INotifier notifier)
}
+ /** {@inheritDoc} */
+ @Override
+ public Node getEditPane ()
+ {
+ final BoxPanel panel = new BoxPanel (Orientation.VERTICAL);
+
+ ////////////////////////////////////////////////////////////
+ // Naming
+
+ panel.createSeparator ("@IDS_SFZ_OPTIONS");
+
+ this.logUnsupportedOpcodes = panel.createCheckBox ("@IDS_SFZ_LOG_UNSUPPORTED_OPCODES");
+
+ ////////////////////////////////////////////////////////////
+ // Metadata
+
+ this.metadataPane.addTo (panel);
+ this.metadataPane.getSeparator ().getStyleClass ().add ("titled-separator-pane");
+
+ final ScrollPane scrollPane = new ScrollPane (panel.getPane ());
+ scrollPane.fitToWidthProperty ().set (true);
+ scrollPane.fitToHeightProperty ().set (true);
+ return scrollPane;
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void saveSettings (final BasicConfig config)
+ {
+ this.metadataPane.saveSettings (config);
+
+ config.setBoolean (this.prefix + LOG_OPCODES, this.logUnsupportedOpcodes.isSelected ());
+ }
+
+
+ /** {@inheritDoc} */
+ @Override
+ public void loadSettings (final BasicConfig config)
+ {
+ this.metadataPane.loadSettings (config);
+
+ this.logUnsupportedOpcodes.setSelected (config.getBoolean (this.prefix + LOG_OPCODES, false));
+ }
+
+
/** {@inheritDoc} */
@Override
public void detect (final File folder, final Consumer consumer)
{
- this.startDetection (new SfzDetectorTask (this.notifier, consumer, folder, this.metadataPane));
+ this.startDetection (new SfzDetectorTask (this.notifier, consumer, folder, this.metadataPane, this.logUnsupportedOpcodes.isSelected ()));
}
}
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetectorTask.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetectorTask.java
index c7f98fc..86c4f0d 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetectorTask.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzDetectorTask.java
@@ -5,11 +5,13 @@
package de.mossgrabers.convertwithmoss.format.sfz;
import java.io.File;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -42,6 +44,7 @@
import de.mossgrabers.convertwithmoss.ui.IMetadataConfig;
import de.mossgrabers.tools.FileUtils;
import de.mossgrabers.tools.Pair;
+import de.mossgrabers.tools.ui.Functions;
/**
@@ -74,6 +77,7 @@ public class SfzDetectorTask extends AbstractDetectorTask
private Map regionAttributes = Collections.emptyMap ();
private final Set processedOpcodes = new HashSet<> ();
private final Set allOpcodes = new HashSet<> ();
+ private final boolean logUnsupportedOpcodes;
/**
@@ -83,10 +87,13 @@ public class SfzDetectorTask extends AbstractDetectorTask
* @param consumer The consumer that handles the detected multi-sample sources
* @param sourceFolder The top source folder for the detection
* @param metadata Additional metadata configuration parameters
+ * @param logUnsupportedOpcodes Logs unsupported SFZ opcodes which are contained in the file
*/
- public SfzDetectorTask (final INotifier notifier, final Consumer consumer, final File sourceFolder, final IMetadataConfig metadata)
+ public SfzDetectorTask (final INotifier notifier, final Consumer consumer, final File sourceFolder, final IMetadataConfig metadata, final boolean logUnsupportedOpcodes)
{
super (notifier, consumer, sourceFolder, metadata, ".sfz");
+
+ this.logUnsupportedOpcodes = logUnsupportedOpcodes;
}
@@ -99,7 +106,7 @@ protected List readFile (final File file)
try
{
- final String content = this.loadTextFile (file);
+ final String content = this.loadTextFileWithReferences (file);
this.clearAttributes ();
return this.parseMetadataFile (file, content);
}
@@ -111,6 +118,76 @@ protected List readFile (final File file)
}
+ /**
+ * Loads a text file in UTF-8 encoding. If UTF-8 fails a string is created anyway but with
+ * unspecified behavior. If the file contains #include statements the referenced file is loaded
+ * as well and included at the position.
+ *
+ * @param file The file to load
+ * @return The loaded text
+ * @throws IOException Could not load the file
+ */
+ private String loadTextFileWithReferences (final File file) throws IOException
+ {
+ final Map cachedContents = new HashMap<> ();
+ final Set processingFiles = new HashSet<> ();
+ return this.parseFile (file, cachedContents, processingFiles);
+ }
+
+
+ private String parseFile (final File file, final Map cachedContents, final Set processingFiles) throws IOException
+ {
+ final String absolutePath = file.getAbsolutePath ();
+
+ // Check if the content is already cached
+ if (cachedContents.containsKey (absolutePath))
+ return cachedContents.get (absolutePath);
+
+ // Check for endless loop
+ if (processingFiles.contains (absolutePath))
+ throw new IOException (Functions.getMessage ("IDS_SFZ_INCLUDE_LOOP_DETECTED", absolutePath));
+
+ // Mark the file as currently processing
+ processingFiles.add (absolutePath);
+
+ final StringBuilder content = new StringBuilder ();
+ final Iterator iterator = this.loadTextFile (file).lines ().iterator ();
+ while (iterator.hasNext ())
+ {
+ final String line = iterator.next ();
+ if (line.startsWith ("#include"))
+ {
+ final String includedFilePath = extractIncludedFilePath (line);
+ if (includedFilePath == null)
+ throw new IOException (Functions.getMessage ("IDS_SFZ_MALFORMED_INCLUDE", line));
+
+ // Recursively parse the included file
+ final File includedFile = new File (file.getParent (), includedFilePath);
+ content.append (this.parseFile (includedFile, cachedContents, processingFiles));
+ }
+ else
+ content.append (line).append ('\n');
+ }
+
+ // Cache the content of the current file in case it is included multiple times...
+ cachedContents.put (absolutePath, content.toString ());
+
+ // Mark the file as processed
+ processingFiles.remove (absolutePath);
+ return content.toString ();
+ }
+
+
+ private static String extractIncludedFilePath (final String line)
+ {
+ final int start = line.indexOf ('"');
+ final int end = line.lastIndexOf ('"');
+ if (start < 0 || end < 0 || start == end)
+ return null;
+ return line.substring (start + 1, end);
+ }
+
+
private void clearAttributes ()
{
this.globalAttributes = Collections.emptyMap ();
@@ -133,13 +210,21 @@ private List parseMetadataFile (final File multiSampleFile,
{
final List>> result = parseSfz (content);
if (result.isEmpty ())
+ {
+ this.notifier.logError ("IDS_ERR_COULD_NOT_DETECT_MULTI_SAMPLE");
return Collections.emptyList ();
+ }
String name = FileUtils.getNameWithoutType (multiSampleFile);
final String n = this.metadataConfig.isPreferFolderName () ? this.sourceFolder.getName () : name;
final String [] parts = AudioFileUtils.createPathParts (multiSampleFile.getParentFile (), this.sourceFolder, n);
final List groups = this.parseGroups (multiSampleFile.getParentFile (), result);
+ if (groups.isEmpty ())
+ {
+ this.notifier.logError ("IDS_ERR_COULD_NOT_DETECT_MULTI_SAMPLE");
+ return Collections.emptyList ();
+ }
final Optional globalName = this.getAttribute (SfzOpcode.GLOBAL_LABEL);
if (globalName.isPresent ())
@@ -234,6 +319,10 @@ private List parseGroups (final File basePath, final List parseGroups (final File basePath, final List parseFile (final InputStream in, final File fil
parameters.put (param.name, param);
}
- readSampleMaps (in, file, multisampleSource, resources, parameters);
+ this.readSampleMaps (in, file, multisampleSource, resources, parameters);
multisampleSources.add (multisampleSource);
@@ -512,14 +512,14 @@ private IGroup parseSampleMap (final String sampleMap, final File parentFolder)
if (samplePath.length () > 2 && samplePath.charAt (1) == ':')
samplePath = samplePath.substring (2);
if (samplePath.length () > 0)
- createSampleZone (parentFolder, group, params, samplePath);
+ this.createSampleZone (parentFolder, group, params, samplePath);
}
return group;
}
- private void createSampleZone (final File parentFolder, final IGroup group, final String [] params, String samplePath) throws IOException
+ private void createSampleZone (final File parentFolder, final IGroup group, final String [] params, final String samplePath) throws IOException
{
final File sampleFile = new File (parentFolder, samplePath);
final ISampleData sampleData = this.createSampleData (sampleFile);
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/wav/WavFileSampleData.java b/src/main/java/de/mossgrabers/convertwithmoss/format/wav/WavFileSampleData.java
index aafa95c..9e4c2c2 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/wav/WavFileSampleData.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/wav/WavFileSampleData.java
@@ -201,11 +201,7 @@ private static void addLoops (final SampleChunk sampleChunk, final List multisampleSources) throws IOException
@@ -155,10 +152,7 @@ public void create (final File destinationFolder, final List
if (multisampleSources.isEmpty ())
return;
- final String combinationName = this.combinationFilename.getText ();
- final String name = multisampleSources.size () > 1 && combinationName.length () > 0 ? combinationName : multisampleSources.get (0).getName ();
- final String multiSampleName = createSafeFilename (name);
-
+ final String multiSampleName = this.getCombinationLibraryName (multisampleSources);
final OutputFormat selectedOutputFormat = this.getSelectedOutputFormat ();
final File multiFile = this.createUniqueFilename (destinationFolder, multiSampleName, ENDING_MAP.get (selectedOutputFormat));
this.notifier.log ("IDS_NOTIFY_STORING", multiFile.getAbsolutePath ());
@@ -169,14 +163,6 @@ public void create (final File destinationFolder, final List
}
- /** {@inheritDoc} */
- @Override
- public void create (final File destinationFolder, final IMultisampleSource multisampleSource) throws IOException
- {
- this.create (destinationFolder, Collections.singletonList (multisampleSource));
- }
-
-
/**
* Create all YSFC chunks for the given multi-samples and store the file.
*
@@ -335,14 +321,7 @@ private static YamahaYsfcKeybank createKeybank (final int sampleNumber, final IS
private OutputFormat getSelectedOutputFormat ()
{
- int selected = 1;
- final ObservableList toggles = this.outputFormatGroup.getToggles ();
- for (int i = 0; i < toggles.size (); i++)
- if (toggles.get (i).isSelected ())
- {
- selected = i;
- break;
- }
- return OutputFormat.values ()[selected];
+ final int selected = Functions.getSelectedToggleIndex (this.outputFormatGroup);
+ return OutputFormat.values ()[selected < 0 ? 1 : selected];
}
}
\ No newline at end of file
diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/yamaha/ysfc/YsfcFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/yamaha/ysfc/YsfcFile.java
index 8521803..65168a3 100644
--- a/src/main/java/de/mossgrabers/convertwithmoss/format/yamaha/ysfc/YsfcFile.java
+++ b/src/main/java/de/mossgrabers/convertwithmoss/format/yamaha/ysfc/YsfcFile.java
@@ -352,10 +352,10 @@ public void fillWaveChunks (final YamahaYsfcEntry keyBankEntry, final List
{
+ private static final int NUMBER_OF_DIRECTORIES = 20;
+
private static final String ENABLE_DARK_MODE = "EnableDarkMode";
private static final String DESTINATION_CREATE_FOLDER_STRUCTURE = "DestinationCreateFolderStructure";
private static final String DESTINATION_ADD_NEW_FILES = "DestinationAddNewFiles";
@@ -112,8 +115,8 @@ public class ConvertWithMossApp extends AbstractFrame implements INotifier, Cons
private BorderPane mainPane;
private BorderPane executePane;
- private final TextField sourcePathField = new TextField ();
- private final TextField destinationPathField = new TextField ();
+ private final ComboBox sourcePathField = new ComboBox<> ();
+ private final ComboBox destinationPathField = new ComboBox<> ();
private File sourceFolder;
private File outputFolder;
private Button convertButton;
@@ -127,6 +130,9 @@ public class ConvertWithMossApp extends AbstractFrame implements INotifier, Cons
private final TabPane sourceTabPane = new TabPane ();
private final TabPane destinationTabPane = new TabPane ();
+ private final List sourcePathHistory = new ArrayList<> ();
+ private final List destinationPathHistory = new ArrayList<> ();
+
private boolean onlyAnalyse = true;
private Button closeButton;
private Button cancelButton;
@@ -239,9 +245,12 @@ public void initialise (final Stage stage, final Optional baseTitleOptio
this.sourceFolderSelectButton.setTooltip (new Tooltip (Functions.getText ("@IDS_MAIN_SELECT_SOURCE_TOOLTIP")));
this.sourceFolderSelectButton.setOnAction (event -> {
+ final File currentSourcePath = new File (this.sourcePathField.getEditor ().getText ());
+ if (currentSourcePath.exists () && currentSourcePath.isDirectory ())
+ this.config.setActivePath (currentSourcePath);
final Optional file = Functions.getFolderFromUser (this.getStage (), this.config, "@IDS_MAIN_SELECT_SOURCE_HEADER");
if (file.isPresent ())
- this.sourcePathField.setText (file.get ().getAbsolutePath ());
+ this.sourcePathField.getEditor ().setText (file.get ().getAbsolutePath ());
});
final BoxPanel sourceUpperPart = new BoxPanel (Orientation.VERTICAL);
@@ -249,6 +258,7 @@ public void initialise (final Stage stage, final Optional baseTitleOptio
sourceTitle.setLabelFor (this.sourcePathField);
sourceUpperPart.addComponent (sourceTitle);
sourceUpperPart.addComponent (new BorderPane (this.sourcePathField, null, this.sourceFolderSelectButton, null, null));
+ this.sourcePathField.setMaxWidth (Double.MAX_VALUE);
this.sourceTabPane.getStyleClass ().add ("paddingLeftBottomRight");
final ObservableList tabs = this.sourceTabPane.getTabs ();
@@ -289,9 +299,12 @@ public void initialise (final Stage stage, final Optional baseTitleOptio
this.destinationFolderSelectButton.setTooltip (new Tooltip (Functions.getText ("@IDS_MAIN_SELECT_DESTINATION_TOOLTIP")));
this.destinationFolderSelectButton.setOnAction (event -> {
+ final File currentDestinationPath = new File (this.destinationPathField.getEditor ().getText ());
+ if (currentDestinationPath.exists () && currentDestinationPath.isDirectory ())
+ this.config.setActivePath (currentDestinationPath);
final Optional file = Functions.getFolderFromUser (this.getStage (), this.config, "@IDS_MAIN_SELECT_DESTINATION_HEADER");
if (file.isPresent ())
- this.destinationPathField.setText (file.get ().getAbsolutePath ());
+ this.destinationPathField.getEditor ().setText (file.get ().getAbsolutePath ());
});
destinationFolderPanel.setRight (this.destinationFolderSelectButton);
@@ -301,6 +314,7 @@ public void initialise (final Stage stage, final Optional baseTitleOptio
destinationHeader.setLabelFor (this.destinationPathField);
destinationUpperPart.addComponent (destinationHeader);
destinationUpperPart.addComponent (destinationFolderPanel);
+ this.destinationPathField.setMaxWidth (Double.MAX_VALUE);
this.destinationTabPane.getStyleClass ().add ("paddingLeftBottomRight");
final ObservableList destinationTabs = this.destinationTabPane.getTabs ();
@@ -356,7 +370,7 @@ public void initialise (final Stage stage, final Optional baseTitleOptio
final StackPane stackPane = new StackPane (this.mainPane, this.executePane);
this.setCenterNode (stackPane);
- this.loadConfig ();
+ this.loadConfiguration ();
this.updateTitle (null);
this.sourcePathField.requestFocus ();
@@ -422,15 +436,33 @@ private void setDarkMode (final boolean isSelected)
/**
* Load configuration settings.
*/
- private void loadConfig ()
+ private void loadConfiguration ()
{
- final String sourcePath = this.config.getProperty (SOURCE_PATH);
- if (sourcePath != null)
- this.sourcePathField.setText (sourcePath);
+ for (int i = 0; i < NUMBER_OF_DIRECTORIES; i++)
+ {
+ final String sourcePath = this.config.getProperty (SOURCE_PATH + i);
+ if (sourcePath == null || sourcePath.isBlank ())
+ break;
+ if (!this.sourcePathHistory.contains (sourcePath))
+ this.sourcePathHistory.add (sourcePath);
+ }
+ this.sourcePathField.getItems ().addAll (this.sourcePathHistory);
+ this.sourcePathField.setEditable (true);
+ if (!this.sourcePathHistory.isEmpty ())
+ this.sourcePathField.getEditor ().setText (this.sourcePathHistory.get (0));
- final String destinationPath = this.config.getProperty (DESTINATION_PATH);
- if (destinationPath != null)
- this.destinationPathField.setText (destinationPath);
+ for (int i = 0; i < NUMBER_OF_DIRECTORIES; i++)
+ {
+ final String destinationPath = this.config.getProperty (DESTINATION_PATH + i);
+ if (destinationPath == null || destinationPath.isBlank ())
+ break;
+ if (!this.destinationPathHistory.contains (destinationPath))
+ this.destinationPathHistory.add (destinationPath);
+ }
+ this.destinationPathField.getItems ().addAll (this.destinationPathHistory);
+ this.destinationPathField.setEditable (true);
+ if (!this.destinationPathHistory.isEmpty ())
+ this.destinationPathField.getEditor ().setText (this.destinationPathHistory.get (0));
final String renamingFilePath = this.config.getProperty (RENAMING_CSV_FILE);
if (renamingFilePath != null)
@@ -464,8 +496,24 @@ public void exit ()
for (final IDetector detector: this.detectors)
detector.shutdown ();
- this.config.setProperty (SOURCE_PATH, this.sourcePathField.getText ());
- this.config.setProperty (DESTINATION_PATH, this.destinationPathField.getText ());
+ this.saveConfiguration ();
+ Platform.exit ();
+ }
+
+
+ /**
+ * Save the configuration.
+ */
+ private void saveConfiguration ()
+ {
+ updateHistory (this.sourcePathField.getEditor ().getText (), this.sourcePathHistory);
+ for (int i = 0; i < NUMBER_OF_DIRECTORIES; i++)
+ this.config.setProperty (SOURCE_PATH + i, this.sourcePathHistory.size () > i ? this.sourcePathHistory.get (i) : "");
+
+ updateHistory (this.destinationPathField.getEditor ().getText (), this.destinationPathHistory);
+ for (int i = 0; i < NUMBER_OF_DIRECTORIES; i++)
+ this.config.setProperty (DESTINATION_PATH + i, this.destinationPathHistory.size () > i ? this.destinationPathHistory.get (i) : "");
+
this.config.setProperty (RENAMING_CSV_FILE, this.renameFilePathField.getText ());
this.config.setBoolean (DESTINATION_CREATE_FOLDER_STRUCTURE, this.createFolderStructure.isSelected ());
this.config.setBoolean (DESTINATION_ADD_NEW_FILES, this.addNewFiles.isSelected ());
@@ -484,8 +532,6 @@ public void exit ()
// Store configuration
super.exit ();
-
- Platform.exit ();
}
@@ -503,10 +549,7 @@ private void execute (final boolean onlyAnalyse)
return;
final int selectedDetector = this.sourceTabPane.getSelectionModel ().getSelectedIndex ();
- if (selectedDetector < 0 || !this.detectors[selectedDetector].checkSettings ())
- return;
-
- if (!this.detectors[selectedDetector].validateParameters ())
+ if (selectedDetector < 0 || !this.detectors[selectedDetector].checkSettings () || !this.detectors[selectedDetector].validateParameters ())
return;
this.loggingArea.clear ();
@@ -550,19 +593,20 @@ private void closeExecution ()
private boolean verifyFolders ()
{
// Check source folder
- this.sourceFolder = new File (this.sourcePathField.getText ());
+ this.sourceFolder = new File (this.sourcePathField.getEditor ().getText ());
if (!this.sourceFolder.exists () || !this.sourceFolder.isDirectory ())
{
Functions.message ("@IDS_NOTIFY_FOLDER_DOES_NOT_EXIST", this.sourceFolder.getAbsolutePath ());
this.sourcePathField.requestFocus ();
return false;
}
+ this.sourcePathHistory.add (0, this.sourceFolder.getAbsolutePath ());
if (this.onlyAnalyse)
return true;
// Check output folder
- this.outputFolder = new File (this.destinationPathField.getText ());
+ this.outputFolder = new File (this.destinationPathField.getEditor ().getText ());
if (!this.outputFolder.exists () && !this.outputFolder.mkdirs ())
{
Functions.message ("@IDS_NOTIFY_FOLDER_COULD_NOT_BE_CREATED", this.outputFolder.getAbsolutePath ());
@@ -575,6 +619,7 @@ private boolean verifyFolders ()
this.destinationPathField.requestFocus ();
return false;
}
+ this.destinationPathHistory.add (0, this.outputFolder.getAbsolutePath ());
// Output folder must be empty or add new must be active
if (!this.addNewFiles.isSelected ())
@@ -907,4 +952,11 @@ private static void rotateTabLabels (final Tab tab)
// Applying a negative Y transformation will move it left.
tabContainer.setTranslateY (-80);
}
+
+
+ private static void updateHistory (final String newItem, final List history)
+ {
+ history.remove (newItem);
+ history.add (0, newItem);
+ }
}
diff --git a/src/main/resources/Strings.properties b/src/main/resources/Strings.properties
index 1037f4c..e4b0e01 100644
--- a/src/main/resources/Strings.properties
+++ b/src/main/resources/Strings.properties
@@ -1,4 +1,4 @@
-TITLE=ConvertWithMoss 11.1.0
+TITLE=ConvertWithMoss 11.2.0
##################################################################################
#
@@ -7,7 +7,7 @@ TITLE=ConvertWithMoss 11.1.0
##################################################################################
IDS_NOTIFY_FOLDER_DOES_NOT_EXIST=The source folder does not exist: %1
-IDS_NOTIFY_FOLDER_COULD_NOT_BE_CREATED=Could not create output folder: %1
+IDS_NOTIFY_FOLDER_COULD_NOT_BE_CREATED=Could not create output folder: %1\n
IDS_NOTIFY_FOLDER_DESTINATION_NOT_A_FOLDER=The selected output path is not a folder: %1
IDS_NOTIFY_FOLDER_MUST_BE_EMPTY=The output folder is not empty. Please select an empty folder.
IDS_NOTIFY_ASCII_LENGTH_TOO_SHORT=Unexpected ASCII text length. Attempt to read %1 bytes but only got %2 bytes.\n
@@ -73,6 +73,7 @@ IDS_NOTIFY_ERR_BROKEN_INSTRUMENT_GENERATORS=Structurally unsound instrument gene
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
IDS_NOTIFY_ERR_MISSING_SAMPLE_GENERATOR=Missing sample generator.\n
+IDS_NOTIFY_ERR_MISSING_SAMPLE_DATA_CHUNK=Missing sample data. Could not detect sample chunk.\n
IDS_NOTIFY_ERR_DIFFERENT_NUMBER_LEFT_RIGHT=Different number of left and right mono samples: %1 %2\n
IDS_NOTIFY_ERR_DIFFERENT_SAMPLE_PITCH=Left and right samples do not have the same pitch: %1 %2\n
IDS_NOTIFY_ERR_DIFFERENT_SAMPLE_RATE=Left and right samples do not have the same sample rate: %1 %2\n
@@ -97,6 +98,8 @@ IDS_ERR_SOURCE_FORMAT_NOT_SUPPORTED=Source format not supported: %1\n
IDS_ERR_DATA_TOO_LARGE=Data is larger than 4GB and cannot be read.\n
IDS_ERR_NOT_AN_AIFF_FILE=The source file is not a proper AIFF file: %1\n
IDS_ERR_COMPRESSED_AIFF_FILE=Cannot convert AIFF file (%1) which is compressed with '%2 (%3)'.\n
+IDS_ERR_COULD_NOT_CREATE_ZONE=Could not create sample zone: %1\n
+IDS_ERR_COULD_NOT_DETECT_MULTI_SAMPLE=Could not detect a multi-sample in the file.\n
IDS_ADV_NOT_A_SAMPLER_PRESET=Not a sampler preset file.\n
@@ -108,7 +111,7 @@ IDS_DEX_SEPARATOR=Disting EX
IDS_DEX_RESAMPLE_TO_16_441=Re-sample to 16bit/44.1kHz
IDS_DEX_TRIM_START_TO_END=Trim sample to range of zone start to end.
-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_N_LAYERS=Round-robin keygroup can only contain up to %1 layers (Range: %2 - %3, Velocity: %4 - %5).\n
IDS_MPC_MORE_THAN_128_KEYGROUPS=More than 128 keygroups present (%1). This might cause issues when loading the program.\n
IDS_MPC_UNSUPPORTED_TYPE=Only Keygroups are supported but found type: %1\n
IDS_MPC_COULD_NOT_PARSE_ZONE_PLAY=Could not parse zone play parameter.\n
@@ -133,6 +136,7 @@ IDS_KMP_ERR_REFERENCED_KSF_NOT_SUPPORTED=Cross-referenced KSF files are currentl
IDS_KMP_ERR_DISTRIBUTED_KSF_NOT_SUPPORTED=Distributed KSF files are currently not supported. Please get in touch and send me the file for analysis.\n
IDS_KMP_COMPRESSED_DATA_NOT_SUPPORTED=Compressed sample data is currently not supported. Please get in touch and send me the file for analysis.\n
IDS_KMP_BIT_SIZE_NOT_SUPPORTED=KSF format only supports 8 or 16 bit samples but found %1.\n
+IDS_KMP_SOURCE_SAMPLES_FORMAT=Source samples are stored as: %1\n
IDS_NCW_NOT_A_NCW_FILE=This is not a NCW file.\n
IDS_NCW_UNKNOWN_VERSION=NCW File: Unknown version: %1\n
@@ -269,6 +273,10 @@ IDS_METADATA_PREFER_FOLDER=Prefer folder name
IDS_METADATA_DEFAULT_CREATOR=Default creator:
IDS_METADATA_CREATORS=Creator tags (if found in name or path):
+IDS_COMBINE_OF_SOURCES=Combine source multi-samples
+IDS_COMBINE_IN_ONE_LIBRARY=Combine all source multi-samples into one library
+IDS_COMBINE_LIBRARY_FILENAME=Library Filename:
+
IDS_OUTPUT_FORMAT=Output Format
IDS_SAMPLE_FILE_SEARCH=Sample File Search
IDS_DIRECTORY_SEARCH=Go this number of directories upwards to start sample file search:
@@ -296,15 +304,36 @@ IDS_DS_USER_INTERFACE=User Interface
IDS_DS_OUTPUT_FORMAT=Output Format
IDS_DS_PRESET=Preset (.dspreset)
IDS_DS_LIBRARY=Library (.dslibrary)
+IDS_DS_BUNDLE=Bundle (.dsbundle)
IDS_DS_MAKE_MONOPHONIC=Make monophonic
IDS_DS_ADD_REVERB=Add Reverb
+IDS_KMP_OPTIONS=KMP Options
+IDS_KMP_GAIN_12DB=Enable the +12dB option
+IDS_KMP_MAXIMIZE_VOLUME=Set sample volume to +99
+IDS_KMP_MAXIMIZE_VOLUME_TOOLTIP=Sets all sample volumes to +99. Use for low volume samples.
+IDS_KMP_WRITE_KMP_FOR_EACH_GROUP=Write group KMPs
+IDS_KMP_WRITE_KMP_FOR_EACH_GROUP_TOOLTIP=Writes a KMP for each group in the source multi-sample. This option will be ignored for split stereo source files.
+
+IDS_MPC_LAYER_LIMIT=Limit layers to
+IDS_MPC_LAYER_LIMIT_4=4
+IDS_MPC_LAYER_LIMIT_8=8 (requires MPC Firmware 3.4 or later)
+
IDS_NKI_KONTAKT_1=Kontakt 1
IDS_NKI_KONTAKT_2=Kontakt 2 to 4.1 (experimental)
IDS_QPAT_SEPARATOR=Waldorf QPAT
IDS_QPAT_RESAMPLE_TO_16_441=Re-sample to 16bit/44.1kHz
+IDS_SF2_NAMING=Naming of output files
+IDS_SF2_NAMING_ADD_FILE_NAME=Prefix with file name
+IDS_SF2_NAMING_ADD_PROGRAM_NUMBER=Prefix with program number
+
+IDS_SFZ_OPTIONS=Options
+IDS_SFZ_LOG_UNSUPPORTED_OPCODES=Log unsupported SFZ opcodes
+IDS_SFZ_MALFORMED_INCLUDE=Malformed #include statement: '%1'\n
+IDS_SFZ_INCLUDE_LOOP_DETECTED=Endless loop detected for #include: '%1'\n
+
IDS_WAV_CHUNK_TITLE=Add or update WAV chunks
IDS_WAV_WRITE_BEXT_CHUNK=Broadcast Audio Metadata (description, originator, date, time)
IDS_WAV_WRITE_INSTRUMENT_CHUNK=Instrument (unshifted note, fine tune, gain, note range, velocity range)
@@ -316,6 +345,3 @@ IDS_YSFC_OUTPUT_FORMAT_OPTION0=Montage User (*.X7U)
IDS_YSFC_OUTPUT_FORMAT_OPTION1=Montage Library (*.X7L)
IDS_YSFC_OUTPUT_FORMAT_OPTION2=MODX/MODX+ User (*.X8U)
IDS_YSFC_OUTPUT_FORMAT_OPTION3=MODX/MODX+ Library (*.X8L)
-IDS_YSFC_SOURCE_COMBINATION=Combine source multi-samples
-IDS_YSFC_MULTI_SAMPLE_COMBINATION=Combine all source multi-samples into one library
-IDS_YSFC_COMBINATION_FILENAME=Library Filename: