diff --git a/documentation/CHANGELOG.md b/documentation/CHANGELOG.md index a4ea6bce..52622ad8 100644 --- a/documentation/CHANGELOG.md +++ b/documentation/CHANGELOG.md @@ -26,6 +26,7 @@ * Fixed: Ignores hidden files/folders and the known Windows system folders when checking for empty-folder (thanks to Douglas Carmichael). * Fixed: A library name typed with its file ending (e.g. "MyLibrary.xrni") produced a doubled-up file name ("MyLibrary_xrni.xrni") - the ending is now recognized for every destination format (thanks to Douglas Carmichael). * Fixed: The "Trim start and end" processing option cut the audio to the zone's start/end but left the loop points at their old positions, so a trimmed sample with a non-zero start got a displaced loop - the loop end could even point past the end of the trimmed audio. The loop points now move with the cut (thanks to Douglas Carmichael). + * Fixed: Replaced the external FLAC encoder library with an own implementation: the library crashed on samples whose length modulo 4096 was 2, 3 or 4 (e.g. "The FLAC encoder failed for sample '...'" when writing Renoise files; SFZ with FLAC option, Bliss and Synclavier Regen were affected as well) (thanks to Douglas Carmichael). * Fixed: Fixed some potential NullPointerExceptions. * 1010music (thanks to Douglas Carmichael) * Fixed: The amplitude decay and release times were written with a different time scale than the one used when reading them back (25 seconds instead of 38 seconds full-scale), so a converted blackbox/Bento preset played its decay and release noticeably shorter than the source. The write scale now matches the read scale. 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 1ccce8e3..a8de6960 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/creator/AbstractCreator.java @@ -30,8 +30,6 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; -import javax.sound.sampled.AudioFileFormat; -import javax.sound.sampled.UnsupportedAudioFileException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; @@ -68,7 +66,6 @@ */ public abstract class AbstractCreator extends AbstractCoreTask implements ICreator { - protected static final AudioFileFormat.Type FLAC_TARGET_FORMAT = new AudioFileFormat.Type ("FLAC", "flac"); /** The post-fix to use for the samples folder. */ protected static final String FOLDER_POSTFIX = " Samples"; @@ -436,18 +433,16 @@ protected String createSampleFilename (final ISampleZone zone, final int zoneInd /** - * Writes all samples in the given audio file format from all groups into the given folder. + * Writes all samples in FLAC format from all groups into the given folder. * * @param sampleFolder The destination folder * @param multisampleSource The multi-sample * @return The written files * @throws IOException Could not store the samples - * @throws UnsupportedAudioFileException The audio format is not supported */ - protected List writeFlacSamples (final File sampleFolder, final IMultisampleSource multisampleSource) throws IOException, UnsupportedAudioFileException + protected List writeFlacSamples (final File sampleFolder, final IMultisampleSource multisampleSource) throws IOException { final List writtenFiles = new ArrayList<> (); - final String extension = "." + FLAC_TARGET_FORMAT.getExtension (); for (final IGroup group: multisampleSource.getGroups ()) { @@ -459,14 +454,14 @@ protected List writeFlacSamples (final File sampleFolder, final IMultisamp final ISampleZone zone = sampleZones.get (zoneIndex); - final File file = new File (sampleFolder, this.createSampleFilename (zone, zoneIndex, extension)); + final File file = new File (sampleFolder, this.createSampleFilename (zone, zoneIndex, ".flac")); this.progress.notifyProgress (); final Optional sampleData = zone.getSampleData (); if (sampleData.isEmpty ()) this.notifier.logError (IDS_NOTIFY_ERR_MISSING_SAMPLE_DATA, zone.getName (), file.getName ()); else { - AudioFileUtils.compressToFLAC (sampleData.get (), FLAC_TARGET_FORMAT, file); + AudioFileUtils.compressToFLAC (sampleData.get (), file); writtenFiles.add (file); } } diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java b/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java index 70d6da55..b71b129f 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/AudioFileUtils.java @@ -31,6 +31,7 @@ import de.mossgrabers.convertwithmoss.core.model.ISampleData; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultAudioMetadata; import de.mossgrabers.convertwithmoss.exception.ParseException; +import de.mossgrabers.convertwithmoss.file.flac.FlacEncoder; import de.mossgrabers.convertwithmoss.file.wav.FormatChunk; import de.mossgrabers.convertwithmoss.file.wav.WaveFile; import de.mossgrabers.tools.ui.Functions; @@ -43,7 +44,15 @@ */ public final class AudioFileUtils { - private static final String BROKEN_WAV = "IDS_NOTIFY_ERR_BROKEN_WAV"; + private static final String BROKEN_WAV = "IDS_NOTIFY_ERR_BROKEN_WAV"; + + /** FLAC supports at maximum a resolution of 24 bit. */ + private static final DestinationAudioFormat FLAC_COMPATIBLE_FORMAT = new DestinationAudioFormat (new int [] + { + 8, + 16, + 24 + }, -1, false); /** @@ -415,31 +424,66 @@ public static void decompressToWav (final InputStream inputStream, final OutputS /** - * Compresses the given sample data contained in the sampleData object into a specific file - * format (e.g. FLAC). + * Compresses the given sample data contained in the sampleData object into a FLAC file. Since + * FLAC supports at maximum a resolution of 24 bit, 32 bit samples are reduced (16 bit for + * 32-bit float). * * @param sampleData The sample data - * @param targetFormat The target format * @param file The file to write to * @throws IOException Could not read/write - * @throws UnsupportedAudioFileException The target audio format is not supported */ - public static void compressToFLAC (final ISampleData sampleData, final AudioFileFormat.Type targetFormat, final File file) throws IOException, UnsupportedAudioFileException + public static void compressToFLAC (final ISampleData sampleData, final File file) throws IOException { - final byte [] wavData = convertToWavData (sampleData, new DestinationAudioFormat ()); + Files.write (file.toPath (), compressToFLAC (sampleData)); + } - // Create a ByteArrayInputStream from the input data array - try (final ByteArrayInputStream bais = new ByteArrayInputStream (wavData); AudioInputStream ais = AudioSystem.getAudioInputStream (bais)) - { - final AudioFormat sourceFormat = ais.getFormat (); - final AudioFormat targetAudioFormat = new AudioFormat (sourceFormat.getSampleRate (), sourceFormat.getSampleSizeInBits (), sourceFormat.getChannels (), true, sourceFormat.isBigEndian ()); - try (final AudioInputStream convertedAIS = AudioSystem.getAudioInputStream (targetAudioFormat, ais)) + + /** + * Compresses the given sample data contained in the sampleData object into FLAC. Since FLAC + * supports at maximum a resolution of 24 bit, 32 bit samples are reduced (16 bit for 32-bit + * float). + * + * @param sampleData The sample data + * @return The FLAC data + * @throws IOException Could not read the sample data + */ + public static byte [] compressToFLAC (final ISampleData sampleData) throws IOException + { + final WaveFile waveFile = convertToWav (sampleData, FLAC_COMPATIBLE_FORMAT); + final FormatChunk formatChunk = waveFile.getFormatChunk (); + final int numberOfChannels = formatChunk.getNumberOfChannels (); + final int bitsPerSample = formatChunk.getSignificantBitsPerSample (); + final int bytesPerSample = bitsPerSample / 8; + final byte [] data = waveFile.getDataChunk ().getData (); + + // De-interleave the samples of the channels; 8 bit WAV data is unsigned and therefore + // converted to the signed form required by FLAC + final int numberOfSamples = data.length / (numberOfChannels * bytesPerSample); + final int [] [] channels = new int [numberOfChannels] [numberOfSamples]; + int position = 0; + for (int i = 0; i < numberOfSamples; i++) + for (int channel = 0; channel < numberOfChannels; channel++) { - // IMPORTANT: we must write to a file not a stream since with a stream the FLAC - // header is not updated! - AudioSystem.write (convertedAIS, targetFormat, file); + final int value; + switch (bytesPerSample) + { + case 1: + value = (data[position] & 0xFF) - 128; + break; + case 2: + value = data[position] & 0xFF | data[position + 1] << 8; + break; + case 3: + value = data[position] & 0xFF | (data[position + 1] & 0xFF) << 8 | data[position + 2] << 16; + break; + default: + throw new IOException ("FLAC: Unsupported bit resolution: " + bitsPerSample); + } + channels[channel][i] = value; + position += bytesPerSample; } - } + + return FlacEncoder.encode (channels, formatChunk.getSampleRate (), bitsPerSample); } diff --git a/src/main/java/de/mossgrabers/convertwithmoss/file/flac/FlacEncoder.java b/src/main/java/de/mossgrabers/convertwithmoss/file/flac/FlacEncoder.java new file mode 100644 index 00000000..ce991144 --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/file/flac/FlacEncoder.java @@ -0,0 +1,1032 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2026 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.file.flac; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + + +/** + * Encoder for the Free Lossless Audio Codec (FLAC) format as specified in RFC 9639. Writes a fixed + * block size stream using constant, verbatim, fixed prediction and linear prediction (LPC) + * sub-frames with Rice coded residuals; stereo blocks additionally choose the smallest of the + * independent and de-correlated (left/side, side/right, mid/side) channel codings. Only features + * of the original FLAC specification are emitted (4-bit Rice parameters, no escape codes), + * therefore the output is readable by old decoders as well. All block sizes are handled correctly, + * including a trailing block which is shorter than the prediction order. + * + * @author Jürgen Moßgraber + */ +public class FlacEncoder +{ + private static final int BLOCK_SIZE = 4096; + private static final int MAX_FIXED_ORDER = 4; + private static final int MAX_LPC_ORDER = 8; + private static final int LPC_PRECISION = 14; + private static final int MAX_RICE_PARAMETER = 14; + private static final int MAX_PARTITION_ORDER = 6; + private static final int PADDING_LENGTH = 40; + + private static final int [] CRC8_TABLE = new int [256]; + private static final int [] CRC16_TABLE = new int [256]; + + static + { + for (int i = 0; i < 256; i++) + { + int crc8 = i; + int crc16 = i << 8; + for (int bit = 0; bit < 8; bit++) + { + crc8 = (crc8 & 0x80) != 0 ? crc8 << 1 ^ 0x07 : crc8 << 1; + crc16 = (crc16 & 0x8000) != 0 ? crc16 << 1 ^ 0x8005 : crc16 << 1; + } + CRC8_TABLE[i] = crc8 & 0xFF; + CRC16_TABLE[i] = crc16 & 0xFFFF; + } + } + + + /** + * Private due to utility class. + */ + private FlacEncoder () + { + // Intentionally empty + } + + + /** + * Encodes the given audio data losslessly into a FLAC stream. + * + * @param channels The audio data, one array of signed samples per channel, all of the same + * length + * @param sampleRate The sample rate in Hertz + * @param bitsPerSample The resolution of the samples, may be 8, 16 or 24 + * @return The FLAC stream + * @throws IOException If the audio attributes cannot be represented in FLAC + */ + public static byte [] encode (final int [] [] channels, final int sampleRate, final int bitsPerSample) throws IOException + { + final int numberOfChannels = channels.length; + if (numberOfChannels < 1 || numberOfChannels > 8) + throw new IOException ("FLAC: Unsupported number of channels: " + numberOfChannels); + if (bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 24) + throw new IOException ("FLAC: Unsupported bit resolution: " + bitsPerSample); + if (sampleRate < 1 || sampleRate > 0xFFFFF) + throw new IOException ("FLAC: Unsupported sample rate: " + sampleRate); + final int numberOfSamples = channels[0].length; + for (final int [] channel: channels) + if (channel.length != numberOfSamples) + throw new IOException ("FLAC: All channels must have the same length."); + + final MessageDigest md5 = createMD5Digest (); + final ByteArrayOutputStream frames = new ByteArrayOutputStream (); + int minFrameSize = Integer.MAX_VALUE; + int maxFrameSize = 0; + int frameIndex = 0; + final byte [] interleaved = new byte [BLOCK_SIZE * numberOfChannels * (bitsPerSample / 8)]; + for (int offset = 0; offset < numberOfSamples; offset += BLOCK_SIZE) + { + final int blockSize = Math.min (BLOCK_SIZE, numberOfSamples - offset); + md5.update (interleaved, 0, interleaveLittleEndian (channels, offset, blockSize, bitsPerSample, interleaved)); + final byte [] frame = encodeFrame (channels, offset, blockSize, frameIndex, sampleRate, bitsPerSample); + frames.write (frame, 0, frame.length); + minFrameSize = Math.min (minFrameSize, frame.length); + maxFrameSize = Math.max (maxFrameSize, frame.length); + frameIndex++; + } + if (frameIndex == 0) + minFrameSize = 0; + + final BitWriter header = new BitWriter (); + header.writeBits (0x664C6143, 32); + // Meta-data block header: type 0 (STREAMINFO), length 34 + header.writeBits (0x00, 8); + header.writeBits (34, 24); + header.writeBits (BLOCK_SIZE, 16); + header.writeBits (BLOCK_SIZE, 16); + header.writeBits (minFrameSize, 24); + header.writeBits (maxFrameSize, 24); + header.writeBits (sampleRate, 20); + header.writeBits (numberOfChannels - 1, 3); + header.writeBits (bitsPerSample - 1, 5); + header.writeLongBits (numberOfSamples, 36); + for (final byte b: md5.digest ()) + header.writeBits (b & 0xFF, 8); + + // A PADDING block terminates the meta-data. The jFLAC based reader which is used to read + // FLAC files cannot handle a stream in which STREAMINFO is the only meta-data block! + header.writeBits (0x81, 8); + header.writeBits (PADDING_LENGTH, 24); + for (int i = 0; i < PADDING_LENGTH; i++) + header.writeBits (0, 8); + + final ByteArrayOutputStream result = new ByteArrayOutputStream (); + final byte [] headerBytes = header.toByteArray (); + result.write (headerBytes, 0, headerBytes.length); + frames.writeTo (result); + return result.toByteArray (); + } + + + /** + * Encodes one FLAC frame. + * + * @param channels The audio data of all channels + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @param frameIndex The index of the frame in the stream + * @param sampleRate The sample rate in Hertz + * @param bitsPerSample The resolution of the samples + * @return The encoded frame + */ + private static byte [] encodeFrame (final int [] [] channels, final int offset, final int blockSize, final int frameIndex, final int sampleRate, final int bitsPerSample) + { + // Encode the sub-frames of all channels first since the channel assignment (independent + // or de-correlated stereo) is chosen by the smallest encoded size and needs to be known + // for the frame header + int channelAssignment = channels.length - 1; + BitWriter [] subframes; + if (channels.length == 2) + { + final int [] [] stereo = calcStereoDecorrelation (channels, offset, blockSize); + final BitWriter left = encodeSubframeToWriter (channels[0], offset, blockSize, bitsPerSample); + final BitWriter right = encodeSubframeToWriter (channels[1], offset, blockSize, bitsPerSample); + final BitWriter mid = encodeSubframeToWriter (stereo[0], 0, blockSize, bitsPerSample); + final BitWriter side = encodeSubframeToWriter (stereo[1], 0, blockSize, bitsPerSample + 1); + + final long leftRight = (long) left.lengthInBits () + right.lengthInBits (); + final long leftSide = (long) left.lengthInBits () + side.lengthInBits (); + final long sideRight = (long) side.lengthInBits () + right.lengthInBits (); + final long midSide = (long) mid.lengthInBits () + side.lengthInBits (); + + final long smallest = Math.min (Math.min (leftRight, midSide), Math.min (leftSide, sideRight)); + if (smallest == leftRight) + subframes = new BitWriter [] + { + left, + right + }; + else if (smallest == leftSide) + { + channelAssignment = 0b1000; + subframes = new BitWriter [] + { + left, + side + }; + } + else if (smallest == sideRight) + { + channelAssignment = 0b1001; + subframes = new BitWriter [] + { + side, + right + }; + } + else + { + channelAssignment = 0b1010; + subframes = new BitWriter [] + { + mid, + side + }; + } + } + else + { + subframes = new BitWriter [channels.length]; + for (int i = 0; i < channels.length; i++) + subframes[i] = encodeSubframeToWriter (channels[i], offset, blockSize, bitsPerSample); + } + + final BitWriter writer = new BitWriter (); + + // Sync code, mandatory 0 bit and fixed block size stream marker + writer.writeBits (0b11111111111110, 14); + writer.writeBits (0, 2); + final int blockSizeCode = getBlockSizeCode (blockSize); + writer.writeBits (blockSizeCode, 4); + final int sampleRateCode = getSampleRateCode (sampleRate); + writer.writeBits (sampleRateCode, 4); + writer.writeBits (channelAssignment, 4); + writer.writeBits (getSampleSizeCode (bitsPerSample), 3); + writer.writeBits (0, 1); + writeUtf8CodedNumber (writer, frameIndex); + if (blockSizeCode == 0b0111) + writer.writeBits (blockSize - 1, 16); + if (sampleRateCode == 0b1101) + writer.writeBits (sampleRate, 16); + else if (sampleRateCode == 0b1110) + writer.writeBits (sampleRate / 10, 16); + writer.writeBits (calcCRC8 (writer.buffer, writer.size), 8); + + for (final BitWriter subframe: subframes) + subframe.appendTo (writer); + + writer.align (); + writer.writeBits (calcCRC16 (writer.buffer, writer.size), 16); + return writer.toByteArray (); + } + + + /** + * Calculates the mid and side channels of a stereo block. The side channel needs 1 bit more + * resolution than the source channels. + * + * @param channels The audio data of both channels + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @return The mid samples (index 0) and side samples (index 1) of the block + */ + private static int [] [] calcStereoDecorrelation (final int [] [] channels, final int offset, final int blockSize) + { + final int [] mid = new int [blockSize]; + final int [] side = new int [blockSize]; + for (int i = 0; i < blockSize; i++) + { + final int left = channels[0][offset + i]; + final int right = channels[1][offset + i]; + mid[i] = left + right >> 1; + side[i] = left - right; + } + return new int [] [] + { + mid, + side + }; + } + + + /** + * Encodes the block of one channel as a sub-frame into a new writer. + * + * @param samples The samples of the channel + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @param bitsPerSample The resolution of the samples + * @return The writer with the encoded sub-frame + */ + private static BitWriter encodeSubframeToWriter (final int [] samples, final int offset, final int blockSize, final int bitsPerSample) + { + final BitWriter writer = new BitWriter (); + encodeSubframe (writer, samples, offset, blockSize, bitsPerSample); + return writer; + } + + + /** + * Encodes the block of one channel as a sub-frame. Constant blocks are stored as a single + * value. Blocks too short for prediction are stored verbatim. All other blocks use the fixed + * predictor (order 0-4) or linear predictor (LPC) with the smallest encoded size; if the Rice + * coded residuals would exceed the verbatim size, the block is stored verbatim instead. + * + * @param writer The writer to append the sub-frame to + * @param samples The samples of the channel + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @param bitsPerSample The resolution of the samples + */ + private static void encodeSubframe (final BitWriter writer, final int [] samples, final int offset, final int blockSize, final int bitsPerSample) + { + // Mandatory sub-frame header padding bit + writer.writeBits (0, 1); + + boolean isConstant = true; + for (int i = 1; i < blockSize; i++) + if (samples[offset + i] != samples[offset]) + { + isConstant = false; + break; + } + if (isConstant) + { + writer.writeBits (0b000000, 6); + writer.writeBits (0, 1); + writer.writeBits (samples[offset], bitsPerSample); + return; + } + + // Store very short trailing blocks verbatim: a predictor of order N requires N warm-up + // samples plus at least 1 residual and the jFLAC based reader which is used to read FLAC + // files mis-decodes predictions when a block is shorter than 16 samples + if (blockSize < 16) + { + writeVerbatimSubframe (writer, samples, offset, blockSize, bitsPerSample); + return; + } + + final Prediction fixed = findBestFixedPrediction (samples, offset, blockSize); + final Prediction lpc = findBestLpcPrediction (samples, offset, blockSize, bitsPerSample); + + // Compare the encoded payload sizes of all sub-frame alternatives + final long fixedBits = fixed.order * (long) bitsPerSample + 6 + fixed.riceBits; + final long lpcBits = lpc == null ? Long.MAX_VALUE : lpc.order * (long) (bitsPerSample + LPC_PRECISION) + 9 + 6 + lpc.riceBits; + final long verbatimBits = (long) blockSize * bitsPerSample; + + if (lpcBits < fixedBits && lpcBits < verbatimBits) + { + writer.writeBits (0b100000 | lpc.order - 1, 6); + writer.writeBits (0, 1); + for (int i = 0; i < lpc.order; i++) + writer.writeBits (samples[offset + i], bitsPerSample); + writer.writeBits (LPC_PRECISION - 1, 4); + writer.writeBits (lpc.shift, 5); + for (int i = 0; i < lpc.order; i++) + writer.writeBits (lpc.coefficients[i], LPC_PRECISION); + writeRiceResidual (writer, lpc); + return; + } + + if (fixedBits >= verbatimBits) + { + writeVerbatimSubframe (writer, samples, offset, blockSize, bitsPerSample); + return; + } + + writer.writeBits (0b001000 | fixed.order, 6); + writer.writeBits (0, 1); + for (int i = 0; i < fixed.order; i++) + writer.writeBits (samples[offset + i], bitsPerSample); + writeRiceResidual (writer, fixed); + } + + + /** + * Finds the fixed predictor order (0-4) with the smallest absolute residual sum and + * calculates its residuals and Rice parameter. + * + * @param samples The samples of the channel + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @return The prediction + */ + private static Prediction findBestFixedPrediction (final int [] samples, final int offset, final int blockSize) + { + final int [] residuals = new int [blockSize]; + System.arraycopy (samples, offset, residuals, 0, blockSize); + int bestOrder = 0; + long bestSum = sumAbsolute (residuals, 0, blockSize); + for (int order = 1; order <= MAX_FIXED_ORDER; order++) + { + for (int i = blockSize - 1; i >= order; i--) + residuals[i] -= residuals[i - 1]; + final long sum = sumAbsolute (residuals, order, blockSize); + if (sum < bestSum) + { + bestSum = sum; + bestOrder = order; + } + } + + // Re-calculate the residuals of the best order + System.arraycopy (samples, offset, residuals, 0, blockSize); + for (int order = 1; order <= bestOrder; order++) + for (int i = blockSize - 1; i >= order; i--) + residuals[i] -= residuals[i - 1]; + + return new Prediction (bestOrder, residuals, blockSize); + } + + + /** + * Calculates a linear predictor for the block: a Welch window is applied for the + * auto-correlation, the Levinson-Durbin recursion provides the predictor coefficients for all + * orders up to MAX_LPC_ORDER and the order with the smallest estimated size is chosen. The + * chosen coefficients are quantized to integers; the residuals are calculated with the exact + * integer arithmetic of the decoder, therefore the compression stays lossless independent of + * the predictor quality. + * + * @param samples The samples of the channel + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @param bitsPerSample The resolution of the samples + * @return The prediction or null if no usable predictor was found + */ + private static Prediction findBestLpcPrediction (final int [] samples, final int offset, final int blockSize, final int bitsPerSample) + { + final int maxOrder = Math.min (MAX_LPC_ORDER, blockSize / 2); + + // Auto-correlation of the windowed block + final double [] windowed = new double [blockSize]; + final double half = (blockSize - 1) / 2.0; + for (int i = 0; i < blockSize; i++) + { + final double w = (i - half) / half; + windowed[i] = samples[offset + i] * (1.0 - w * w); + } + final double [] autoCorrelation = new double [maxOrder + 1]; + for (int lag = 0; lag <= maxOrder; lag++) + { + double sum = 0; + for (int i = lag; i < blockSize; i++) + sum += windowed[i] * windowed[i - lag]; + autoCorrelation[lag] = sum; + } + if (autoCorrelation[0] <= 0) + return null; + + // Levinson-Durbin recursion; keep the coefficients of all orders + final double [] [] coefficients = new double [maxOrder + 1] []; + final double [] coefficientsWork = new double [maxOrder]; + double error = autoCorrelation[0]; + int bestOrder = 0; + double bestEstimate = Double.MAX_VALUE; + for (int order = 1; order <= maxOrder; order++) + { + double accumulator = autoCorrelation[order]; + for (int i = 0; i < order - 1; i++) + accumulator -= coefficientsWork[i] * autoCorrelation[order - 1 - i]; + final double reflection = accumulator / error; + final double [] previous = coefficientsWork.clone (); + coefficientsWork[order - 1] = reflection; + for (int i = 0; i < order - 1; i++) + coefficientsWork[i] = previous[i] - reflection * previous[order - 2 - i]; + error *= 1 - reflection * reflection; + if (!Double.isFinite (error) || error < 0) + break; + coefficients[order] = coefficientsWork.clone (); + + // Estimated size in bits: Rice coded residuals plus warm-up and coefficients + final double estimate = 0.5 * blockSize * Math.log (Math.max (error / blockSize, 1e-9)) / Math.log (2) + order * (double) (bitsPerSample + LPC_PRECISION); + if (estimate < bestEstimate) + { + bestEstimate = estimate; + bestOrder = order; + } + } + if (bestOrder == 0) + return null; + + // Quantize the coefficients of the best order + final double [] lpcCoefficients = coefficients[bestOrder]; + double maxCoefficient = 0; + for (final double coefficient: lpcCoefficients) + maxCoefficient = Math.max (maxCoefficient, Math.abs (coefficient)); + if (maxCoefficient <= 0 || !Double.isFinite (maxCoefficient)) + return null; + int shift = LPC_PRECISION - 1 - (Math.getExponent (maxCoefficient) + 1); + if (shift > 15) + shift = 15; + if (shift < 0) + return null; + final int [] quantized = new int [bestOrder]; + final int limit = (1 << LPC_PRECISION - 1) - 1; + double quantizationError = 0; + for (int i = 0; i < bestOrder; i++) + { + final double scaled = lpcCoefficients[i] * (1 << shift) + quantizationError; + long rounded = Math.round (scaled); + if (rounded > limit) + rounded = limit; + else if (rounded < -limit - 1) + rounded = -limit - 1; + quantizationError = scaled - rounded; + quantized[i] = (int) rounded; + } + + // Calculate the residuals with the integer arithmetic of the decoder + final int [] residuals = new int [blockSize]; + for (int i = bestOrder; i < blockSize; i++) + { + long prediction = 0; + for (int j = 0; j < bestOrder; j++) + prediction += (long) quantized[j] * samples[offset + i - 1 - j]; + final long residual = samples[offset + i] - (prediction >> shift); + // Reject pathological predictors + if (residual > 1 << 30 || residual < -(1 << 30)) + return null; + residuals[i] = (int) residual; + } + + final Prediction prediction = new Prediction (bestOrder, residuals, blockSize); + prediction.coefficients = quantized; + prediction.shift = shift; + return prediction; + } + + + /** + * Writes the residuals of a prediction: residual coding method 0 (4-bit Rice parameters) with + * the partition order chosen by the prediction analysis. + * + * @param writer The writer to append the residuals to + * @param prediction The prediction + */ + private static void writeRiceResidual (final BitWriter writer, final Prediction prediction) + { + writer.writeBits (0b00, 2); + writer.writeBits (prediction.partitionOrder, 4); + final int partitionSize = prediction.blockSize >> prediction.partitionOrder; + for (int partition = 0; partition < 1 << prediction.partitionOrder; partition++) + { + final int riceParameter = prediction.riceParameters[partition]; + writer.writeBits (riceParameter, 4); + final int from = partition == 0 ? prediction.order : partition * partitionSize; + final int to = (partition + 1) * partitionSize; + for (int i = from; i < to; i++) + { + final int zigzag = zigzagEncode (prediction.residuals[i]); + writer.writeUnary (zigzag >>> riceParameter); + if (riceParameter > 0) + writer.writeBits (zigzag, riceParameter); + } + } + } + + + /** + * Writes a verbatim sub-frame which stores all samples uncompressed. + * + * @param writer The writer to append the sub-frame to + * @param samples The samples of the channel + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @param bitsPerSample The resolution of the samples + */ + private static void writeVerbatimSubframe (final BitWriter writer, final int [] samples, final int offset, final int blockSize, final int bitsPerSample) + { + writer.writeBits (0b000001, 6); + writer.writeBits (0, 1); + for (int i = 0; i < blockSize; i++) + writer.writeBits (samples[offset + i], bitsPerSample); + } + + + /** + * Finds the Rice parameter with the smallest encoded size for the given residual range. + * + * @param residuals The residuals of the block + * @param from The index of the first residual + * @param to The index after the last residual + * @return The Rice parameter in the range of [0, MAX_RICE_PARAMETER] + */ + private static int findRiceParameter (final int [] residuals, final int from, final int to) + { + int bestParameter = 0; + long bestCost = Long.MAX_VALUE; + for (int parameter = 0; parameter <= MAX_RICE_PARAMETER; parameter++) + { + final long cost = calcRiceCost (residuals, from, to, parameter); + if (cost < bestCost) + { + bestCost = cost; + bestParameter = parameter; + } + } + return bestParameter; + } + + + /** + * Calculates the number of bits needed to Rice code the given residual range. + * + * @param residuals The residuals of the block + * @param from The index of the first residual + * @param to The index after the last residual + * @param parameter The Rice parameter + * @return The number of bits + */ + private static long calcRiceCost (final int [] residuals, final int from, final int to, final int parameter) + { + long cost = 0; + for (int i = from; i < to; i++) + cost += (zigzagEncode (residuals[i]) >>> parameter) + 1L + parameter; + return cost; + } + + + /** + * Maps a signed residual to an unsigned value by folding: 0, -1, 1, -2, ... become 0, 1, 2, 3, + * ... + * + * @param value The signed value + * @return The folded unsigned value + */ + private static int zigzagEncode (final int value) + { + return value >= 0 ? value << 1 : (-value << 1) - 1; + } + + + /** + * Sums up the absolute values of the given range. + * + * @param values The values + * @param startIndex The index of the first value + * @param endIndex The index after the last value + * @return The sum + */ + private static long sumAbsolute (final int [] values, final int startIndex, final int endIndex) + { + long sum = 0; + for (int i = startIndex; i < endIndex; i++) + sum += Math.abs ((long) values[i]); + return sum; + } + + + /** + * Interleaves a block of all channels into little-endian bytes as needed for the MD5 checksum + * of the un-encoded audio data. + * + * @param channels The audio data of all channels + * @param offset The index of the first sample of the block + * @param blockSize The number of samples in the block + * @param bitsPerSample The resolution of the samples + * @param output Where to write the bytes to + * @return The number of bytes written + */ + private static int interleaveLittleEndian (final int [] [] channels, final int offset, final int blockSize, final int bitsPerSample, final byte [] output) + { + final int bytesPerSample = bitsPerSample / 8; + int position = 0; + for (int i = 0; i < blockSize; i++) + for (final int [] channel: channels) + { + final int value = channel[offset + i]; + for (int b = 0; b < bytesPerSample; b++) + output[position++] = (byte) (value >> 8 * b); + } + return position; + } + + + /** + * Gets the 4-bit code for the block size. Common sizes have a direct code, all others are + * stored explicitly at the end of the frame header. + * + * @param blockSize The number of samples in the block + * @return The code + */ + private static int getBlockSizeCode (final int blockSize) + { + switch (blockSize) + { + case 192: + return 0b0001; + case 576: + return 0b0010; + case 1152: + return 0b0011; + case 2304: + return 0b0100; + case 4608: + return 0b0101; + case 256: + return 0b1000; + case 512: + return 0b1001; + case 1024: + return 0b1010; + case 2048: + return 0b1011; + case 4096: + return 0b1100; + case 8192: + return 0b1101; + case 16384: + return 0b1110; + case 32768: + return 0b1111; + default: + // 16 bit (blockSize - 1) follows at the end of the frame header + return 0b0111; + } + } + + + /** + * Gets the 4-bit code for the sample rate. Common rates have a direct code, other rates are + * stored explicitly at the end of the frame header or are only present in the STREAMINFO + * block. + * + * @param sampleRate The sample rate in Hertz + * @return The code + */ + private static int getSampleRateCode (final int sampleRate) + { + switch (sampleRate) + { + case 88200: + return 0b0001; + case 176400: + return 0b0010; + case 192000: + return 0b0011; + case 8000: + return 0b0100; + case 16000: + return 0b0101; + case 22050: + return 0b0110; + case 24000: + return 0b0111; + case 32000: + return 0b1000; + case 44100: + return 0b1001; + case 48000: + return 0b1010; + case 96000: + return 0b1011; + default: + // 16 bit sample rate in Hz or in tens of Hz follows at the end of the frame + // header, otherwise the decoder reads it from the STREAMINFO block + if (sampleRate <= 0xFFFF) + return 0b1101; + if (sampleRate % 10 == 0 && sampleRate / 10 <= 0xFFFF) + return 0b1110; + return 0b0000; + } + } + + + /** + * Gets the 3-bit code for the sample resolution. + * + * @param bitsPerSample The resolution of the samples + * @return The code + */ + private static int getSampleSizeCode (final int bitsPerSample) + { + switch (bitsPerSample) + { + case 8: + return 0b001; + case 16: + return 0b100; + case 24: + return 0b110; + default: + throw new IllegalArgumentException ("FLAC: Unsupported bit resolution: " + bitsPerSample); + } + } + + + /** + * Writes a number in the variable length UTF-8 style coding used for the frame number. + * + * @param writer The writer to append the bytes to + * @param number The number to write, must be positive + */ + private static void writeUtf8CodedNumber (final BitWriter writer, final long number) + { + if (number < 0x80) + { + writer.writeBits ((int) number, 8); + return; + } + int continuationBytes = 1; + while (number >= 1L << 6 + 5 * continuationBytes) + continuationBytes++; + writer.writeBits (0xFF00 >> continuationBytes + 1 & 0xFF | (int) (number >>> 6 * continuationBytes), 8); + for (int i = continuationBytes - 1; i >= 0; i--) + writer.writeBits (0x80 | (int) (number >>> 6 * i) & 0x3F, 8); + } + + + /** + * Calculates the CRC-8 checksum (polynomial 0x07) as used for the frame header. + * + * @param data The data + * @param length The number of bytes to process + * @return The checksum + */ + private static int calcCRC8 (final byte [] data, final int length) + { + int crc = 0; + for (int i = 0; i < length; i++) + crc = CRC8_TABLE[crc ^ data[i] & 0xFF]; + return crc; + } + + + /** + * Calculates the CRC-16 checksum (polynomial 0x8005) as used for the frame footer. + * + * @param data The data + * @param length The number of bytes to process + * @return The checksum + */ + private static int calcCRC16 (final byte [] data, final int length) + { + int crc = 0; + for (int i = 0; i < length; i++) + crc = crc << 8 & 0xFFFF ^ CRC16_TABLE[crc >> 8 ^ data[i] & 0xFF]; + return crc; + } + + + /** + * Creates the MD5 digest for the checksum of the un-encoded audio data. + * + * @return The digest + * @throws IOException The MD5 algorithm is not available + */ + private static MessageDigest createMD5Digest () throws IOException + { + try + { + return MessageDigest.getInstance ("MD5"); + } + catch (final NoSuchAlgorithmException ex) + { + throw new IOException (ex); + } + } + + + /** + * The result of a prediction analysis of one block, with its residuals and the Rice partition + * order and parameters with the smallest encoded size. + */ + private static class Prediction + { + final int order; + final int [] residuals; + final int blockSize; + int partitionOrder; + int [] riceParameters; + long riceBits; + int [] coefficients; + int shift; + + + Prediction (final int order, final int [] residuals, final int blockSize) + { + this.order = order; + this.residuals = residuals; + this.blockSize = blockSize; + + // Find the Rice partitioning with the smallest total size; a partitioning is only + // possible if the block size is divisible and the first partition is not empty + this.riceBits = Long.MAX_VALUE; + for (int candidate = 0; candidate <= MAX_PARTITION_ORDER; candidate++) + { + final int numberOfPartitions = 1 << candidate; + if ((blockSize & numberOfPartitions - 1) != 0) + continue; + final int partitionSize = blockSize >> candidate; + if (partitionSize <= order) + break; + final int [] parameters = new int [numberOfPartitions]; + long bits = 0; + for (int partition = 0; partition < numberOfPartitions; partition++) + { + final int from = partition == 0 ? order : partition * partitionSize; + final int to = (partition + 1) * partitionSize; + parameters[partition] = findRiceParameter (residuals, from, to); + bits += 4 + calcRiceCost (residuals, from, to, parameters[partition]); + } + if (bits < this.riceBits) + { + this.riceBits = bits; + this.partitionOrder = candidate; + this.riceParameters = parameters; + } + } + } + } + + + /** Writes single bits into a growing byte buffer, most significant bit first. */ + private static class BitWriter + { + private byte [] buffer = new byte [16384]; + private int size = 0; + private int bitCache = 0; + private int bitCount = 0; + + + /** + * Writes the given number of low bits of the value. + * + * @param value The value to write + * @param count The number of bits, 0 to 32 + */ + final void writeBits (final int value, final int count) + { + int remaining = count; + while (remaining > 0) + { + final int take = Math.min (8 - this.bitCount, remaining); + final int chunk = value >>> remaining - take & (1 << take) - 1; + this.bitCache = this.bitCache << take | chunk; + this.bitCount += take; + remaining -= take; + if (this.bitCount == 8) + this.flushByte (); + } + } + + + /** + * Writes the given number of low bits of the long value. + * + * @param value The value to write + * @param count The number of bits, 0 to 64 + */ + final void writeLongBits (final long value, final int count) + { + if (count > 32) + { + this.writeBits ((int) (value >>> 32), count - 32); + this.writeBits ((int) value, 32); + } + else + this.writeBits ((int) value, count); + } + + + /** + * Writes a value in unary coding: the value as a number of 0 bits followed by a 1 bit. + * + * @param value The value to write, must be positive + */ + final void writeUnary (final int value) + { + int remaining = value; + while (remaining >= 32) + { + this.writeBits (0, 32); + remaining -= 32; + } + this.writeBits (1, remaining + 1); + } + + + /** + * Gets the number of written bits. + * + * @return The number of bits + */ + final int lengthInBits () + { + return this.size * 8 + this.bitCount; + } + + + /** + * Appends all written bits to the given writer. + * + * @param other The writer to append to + */ + final void appendTo (final BitWriter other) + { + for (int i = 0; i < this.size; i++) + other.writeBits (this.buffer[i] & 0xFF, 8); + if (this.bitCount > 0) + other.writeBits (this.bitCache & (1 << this.bitCount) - 1, this.bitCount); + } + + + /** + * Fills up the current byte with 0 bits. + */ + final void align () + { + if (this.bitCount > 0) + { + this.bitCache <<= 8 - this.bitCount; + this.bitCount = 8; + this.flushByte (); + } + } + + + /** + * Gets all written bytes. The writer must be byte aligned. + * + * @return The bytes + */ + final byte [] toByteArray () + { + final byte [] result = new byte [this.size]; + System.arraycopy (this.buffer, 0, result, 0, this.size); + return result; + } + + + private void flushByte () + { + if (this.size == this.buffer.length) + { + final byte [] grown = new byte [this.buffer.length * 2]; + System.arraycopy (this.buffer, 0, grown, 0, this.size); + this.buffer = grown; + } + this.buffer[this.size] = (byte) this.bitCache; + this.size++; + this.bitCache = 0; + this.bitCount = 0; + } + } +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/bliss/BlissCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/bliss/BlissCreator.java index a3420ae8..eb4f9739 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/bliss/BlissCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/bliss/BlissCreator.java @@ -8,16 +8,12 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.EnumMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.zip.ZipOutputStream; -import javax.sound.sampled.UnsupportedAudioFileException; - import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -355,32 +351,16 @@ protected void rewriteFile (final IMultisampleSource multisampleSource, final IS if (sampleDataOpt.isEmpty ()) return; - // Trim and convert to FLAC + // Trim sample from zone start to end ISampleData sampleData = sampleDataOpt.get (); - final Path tempFile = Files.createTempFile ("CWM-", ".flac"); - try - { - // Trim sample from zone start to end - if (zone.getStart () > 0) - { - final WaveFile waveFile = AudioFileUtils.convertToWav (sampleData, DESTINATION_AUDIO_FORMAT); - trimStartToEnd (waveFile, zone); - sampleData = new WavFileSampleData (waveFile); - } - - // It is important to write to a file otherwise the FLAC header is broken! - AudioFileUtils.compressToFLAC (sampleData, FLAC_TARGET_FORMAT, tempFile.toFile ()); - - Files.copy (tempFile, outputStream); - } - catch (final UnsupportedAudioFileException ex) + if (zone.getStart () > 0) { - throw new IOException (ex); - } - finally - { - Files.deleteIfExists (tempFile); + final WaveFile waveFile = AudioFileUtils.convertToWav (sampleData, DESTINATION_AUDIO_FORMAT); + trimStartToEnd (waveFile, zone); + sampleData = new WavFileSampleData (waveFile); } + + outputStream.write (AudioFileUtils.compressToFLAC (sampleData)); } diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/renoise/RenoiseCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/renoise/RenoiseCreator.java index 0d476fab..9fd67d16 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/renoise/RenoiseCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/renoise/RenoiseCreator.java @@ -7,8 +7,6 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.Date; import java.util.List; import java.util.Locale; @@ -16,8 +14,6 @@ import java.util.zip.CRC32; import java.util.zip.ZipOutputStream; -import javax.sound.sampled.UnsupportedAudioFileException; - import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -343,9 +339,9 @@ private static void addParameter (final Document document, final Element deviceE /** - * Store all samples of the multi-sample into the ZIP. The samples are stored as FLAC; if the - * (bundled) FLAC encoder fails on a sample - some samples trigger a bug in the encoder library - * - the sample is stored as an uncompressed WAV file instead, which Renoise reads as well. + * Store all samples of the multi-sample into the ZIP. The samples are stored as FLAC; should + * the FLAC encoder ever fail on a sample, the sample is stored as an uncompressed WAV file + * instead, which Renoise reads as well. * * @param zipOutputStream The ZIP output stream * @param multisampleSource The multi-sample @@ -401,7 +397,7 @@ private void storeRenoiseSample (final ZipOutputStream zipOutputStream, final IS String extension; try { - data = encodeToFlac (renderSource); + data = AudioFileUtils.compressToFLAC (renderSource); extension = ".flac"; } catch (final IOException | RuntimeException _) @@ -418,34 +414,6 @@ private void storeRenoiseSample (final ZipOutputStream zipOutputStream, final IS } - /** - * FLAC encode the given sample data and return the bytes. It is important to write to a file - * first otherwise the FLAC header is not updated. 32-bit float samples are reduced to a FLAC - * compatible resolution by compressToFLAC. - * - * @param sampleData The sample data - * @return The FLAC encoded bytes - * @throws IOException Could not encode the sample - */ - private static byte [] encodeToFlac (final ISampleData sampleData) throws IOException - { - final Path tempFile = Files.createTempFile ("CWM-Renoise-", ".flac"); - try - { - AudioFileUtils.compressToFLAC (sampleData, FLAC_TARGET_FORMAT, tempFile.toFile ()); - return Files.readAllBytes (tempFile); - } - catch (final UnsupportedAudioFileException ex) - { - throw new IOException (ex); - } - finally - { - Files.deleteIfExists (tempFile); - } - } - - /** * Get the (first) loop of a zone if a cross-fade should be baked into its audio. Only forward * loops with a cross-fade greater than zero are considered. diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java index 4815a434..d4ee56b7 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/sfz/SfzCreator.java @@ -19,8 +19,6 @@ import java.util.Optional; import java.util.Set; -import javax.sound.sampled.UnsupportedAudioFileException; - import de.mossgrabers.convertwithmoss.core.IMultisampleSource; import de.mossgrabers.convertwithmoss.core.INotifier; import de.mossgrabers.convertwithmoss.core.ParameterLevel; @@ -124,14 +122,7 @@ public void createPreset (final File destinationFolder, final IMultisampleSource safeCreateDirectory (sampleFolder); if (this.settingsConfiguration.convertToFlac ()) - try - { - this.writeFlacSamples (sampleFolder, multisampleSource); - } - catch (final UnsupportedAudioFileException ex) - { - throw new IOException (ex); - } + this.writeFlacSamples (sampleFolder, multisampleSource); else this.writeSamples (sampleFolder, multisampleSource); diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/synclavier/SynclavierRegenCreator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/synclavier/SynclavierRegenCreator.java index 75638f0e..1ef0ecd6 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/synclavier/SynclavierRegenCreator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/synclavier/SynclavierRegenCreator.java @@ -9,7 +9,6 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -20,8 +19,6 @@ import java.util.Optional; import java.util.Set; -import javax.sound.sampled.UnsupportedAudioFileException; - import de.mossgrabers.convertwithmoss.core.IMultisampleSource; import de.mossgrabers.convertwithmoss.core.INotifier; import de.mossgrabers.convertwithmoss.core.algorithm.MathUtils; @@ -619,25 +616,12 @@ private static String sampleRow (final String sampleName, final IAudioMetadata a */ private static void writeSample (final File libraryFolder, final String sampleName, final ISampleZone zone) throws IOException { - final Path tempFile = Files.createTempFile ("CWM-", ".flac"); - try - { - final Optional sampleData = zone.getSampleData (); - if (sampleData.isEmpty ()) - throw new IOException ("Empty sample data in zone: " + zone.getName ()); - AudioFileUtils.compressToFLAC (sampleData.get (), FLAC_TARGET_FORMAT, tempFile.toFile ()); - final byte [] flacData = Files.readAllBytes (tempFile); - final byte [] obfuscated = SynclavierRegenCodec.transform (flacData, sampleName); - Files.write (new File (libraryFolder, sampleName + ".sflc").toPath (), obfuscated); - } - catch (final UnsupportedAudioFileException ex) - { - throw new IOException (ex); - } - finally - { - Files.deleteIfExists (tempFile); - } + final Optional sampleData = zone.getSampleData (); + if (sampleData.isEmpty ()) + throw new IOException ("Empty sample data in zone: " + zone.getName ()); + final byte [] flacData = AudioFileUtils.compressToFLAC (sampleData.get ()); + final byte [] obfuscated = SynclavierRegenCodec.transform (flacData, sampleName); + Files.write (new File (libraryFolder, sampleName + ".sflc").toPath (), obfuscated); }