diff --git a/CHANGELOG.md b/CHANGELOG.md index d834854..804ca54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,14 @@ # Changes -## 10.0.1 (unreleased) +## 10.1.0 -* disting EX +* All formats + * Fixed: Increased the heap memory to 64GB to support larger source files. + * Fixed: WAV files in 32-bit float can now be converted to 16-bit PCM (workaround for bug in Java AudioSystem). +* 1010music format - Writing + * New: Added an option to trim samples with a delayed start. +* disting EX - Writing + * New: Added an option to trim samples with a delayed start. * Fixed: The MIDI note for the switch (SW) was off by 1 octave (disting assumes C3 as MIDI note 48 instead of 60). This caused playback issues. * Fixed: Release trigger groups are now removed from the output since the distingEX does not support release triggers. * SFZ diff --git a/README-FORMATS.md b/README-FORMATS.md index e42dc10..6994160 100644 --- a/README-FORMATS.md +++ b/README-FORMATS.md @@ -57,6 +57,7 @@ There are no metadata fields (category, creator, etc.) specified in the format. ### Destination Options * Option to set the *Interpolation Quality*. Setting it to *High* requires a bit more processing power on the 1010music devices. +* Option to trim sample to range of zone start to end. Since the format does not support a sample start attribute for multi-sample, this fixes the issue. * Options to write/update [WAV Chunk Information](#wav-chunk-information) ## AIFF @@ -155,6 +156,7 @@ The basic multi-sample setup is encoded in the file-names of the samples. Furthe ### Destination Options * 'Limit sample resolution and rate to 16bit/44.1kHz': If enabled samples of a high resolution will be resampled to 16bit and 44.1kHz. While the device can play higher resolutions as well it decrease the number of voices it can play. +* Option to trim sample to range of zone start to end. Since the format does not support a sample start attribute, this fixes the issue. * Options to write/update [WAV Chunk Information](#wav-chunk-information). Writing the Sample chunk is important since the disting EX reads the loop information from it. ## Korg KMP/KSF diff --git a/pom.xml b/pom.xml index e2139da..dc3892e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 de.mossgrabers convertwithmoss - 10.0.0 + 10.1.0 jar ConvertWithMoss 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 1b4cada..280e277 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java @@ -42,6 +42,8 @@ import de.mossgrabers.convertwithmoss.file.AudioFileUtils; import de.mossgrabers.convertwithmoss.file.riff.RiffID; import de.mossgrabers.convertwithmoss.file.wav.BroadcastAudioExtensionChunk; +import de.mossgrabers.convertwithmoss.file.wav.DataChunk; +import de.mossgrabers.convertwithmoss.file.wav.FormatChunk; import de.mossgrabers.convertwithmoss.file.wav.InstrumentChunk; import de.mossgrabers.convertwithmoss.file.wav.SampleChunk; import de.mossgrabers.convertwithmoss.file.wav.SampleChunk.SampleChunkLoop; @@ -450,7 +452,23 @@ protected List writeSamples (final File sampleFolder, final IMultisampleSo */ protected List writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final DestinationAudioFormat destinationFormat) throws IOException { - return this.writeSamples (sampleFolder, multisampleSource, ".wav", destinationFormat); + return this.writeSamples (sampleFolder, multisampleSource, ".wav", destinationFormat, false); + } + + + /** + * Writes all samples in WAV format from all groups into the given folder. + * + * @param sampleFolder The destination folder + * @param multisampleSource The multi-sample + * @param destinationFormat The destination audio format + * @param trim Trim the sample from zone start to end if enabled + * @return The written files + * @throws IOException Could not store the samples + */ + protected List writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final DestinationAudioFormat destinationFormat, final boolean trim) throws IOException + { + return this.writeSamples (sampleFolder, multisampleSource, ".wav", destinationFormat, trim); } @@ -465,7 +483,7 @@ protected List writeSamples (final File sampleFolder, final IMultisampleSo */ protected List writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final String fileEnding) throws IOException { - return this.writeSamples (sampleFolder, multisampleSource, fileEnding, DESTINATION_FORMAT); + return this.writeSamples (sampleFolder, multisampleSource, fileEnding, DESTINATION_FORMAT, false); } @@ -476,10 +494,11 @@ protected List writeSamples (final File sampleFolder, final IMultisampleSo * @param multisampleSource The multi-sample * @param fileEnding The suffix to use for the file * @param destinationFormat The destination audio format + * @param trim Trim the sample from zone start to end if enabled * @return The written files * @throws IOException Could not store the samples */ - protected List writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final String fileEnding, final DestinationAudioFormat destinationFormat) throws IOException + protected List writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final String fileEnding, final DestinationAudioFormat destinationFormat, final boolean trim) throws IOException { final List writtenFiles = new ArrayList<> (); @@ -499,8 +518,8 @@ protected List writeSamples (final File sampleFolder, final IMultisampleSo if (outputCount % 80 == 0) this.notifyNewline (); - if (this.requiresRewrite (destinationFormat)) - this.rewriteFile (multisampleSource.getMetadata (), zone, fos, destinationFormat); + if (this.requiresRewrite (destinationFormat) || trim) + this.rewriteFile (multisampleSource.getMetadata (), zone, fos, destinationFormat, trim); else { final ISampleData sampleData = zone.getSampleData (); @@ -612,25 +631,30 @@ protected static void recalculateSamplePositions (final IMultisampleSource multi * @param zone The zone from which to take the data to store into the chunks * @param outputStream Where to write the result * @param destinationFormat The destination audio format + * @param trim Trim the sample from zone start to end if enabled * @throws IOException Could not store the samples */ - private void rewriteFile (final IMetadata metadata, final ISampleZone zone, final OutputStream outputStream, final DestinationAudioFormat destinationFormat) throws IOException + private void rewriteFile (final IMetadata metadata, final ISampleZone zone, final OutputStream outputStream, final DestinationAudioFormat destinationFormat, final boolean trim) throws IOException { final ISampleData sampleData = zone.getSampleData (); if (sampleData == null) return; + + // Convert resolution final WaveFile wavFile = AudioFileUtils.convertToWav (sampleData, destinationFormat); + // Trim sample from zone start to end + if (trim) + trimStartToEnd (wavFile, zone); + + // Update information chunks if (this.isUpdateBroadcastAudioChunk ()) updateBroadcastAudioChunk (metadata, wavFile); - final int unityNote = MathUtils.clamp (zone.getKeyRoot (), 0, 127); if (this.isUpdateInstrumentChunk ()) updateInstrumentChunk (zone, wavFile, unityNote); - if (this.isUpdateSampleChunk ()) updateSampleChunk (zone, wavFile, unityNote); - if (this.isRemoveJunkChunks ()) wavFile.removeChunks (RiffID.JUNK_ID, RiffID.JUNK2_ID, RiffID.FILLER_ID, RiffID.MD5_ID); @@ -638,6 +662,47 @@ private void rewriteFile (final IMetadata metadata, final ISampleZone zone, fina } + /** + * Trims the data of the wave file to the part from the zone start and zone end. The zone is + * updated accordingly. + * + * @param wavFile The WAV file to trim + * @param zone The zone + */ + private static void trimStartToEnd (final WaveFile wavFile, final ISampleZone zone) + { + final FormatChunk formatChunk = wavFile.getFormatChunk (); + + // Create the truncated data array + final DataChunk dataChunk = wavFile.getDataChunk (); + final byte [] data = dataChunk.getData (); + final int start = zone.getStart (); + final int stop = zone.getStop (); + final int lengthInSamples = stop - start; + final int numBytesPerSample = formatChunk.calculateBytesPerSample (); + final int startByte = start * numBytesPerSample; + final int newLength = lengthInSamples * numBytesPerSample; + final byte [] truncatedData = new byte [newLength]; + System.arraycopy (data, startByte, truncatedData, 0, Math.min (newLength, data.length - startByte)); + + // Replace the previous data chunk + final DataChunk truncatedDataChunk = new DataChunk (formatChunk, lengthInSamples); + truncatedDataChunk.setData (truncatedData); + wavFile.setDataChunk (truncatedDataChunk); + + // Update the zone values - necessary for follow-up instrument/sample chunks! + zone.setStart (0); + zone.setStop (lengthInSamples); + final List loops = zone.getLoops (); + if (!loops.isEmpty ()) + { + final ISampleLoop loop = loops.get (0); + loop.setStart (Math.max (loop.getStart () - start, 0)); + loop.setEnd (Math.min (loop.getEnd () - start, lengthInSamples)); + } + } + + private static void updateSampleChunk (final ISampleZone zone, final WaveFile wavFile, final int unityNote) { final List loops = zone.getLoops (); diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java b/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java index c16fee3..8b99bd3 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.nio.file.Files; import java.util.ArrayList; import java.util.List; @@ -252,7 +254,13 @@ public static WaveFile convertToWav (final ISampleData sampleData, final Destina */ private static byte [] convertToWav (final byte [] inputData, final DestinationAudioFormat destinationFormat) throws IOException { - try (final AudioInputStream audioInputStream = AudioSystem.getAudioInputStream (new ByteArrayInputStream (inputData))) + return convertToWav (new ByteArrayInputStream (inputData), destinationFormat); + } + + + private static byte [] convertToWav (final InputStream inputStream, final DestinationAudioFormat destinationFormat) throws IOException + { + try (final AudioInputStream audioInputStream = AudioSystem.getAudioInputStream (inputStream)) { final AudioFormat audioFormat = audioInputStream.getFormat (); final int bitResolution = getMatchingBitResolution (audioFormat.getSampleSizeInBits (), destinationFormat.getBitResolutions ()); @@ -262,21 +270,20 @@ public static WaveFile convertToWav (final ISampleData sampleData, final Destina if (maxSampleRate != -1 && (sampleRate > maxSampleRate || destinationFormat.isUpSample ())) sampleRate = maxSampleRate; - final AudioFormat newAudioFormat = new AudioFormat (sampleRate, bitResolution, audioFormat.getChannels (), audioFormat.getEncoding () == Encoding.PCM_SIGNED, audioFormat.isBigEndian ()); - File tempFile = null; - try (final AudioInputStream convertedAudioInputStream = AudioSystem.getAudioInputStream (newAudioFormat, audioInputStream)) - { - // Cannot write to a stream since the length is not known and therefore the WAV - // header cannot be written and write method crashes - tempFile = File.createTempFile ("wav", "tmp"); - AudioSystem.write (convertedAudioInputStream, AudioFileFormat.Type.WAVE, tempFile); - return Files.readAllBytes (tempFile.toPath ()); - } - finally + final Encoding encoding = audioFormat.getEncoding (); + final boolean is32BitFloat = encoding == Encoding.PCM_FLOAT && audioFormat.getSampleSizeInBits () == 32; + final AudioFormat newAudioFormat = new AudioFormat (sampleRate, is32BitFloat ? 16 : bitResolution, audioFormat.getChannels (), encoding == Encoding.PCM_SIGNED || is32BitFloat, audioFormat.isBigEndian ()); + + // AudioSystem handles 32bit float values incorrect. We need our own implementation. + if (is32BitFloat) { - if (tempFile != null) - tempFile.delete (); + try (AudioInputStream convertedAudioInputStream = convertAudioStreamFrom32BitFloatTo16BitPCM (audioInputStream, audioFormat, newAudioFormat)) + { + return doConvertToWav (convertedAudioInputStream, newAudioFormat); + } } + + return doConvertToWav (audioInputStream, newAudioFormat); } catch (final UnsupportedAudioFileException ex) { @@ -285,6 +292,46 @@ public static WaveFile convertToWav (final ISampleData sampleData, final Destina } + private static AudioInputStream convertAudioStreamFrom32BitFloatTo16BitPCM (final AudioInputStream inputStream, final AudioFormat sourceAudioFormat, final AudioFormat destinationAudioFormat) throws IOException + { + if (destinationAudioFormat.getSampleSizeInBits () != 16) + throw new IOException (Functions.getMessage ("IDS_WAV_ONLY_16_BIT_SUPPORTED", Integer.toString (destinationAudioFormat.getSampleSizeInBits ()))); + + final byte [] sourceData = inputStream.readAllBytes (); + final ByteBuffer inputBuffer = ByteBuffer.wrap (sourceData).order (sourceAudioFormat.isBigEndian () ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + final ByteBuffer outputBuffer = ByteBuffer.allocate (sourceData.length / 2).order (destinationAudioFormat.isBigEndian () ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); + + for (int i = 0; i < sourceData.length; i += 4) + { + float floatValue = inputBuffer.getFloat (i); + + // Convert float to 16-bit PCM + outputBuffer.putShort ((short) (floatValue * Short.MAX_VALUE)); + } + + return new AudioInputStream (new ByteArrayInputStream (outputBuffer.array ()), destinationAudioFormat, inputStream.getFrameLength ()); + } + + + private static byte [] doConvertToWav (final AudioInputStream audioInputStream, final AudioFormat newAudioFormat) throws IOException + { + File tempFile = null; + try (final AudioInputStream convertedAudioInputStream = AudioSystem.getAudioInputStream (newAudioFormat, audioInputStream)) + { + // Cannot write to a stream since the length is not known and therefore the WAV + // header cannot be written and write method crashes + tempFile = File.createTempFile ("wav", "tmp"); + AudioSystem.write (convertedAudioInputStream, AudioFileFormat.Type.WAVE, tempFile); + return Files.readAllBytes (tempFile.toPath ()); + } + finally + { + if (tempFile != null) + tempFile.delete (); + } + } + + /** * De-compresses the input file and writes audio data in WAV format to the given output stream. * diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/wav/DataChunk.java b/src/main/java/de/mossgrabers/convertwithmoss/file/wav/DataChunk.java index 4a38c59..11843db 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/wav/DataChunk.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/wav/DataChunk.java @@ -25,7 +25,7 @@ public class DataChunk extends RIFFChunk */ public DataChunk (final FormatChunk formatChunk, final int lengthInSamples) { - super (RiffID.DATA_ID, new byte [calculateDataSize (formatChunk, lengthInSamples)], calculateDataSize (formatChunk, lengthInSamples)); + super (RiffID.DATA_ID, new byte [formatChunk.calculateDataSize (lengthInSamples)], formatChunk.calculateDataSize (lengthInSamples)); } @@ -54,48 +54,20 @@ public int calculateLength (final FormatChunk formatChunk) throws CompressionNot final int compressionCode = formatChunk.getCompressionCode (); if (compressionCode == FormatChunk.WAVE_FORMAT_PCM || compressionCode == FormatChunk.WAVE_FORMAT_IEEE_FLOAT) - return calculateLength (formatChunk, this.getData ()); + return formatChunk.calculateLength (this.getData ()); if (compressionCode == FormatChunk.WAVE_FORMAT_EXTENSIBLE) { final int numberOfChannels = formatChunk.getNumberOfChannels (); if (numberOfChannels > 2) throw new CompressionNotSupportedException ("WAV files in Extensible format are only supported for stereo files."); - return calculateLength (formatChunk, this.getData ()); + return formatChunk.calculateLength (this.getData ()); } throw new CompressionNotSupportedException ("Unsupported data compression: " + FormatChunk.getCompression (compressionCode)); } - /** - * Calculates the length of the data in samples. - * - * @param chunk The format chunk, necessary for the calculation (sample size and number of - * channels - * @param data The data - * @return The length of the sample in samples (frames) of 1 channel - */ - private static int calculateLength (final FormatChunk chunk, final byte [] data) - { - return data.length / (chunk.getNumberOfChannels () * chunk.getSignicantBitsPerSample () / 8); - } - - - /** - * Calculates the data size. - * - * @param chunk The format chunk, necessary for the calculation (sample size and number of - * channels - * @param lengthInSamples The length of the sample (number of samples) - * @return The size of the data block - */ - private static int calculateDataSize (final FormatChunk chunk, final int lengthInSamples) - { - return lengthInSamples * (chunk.getNumberOfChannels () * chunk.getSignicantBitsPerSample () / 8); - } - - /** {@inheritDoc} */ @Override public String infoText () diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/wav/FormatChunk.java b/src/main/java/de/mossgrabers/convertwithmoss/file/wav/FormatChunk.java index 05ef222..667f438 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/wav/FormatChunk.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/wav/FormatChunk.java @@ -101,7 +101,7 @@ public FormatChunk (final int numberOfChannels, final int sampleRate, final int this.setCompressionCode (WAVE_FORMAT_PCM); this.setNumberOfChannels (numberOfChannels); this.setSampleRate (sampleRate); - this.setSignicantBitsPerSample (bitsPerSample); + this.setSignificantBitsPerSample (bitsPerSample); } @@ -251,7 +251,7 @@ public int getBlockAlign () */ private void updateBlockAlign () { - final int bitsPerSample = this.getSignicantBitsPerSample (); + final int bitsPerSample = this.getSignificantBitsPerSample (); final int numberOfChannels = this.getNumberOfChannels (); final int blockAlign = bitsPerSample / 8 * numberOfChannels; this.setIntAsTwoBytes (0x0C, blockAlign); @@ -269,7 +269,7 @@ private void updateBlockAlign () * * @return The four bytes converted to an integer */ - public int getSignicantBitsPerSample () + public int getSignificantBitsPerSample () { if (0x0E < this.getData ().length) return this.getTwoBytesAsInt (0x0E); @@ -286,7 +286,7 @@ public int getSignicantBitsPerSample () * * @param bitsPerSample The bits per sample */ - public void setSignicantBitsPerSample (final int bitsPerSample) + public void setSignificantBitsPerSample (final int bitsPerSample) { this.setIntAsTwoBytes (0x0E, bitsPerSample); @@ -294,6 +294,42 @@ public void setSignicantBitsPerSample (final int bitsPerSample) } + /** + * Calculates the length of the data in samples. + * + * @param data The data + * @return The length of the sample in samples (frames) of 1 channel + */ + public int calculateLength (final byte [] data) + { + return data.length / calculateBytesPerSample (); + } + + + /** + * Calculates the data size. + * + * @param lengthInSamples The length of the sample (number of samples) + * @return The size of the data block + */ + public int calculateDataSize (final int lengthInSamples) + { + return lengthInSamples * calculateBytesPerSample (); + } + + + /** + * Calculate the number of bytes which are used for one sample depending on the significant bite + * per sample and the number of channels. + * + * @return The number of bytes + */ + public int calculateBytesPerSample () + { + return this.getNumberOfChannels () * this.getSignificantBitsPerSample () / 8; + } + + /** {@inheritDoc} */ @Override public String infoText () @@ -304,7 +340,7 @@ public String infoText () sb.append ("Sample Rate: ").append (this.getSampleRate ()).append ('\n'); sb.append ("Average bytes per second: ").append (this.getAverageBytesPerSecond ()).append ('\n'); sb.append ("Block align: ").append (this.getBlockAlign ()).append ('\n'); - sb.append ("Significant bits per sample: ").append (this.getSignicantBitsPerSample ()).append ('\n'); + sb.append ("Significant bits per sample: ").append (this.getSignificantBitsPerSample ()).append ('\n'); sb.append ("Extra bytes: ").append (this.getSize () - CHUNK_SIZE); return sb.toString (); } diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/wav/WaveFile.java b/src/main/java/de/mossgrabers/convertwithmoss/file/wav/WaveFile.java index 51c4b4e..39b1118 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/wav/WaveFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/wav/WaveFile.java @@ -130,11 +130,9 @@ public BroadcastAudioExtensionChunk getBroadcastAudioExtensionChunk () */ public void setBroadcastAudioExtensionChunk (final BroadcastAudioExtensionChunk broadcastAudioExtensionChunk) { - if (this.broadcastAudioExtensionChunk != null) - this.chunkStack.remove (this.broadcastAudioExtensionChunk); this.broadcastAudioExtensionChunk = broadcastAudioExtensionChunk; - if (this.broadcastAudioExtensionChunk != null) - this.chunkStack.add (this.broadcastAudioExtensionChunk); + this.chunkStack.clear (); + fillChunkStack (); } @@ -156,11 +154,9 @@ public InstrumentChunk getInstrumentChunk () */ public void setInstrumentChunk (final InstrumentChunk instrumentChunk) { - if (this.instrumentChunk != null) - this.chunkStack.remove (this.instrumentChunk); this.instrumentChunk = instrumentChunk; - if (this.instrumentChunk != null) - this.chunkStack.add (this.instrumentChunk); + this.chunkStack.clear (); + fillChunkStack (); } @@ -182,11 +178,9 @@ public SampleChunk getSampleChunk () */ public void setSampleChunk (final SampleChunk sampleChunk) { - if (this.sampleChunk != null) - this.chunkStack.remove (this.sampleChunk); this.sampleChunk = sampleChunk; - if (this.sampleChunk != null) - this.chunkStack.add (this.sampleChunk); + this.chunkStack.clear (); + fillChunkStack (); } @@ -201,6 +195,19 @@ public FormatChunk getFormatChunk () } + /** + * Get the data chunk. + * + * @param dataChunk The data chunk + */ + public void setDataChunk (final DataChunk dataChunk) + { + this.dataChunk = dataChunk; + this.chunkStack.clear (); + fillChunkStack (); + } + + /** * Get the data chunk if present in the WAV file. * @@ -240,7 +247,7 @@ public void combine (final WaveFile otherWave) throws CombinationNotPossibleExce final int length = Math.max (leftData.length, rightData.length); final byte [] combinedData = new byte [length * 2]; - final int blockSize = this.formatChunk.getSignicantBitsPerSample () / 8; + final int blockSize = this.formatChunk.getSignificantBitsPerSample () / 8; for (int count = 0; count < length; count += blockSize) { if (count < leftData.length) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/disting/DistingExCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/disting/DistingExCreator.java index 5c741ec..bff96d6 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/disting/DistingExCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/disting/DistingExCreator.java @@ -46,6 +46,7 @@ public class DistingExCreator extends WavCreator { private static final String DEX_LIMIT_TO_16_441 = "DistingLimitTo16441"; + private static final String DEX_TRIM_START_TO_END = "DistingTrimStartToEnd"; private static final DestinationAudioFormat OPTIMIZED_AUDIO_FORMAT = new DestinationAudioFormat (new int [] { 16 @@ -55,6 +56,7 @@ public class DistingExCreator extends WavCreator private final Map velocityLayerIndices = new HashMap<> (); private String filenamePrefix; private CheckBox limitTo16441; + private CheckBox trimStartToEnd; /** @@ -77,6 +79,7 @@ public Node getEditPane () panel.createSeparator ("@IDS_DEX_SEPARATOR"); this.limitTo16441 = panel.createCheckBox ("@IDS_DEX_LIMIT_TO_16_441"); + this.trimStartToEnd = panel.createCheckBox ("@IDS_DEX_TRIM_START_TO_END"); final TitledSeparator separator = this.addWavChunkOptions (panel); separator.getStyleClass ().add ("titled-separator-pane"); @@ -90,6 +93,7 @@ public Node getEditPane () public void loadSettings (final BasicConfig config) { this.limitTo16441.setSelected (config.getBoolean (DEX_LIMIT_TO_16_441, true)); + this.trimStartToEnd.setSelected (config.getBoolean (DEX_TRIM_START_TO_END, true)); this.loadWavChunkSettings (config, "Disting"); } @@ -100,6 +104,7 @@ public void loadSettings (final BasicConfig config) public void saveSettings (final BasicConfig config) { config.setBoolean (DEX_LIMIT_TO_16_441, this.limitTo16441.isSelected ()); + config.setBoolean (DEX_TRIM_START_TO_END, this.trimStartToEnd.isSelected ()); this.saveWavChunkSettings (config, "Disting"); } @@ -109,6 +114,8 @@ public void saveSettings (final BasicConfig config) @Override public void create (final File destinationFolder, final IMultisampleSource multisampleSource) throws IOException { + final boolean trim = this.trimStartToEnd.isSelected (); + this.prepareKeyAndVelocityRanges (multisampleSource); final String sampleName = createSafeFilename (multisampleSource.getName ()); @@ -121,6 +128,8 @@ public void create (final File destinationFolder, final IMultisampleSource multi this.notifier.logError ("IDS_NOTIFY_ALREADY_EXISTS", multiFile.getAbsolutePath ()); return; } + // Note: trim doesn't need to be used in the preset since the loop information is stored in + // the sample chunk! storeMultisample (multisampleSource, multiFile, safeSampleFolderName); this.notifier.log ("IDS_NOTIFY_STORING", safeSampleFolderName); @@ -132,7 +141,7 @@ public void create (final File destinationFolder, final IMultisampleSource multi final boolean doLimit = this.limitTo16441.isSelected (); if (doLimit) recalculateSamplePositions (multisampleSource, 44100); - this.writeSamples (sampleFolder, multisampleSource, doLimit ? OPTIMIZED_AUDIO_FORMAT : DEFEAULT_AUDIO_FORMAT); + this.writeSamples (sampleFolder, multisampleSource, doLimit ? OPTIMIZED_AUDIO_FORMAT : DEFEAULT_AUDIO_FORMAT, trim); this.notifier.log ("IDS_NOTIFY_PROGRESS_DONE"); } 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 1a2b6f8..c0e2a1b 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KSFFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/kmp/KSFFile.java @@ -250,7 +250,7 @@ public static void write (final ISampleZone sampleZone, final int sampleIndex, f out.write (formatChunk.getNumberOfChannels ()); // 8/16 - final int bits = formatChunk.getSignicantBitsPerSample (); + final int bits = formatChunk.getSignificantBitsPerSample (); if (bits != 8 && bits != 16) throw new IOException (Functions.getMessage ("IDS_KMP_BIT_SIZE_NOT_SUPPORTED", Integer.toString (bits))); out.write (bits); diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010Creator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010Creator.java index ccd59e6..7e57063 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010Creator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/music1010/Music1010Creator.java @@ -48,16 +48,13 @@ public class Music1010Creator extends AbstractCreator { private static final String MUSIC_1010_INTERPOLATION_QUALITY = "Music1010InterpolationQuality"; private static final String MUSIC_1010_RESAMPLE_TO_24_48 = "Music1010ResampleTo2448"; + private static final String MUSIC_1010_TRIM_START_TO_END = "Music1010TrimStartToEnd"; private static final DestinationAudioFormat OPTIMIZED_AUDIO_FORMAT = new DestinationAudioFormat (new int [] { 24 }, 48000, true); private static final DestinationAudioFormat DEFEAULT_AUDIO_FORMAT = new DestinationAudioFormat (); - private ToggleGroup interpolationQualityGroup; - private boolean isInterpolationQualityHigh; - private CheckBox resampleTo2448; - private static final Map EMPTY_PARAM_ATTRIBUTES = new HashMap<> (); private static final Map MULTISAMPLE_PARAM_ATTRIBUTES = new HashMap<> (); static @@ -155,6 +152,11 @@ public class Music1010Creator extends AbstractCreator MULTISAMPLE_PARAM_ATTRIBUTES.put ("recmonoutbus", "0"); } + private ToggleGroup interpolationQualityGroup; + private boolean isInterpolationQualityHigh; + private CheckBox resampleTo2448; + private CheckBox trimStartToEnd; + /** * Constructor. @@ -184,6 +186,7 @@ public Node getEditPane () order2.setToggleGroup (this.interpolationQualityGroup); this.resampleTo2448 = panel.createCheckBox ("@IDS_1010_MUSIC_CONVERT_TO_24_48"); + this.trimStartToEnd = panel.createCheckBox ("@IDS_1010_MUSIC_TRIM_START_TO_END"); final TitledSeparator separator = this.addWavChunkOptions (panel); separator.getStyleClass ().add ("titled-separator-pane"); @@ -198,6 +201,7 @@ public void loadSettings (final BasicConfig config) { this.interpolationQualityGroup.selectToggle (this.interpolationQualityGroup.getToggles ().get (config.getBoolean (MUSIC_1010_INTERPOLATION_QUALITY, false) ? 1 : 0)); this.resampleTo2448.setSelected (config.getBoolean (MUSIC_1010_RESAMPLE_TO_24_48, true)); + this.trimStartToEnd.setSelected (config.getBoolean (MUSIC_1010_TRIM_START_TO_END, true)); this.loadWavChunkSettings (config, "Music1010"); } @@ -209,6 +213,7 @@ public void saveSettings (final BasicConfig config) { config.setBoolean (MUSIC_1010_INTERPOLATION_QUALITY, this.isHighInterpolationQuality ()); config.setBoolean (MUSIC_1010_RESAMPLE_TO_24_48, this.resampleTo2448.isSelected ()); + config.setBoolean (MUSIC_1010_TRIM_START_TO_END, this.trimStartToEnd.isSelected ()); this.saveWavChunkSettings (config, "Music1010"); } @@ -218,6 +223,9 @@ public void saveSettings (final BasicConfig config) @Override public void create (final File destinationFolder, final IMultisampleSource multisampleSource) throws IOException { + final boolean resample = this.resampleTo2448.isSelected (); + final boolean trim = this.trimStartToEnd.isSelected (); + this.setInterpolationQuality (this.isHighInterpolationQuality ()); final String sampleName = createSafeFilename (multisampleSource.getName ()); @@ -232,13 +240,18 @@ public void create (final File destinationFolder, final IMultisampleSource multi return; } - final Optional metadata = this.createMetadata (sampleName, multisampleSource); + final Optional metadata = this.createMetadata (sampleName, multisampleSource, trim); if (metadata.isEmpty ()) return; this.notifier.log ("IDS_NOTIFY_STORING", multiFile.getAbsolutePath ()); - this.storePreset (presetFolder, multisampleSource, multiFile, metadata.get ()); + storePreset (presetFolder, multisampleSource, multiFile, metadata.get ()); + + // Store all samples + if (resample) + recalculateSamplePositions (multisampleSource, 48000); + this.writeSamples (presetFolder, multisampleSource, resample ? OPTIMIZED_AUDIO_FORMAT : DEFEAULT_AUDIO_FORMAT, trim); this.notifier.log ("IDS_NOTIFY_PROGRESS_DONE"); } @@ -253,18 +266,12 @@ public void create (final File destinationFolder, final IMultisampleSource multi * @param metadata The preset metadata description file * @throws IOException Could not store the file */ - private void storePreset (final File destinationFolder, final IMultisampleSource multisampleSource, final File multiFile, final String metadata) throws IOException + private static void storePreset (final File destinationFolder, final IMultisampleSource multisampleSource, final File multiFile, final String metadata) throws IOException { try (final FileWriter writer = new FileWriter (multiFile, StandardCharsets.UTF_8)) { writer.write (metadata); } - - // Store all samples - final boolean resample = this.resampleTo2448.isSelected (); - if (resample) - recalculateSamplePositions (multisampleSource, 48000); - this.writeSamples (destinationFolder, multisampleSource, resample ? OPTIMIZED_AUDIO_FORMAT : DEFEAULT_AUDIO_FORMAT); } @@ -273,9 +280,10 @@ private void storePreset (final File destinationFolder, final IMultisampleSource * * @param folderName The name to use for the sample folder * @param multisampleSource The multi-sample + * @param trim Trim to start/end if true * @return The XML structure */ - private Optional createMetadata (final String folderName, final IMultisampleSource multisampleSource) + private Optional createMetadata (final String folderName, final IMultisampleSource multisampleSource, final boolean trim) { final Optional optionalDocument = this.createXMLDocument (); if (optionalDocument.isEmpty ()) @@ -304,7 +312,7 @@ private Optional createMetadata (final String folderName, final IMultisa for (final IGroup group: groups) for (final ISampleZone zone: group.getSampleZones ()) { - createSample (document, folderName, presetPath, sessionElement, zone, sampleIndex); + createSample (document, folderName, presetPath, sessionElement, zone, sampleIndex, trim); sampleIndex++; } @@ -407,8 +415,9 @@ private Element createSlots (final Document document, final Element sessionEleme * @param groupElement The element where to add the sample information * @param zone Where to get the sample info from * @param sampleIndex The index of the sample + * @param trim Trim to start/end if true */ - private static void createSample (final Document document, final String folderName, final String presetPath, final Element groupElement, final ISampleZone zone, final int sampleIndex) + private static void createSample (final Document document, final String folderName, final String presetPath, final Element groupElement, final ISampleZone zone, final int sampleIndex, final boolean trim) { ///////////////////////////////////////////////////// // Sample element and attributes @@ -425,11 +434,15 @@ private static void createSample (final Document document, final String folderNa // Stored in WAV file: zone.getGain (), zone.getTune () - final int start = limitToDefault (zone.getStart (), 0); - XMLUtils.setIntegerAttribute (paramsElement, Music1010Tag.ATTR_SAMPLE_START, start); - final int stop = zone.getStop (); + // Music1010Tag.ATTR_SAMPLE_START is not supported for multi-samples! Therefore, the sample + // needs to be truncated instead! + int stop = zone.getStop (); if (stop > 0) - XMLUtils.setIntegerAttribute (paramsElement, Music1010Tag.ATTR_SAMPLE_LENGTH, start + stop); + { + if (trim) + stop -= zone.getStart (); + XMLUtils.setIntegerAttribute (paramsElement, Music1010Tag.ATTR_SAMPLE_LENGTH, stop); + } // No zone.getTrigger (); diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java index eab34d8..ba5e772 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sf2/Sf2Creator.java @@ -211,7 +211,7 @@ private static Sf2Preset createSf2Preset (final IMultisampleSource multisampleSo final byte [] data = dataChunk.getData (); - final boolean is24Bit = formatChunk.getSignicantBitsPerSample () == 24; + final boolean is24Bit = formatChunk.getSignificantBitsPerSample () == 24; final boolean isStereo = formatChunk.getNumberOfChannels () == 2; final List sampleDataList = convertData (data, numSamples, is24Bit, isStereo); diff --git a/src/main/resources/Strings.properties b/src/main/resources/Strings.properties index e21843a..e0cda26 100644 --- a/src/main/resources/Strings.properties +++ b/src/main/resources/Strings.properties @@ -1,4 +1,4 @@ -TITLE=ConvertWithMoss 10.0.1 +TITLE=ConvertWithMoss 10.1.0 ################################################################################## # @@ -99,6 +99,7 @@ IDS_DEX_NO_SD_PRESET=This preset is not for the SD-Algorithm.\n IDS_DEX_NO_SAMPLE_FOLDER=The sample folder does not exist: %1\n IDS_DEX_SEPARATOR=Disting EX IDS_DEX_LIMIT_TO_16_441=Limit sample resolution and rate 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_128_KEYGROUPS=More than 128 keygroups present (%1). This might cause issues when loading the program.\n @@ -210,6 +211,7 @@ IDS_WAV_ERR_IN_GROUP_PATTERN=Could not parse group pattern: %1\n IDS_WAV_ONLY_ONE_NOTE=All files have the same MIDI note.\n IDS_WAV_NO_MIDI_NOTE_DETECTED=Could not detect MIDI note in file name: %1\n IDS_WAV_COMBINATION_NOT_POSSIBLE=Cannot combine the two files.\n +IDS_WAV_ONLY_16_BIT_SUPPORTED=Can only convert 32-bit float to 16-bit PCM but destination bits are %1.\n IDS_YSFC_NO_MULTISAMPLE_DATA=The file does not contain a multi-sample.\n IDS_YSFC_NO_UKNOWN_CHUNK_MAGIC=Unknown chunk magic ID: %1\n @@ -269,6 +271,7 @@ IDS_1010_MUSIC_INTERPOLATION_QUALITY_NORMAL=Normal IDS_1010_MUSIC_INTERPOLATION_QUALITY_HIGH=High IDS_1010_MUSIC_CONVERT_TO_24_48=Re-sample to 24bit/48kHz IDS_1010_MUSIC_NO_MULTISAMPLE=No multi-sample found. Creating aggregated multi-sample.\n +IDS_1010_MUSIC_TRIM_START_TO_END=Trim sample to range of zone start to end. IDS_DS_USER_INTERFACE=User Interface IDS_DS_OUTPUT_FORMAT=Output Format