Skip to content

Commit

Permalink
* All formats
Browse files Browse the repository at this point in the history
  * 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.
  • Loading branch information
git-moss committed Jun 16, 2024
1 parent 08e628c commit ba0b7b2
Show file tree
Hide file tree
Showing 13 changed files with 259 additions and 99 deletions.
10 changes: 8 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions README-FORMATS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>de.mossgrabers</groupId>
<artifactId>convertwithmoss</artifactId>
<version>10.0.0</version>
<version>10.1.0</version>
<packaging>jar</packaging>
<name>ConvertWithMoss</name>
<organization>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -450,7 +452,23 @@ protected List<File> writeSamples (final File sampleFolder, final IMultisampleSo
*/
protected List<File> 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<File> writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final DestinationAudioFormat destinationFormat, final boolean trim) throws IOException
{
return this.writeSamples (sampleFolder, multisampleSource, ".wav", destinationFormat, trim);
}


Expand All @@ -465,7 +483,7 @@ protected List<File> writeSamples (final File sampleFolder, final IMultisampleSo
*/
protected List<File> 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);
}


Expand All @@ -476,10 +494,11 @@ protected List<File> 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<File> writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final String fileEnding, final DestinationAudioFormat destinationFormat) throws IOException
protected List<File> writeSamples (final File sampleFolder, final IMultisampleSource multisampleSource, final String fileEnding, final DestinationAudioFormat destinationFormat, final boolean trim) throws IOException
{
final List<File> writtenFiles = new ArrayList<> ();

Expand All @@ -499,8 +518,8 @@ protected List<File> 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 ();
Expand Down Expand Up @@ -612,32 +631,78 @@ 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);

wavFile.write (outputStream);
}


/**
* 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<ISampleLoop> 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<ISampleLoop> loops = zone.getLoops ();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 ());
Expand All @@ -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)
{
Expand All @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}


Expand Down Expand Up @@ -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 ()
Expand Down
Loading

0 comments on commit ba0b7b2

Please sign in to comment.