From 49aecf7b199641c3f7cd4e9e615506edf7d152af Mon Sep 17 00:00:00 2001 From: Python Blue <29385655+PythonBlue@users.noreply.github.com> Date: Fri, 11 Jul 2025 17:24:59 -0400 Subject: [PATCH 01/12] Experimental debut Experimental support for converting from Fairlight CMI 3 voices. Only basic mapping parameters and loop information are supported for now. --- .../core/ConverterBackend.java | 2 + .../format/cmi3/VCDetector.java | 97 +++ .../format/cmi3/VCDetectorUI.java | 109 ++++ .../convertwithmoss/format/cmi3/VCFile.java | 550 ++++++++++++++++++ 4 files changed, 758 insertions(+) create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetector.java create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetectorUI.java create mode 100644 src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java b/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java index 0b49aeb8..600bf4d8 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java @@ -36,6 +36,7 @@ import de.mossgrabers.convertwithmoss.format.kmp.KMPDetector; import de.mossgrabers.convertwithmoss.format.korgmultisample.KorgmultisampleCreator; import de.mossgrabers.convertwithmoss.format.korgmultisample.KorgmultisampleDetector; +import de.mossgrabers.convertwithmoss.format.cmi3.VCDetector; import de.mossgrabers.convertwithmoss.format.music1010.Music1010Creator; import de.mossgrabers.convertwithmoss.format.music1010.Music1010Detector; import de.mossgrabers.convertwithmoss.format.nki.NkiCreator; @@ -94,6 +95,7 @@ public ConverterBackend (final INotifier notifier) this.detectors = new IDetector [] { + new VCDetector (notifier), new Music1010Detector (notifier), new AbletonDetector (notifier), new MPCKeygroupDetector (notifier), diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetector.java new file mode 100644 index 00000000..903d2c34 --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetector.java @@ -0,0 +1,97 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2024 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.format.cmi3; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.function.Consumer; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; + +import de.mossgrabers.convertwithmoss.core.IMultisampleSource; +import de.mossgrabers.convertwithmoss.core.INotifier; +import de.mossgrabers.convertwithmoss.core.detector.AbstractDetector; +import de.mossgrabers.convertwithmoss.core.detector.DefaultMultisampleSource; +import de.mossgrabers.convertwithmoss.core.model.IGroup; +import de.mossgrabers.convertwithmoss.core.model.IMetadata; +import de.mossgrabers.convertwithmoss.core.model.ISampleData; +import de.mossgrabers.convertwithmoss.core.model.ISampleLoop; +import de.mossgrabers.convertwithmoss.core.model.ISampleZone; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultGroup; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleLoop; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleZone; +import de.mossgrabers.convertwithmoss.core.settings.MetadataSettingsUI; +import de.mossgrabers.convertwithmoss.exception.ParseException; +import de.mossgrabers.convertwithmoss.file.AudioFileUtils; +import de.mossgrabers.tools.ui.Functions; + + +/** + * Descriptor for Fairlight CMI3 Voice (VC) files detector. + * + * @author Jürgen Moßgraber + */ +public class VCDetector extends AbstractDetector +{ + private static final String [] VC_ENDINGS = + { + ".vc", + ".VC" + }; + + /** + * Constructor. + * + * @param notifier The notifier + */ + public VCDetector (final INotifier notifier) + { + super ("Fairlight CMI3 Voice", "VC", notifier, new VCDetectorUI ("VC")); + } + + + /** {@inheritDoc} */ + @Override + protected void configureFileEndings (final boolean detectPerformances) + { + this.fileEndings = VC_ENDINGS; + } + + /** {@inheritDoc} */ + @Override + protected List readPresetFile (final File sourceFile) + { + final List multiSampleSources = new ArrayList<> (); + multiSampleSources.addAll(this.readVCFile(sourceFile)); + + return multiSampleSources; + } + + + /** + * Reads a VC file and creates a multi-sample source from it. + * + * @param sourceFile The VC file + * @return The multi-sample source if found + */ + private List readVCFile (final File sourceFile) + { + final List multiSampleSources = new ArrayList<> (); + try (final FileInputStream stream = new FileInputStream (sourceFile)) + { + final VCFile vcFile = new VCFile (this.notifier, sourceFile); + multiSampleSources.addAll(vcFile.read (stream, sourceFile)); + } + catch (final IOException | ParseException ex) + { + this.notifier.logError ("IDS_ERR_SOURCE_FORMAT_NOT_SUPPORTED", ex); + return Collections.emptyList (); + } + return multiSampleSources; + } + +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetectorUI.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetectorUI.java new file mode 100644 index 00000000..473cd36d --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCDetectorUI.java @@ -0,0 +1,109 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2025 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.format.cmi3; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import de.mossgrabers.convertwithmoss.core.INotifier; +import de.mossgrabers.convertwithmoss.core.settings.MetadataSettingsUI; +import de.mossgrabers.tools.ui.BasicConfig; +import de.mossgrabers.tools.ui.panel.BoxPanel; +import javafx.geometry.Orientation; +import javafx.scene.Node; +import javafx.scene.control.CheckBox; +import javafx.scene.control.ScrollPane; + + +/** + * Settings for the CMI3 detector. + * + * @author Jürgen Moßgraber + */ +public class VCDetectorUI extends MetadataSettingsUI +{ + + + /** + * Constructor. + * + * @param prefix The prefix to use for the properties tags + */ + public VCDetectorUI (final String prefix) + { + super (prefix); + } + + + /** {@inheritDoc} */ + @Override + public Node getEditPane () + { + final BoxPanel panel = new BoxPanel (Orientation.VERTICAL); + + //////////////////////////////////////////////////////////// + // Options + + //////////////////////////////////////////////////////////// + // Metadata + + this.addTo (panel); + this.getSeparator ().getStyleClass ().add ("titled-separator-pane"); + + final ScrollPane scrollPane = new ScrollPane (panel.getPane ()); + scrollPane.fitToWidthProperty ().set (true); + scrollPane.fitToHeightProperty ().set (true); + return scrollPane; + } + + + /** {@inheritDoc} */ + @Override + public void saveSettings (final BasicConfig config) + { + super.saveSettings (config); + } + + + /** {@inheritDoc} */ + @Override + public void loadSettings (final BasicConfig config) + { + super.loadSettings (config); + } + + + /** {@inheritDoc} */ + @Override + public boolean checkSettingsUI (final INotifier notifier) + { + if (!super.checkSettingsUI (notifier)) + return false; + + return true; + } + + + /** {@inheritDoc} */ + @Override + public boolean checkSettingsCLI (INotifier notifier, Map parameters) + { + if (!super.checkSettingsCLI (notifier, parameters)) + return false; + + return true; + } + + + /** {@inheritDoc} */ + @Override + public String [] getCLIParameterNames () + { + final List parameterNames = new ArrayList<> (Arrays.asList (super.getCLIParameterNames ())); + return parameterNames.toArray (new String [parameterNames.size ()]); + } +} diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java new file mode 100644 index 00000000..72aab809 --- /dev/null +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -0,0 +1,550 @@ +// Written by Jürgen Moßgraber - mossgrabers.de +// (c) 2019-2025 +// Licensed under LGPLv3 - http://www.gnu.org/licenses/lgpl-3.0.txt + +package de.mossgrabers.convertwithmoss.format.cmi3; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Date; +import java.util.Collections; +import java.util.List; + +import de.mossgrabers.convertwithmoss.core.IMultisampleSource; +import de.mossgrabers.convertwithmoss.core.INotifier; +import de.mossgrabers.convertwithmoss.core.creator.AbstractCreator; +import de.mossgrabers.convertwithmoss.core.detector.DefaultMultisampleSource; +import de.mossgrabers.convertwithmoss.core.model.IAudioMetadata; +import de.mossgrabers.convertwithmoss.core.model.IGroup; +import de.mossgrabers.convertwithmoss.core.model.IMetadata; +import de.mossgrabers.convertwithmoss.core.model.ISampleLoop; +import de.mossgrabers.convertwithmoss.core.model.ISampleZone; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultGroup; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleLoop; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleZone; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultAudioMetadata; +import de.mossgrabers.convertwithmoss.core.model.implementation.InMemorySampleData; +import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleZone; +import de.mossgrabers.convertwithmoss.exception.CompressionNotSupportedException; +import de.mossgrabers.convertwithmoss.exception.ParseException; +import de.mossgrabers.convertwithmoss.file.AudioFileUtils; +import de.mossgrabers.tools.FileUtils; +import de.mossgrabers.tools.StringUtils; +import de.mossgrabers.tools.ui.Functions; + + +/** + * Accessor to a Fairlight CMI 3 voice (VC) file. + * + * @author Jürgen Moßgraber + */ +public class VCFile +{ + private static final int VC_VERSION_A = 768; + private static final int VC_VERSION_B = 769; + + private static final int VC_NAME_SIZE = 16; + + private final INotifier notifier; + private int fileSeeker; + private int fileSeeker2; + + private String name; + + + /** + * Constructor. + * + * @param notifier For logging errors + * @param vcFile The source file + */ + public VCFile (final INotifier notifier, final File vcFile) throws IOException, ParseException + { + this.notifier = notifier; + this.name = FileUtils.getNameWithoutType(vcFile); + } + + + /** + * Get the name. + * + * @return The name + */ + public String getName () + { + return this.name; + } + + + /** + * Read and parse a VC file. + * + * @throws IOException Could not read the file + * @throws ParseException Error during parsing + */ + public List read (final InputStream inputStream, final File sourceFile) throws IOException, ParseException + { + final DataInputStream in = new DataInputStream (inputStream); + final List groups = new ArrayList<> (); + final IGroup group = new DefaultGroup ("CMI3"); + int numSubVoices = 0; + int voiceTune = 0; + int channels = 0; + int fileSeeker = 0; + int fileSeeker2 = 0; + int voiceFunctionCount = 0; + int mappingOffset = 0; + + byte[] inBytes = in.readAllBytes(); + byte[] header = Arrays.copyOfRange(inBytes, 0, 2816); + DefaultAudioMetadata[] audioMetadata = new DefaultAudioMetadata[256]; + InMemorySampleData[] sampleData = new InMemorySampleData[256]; + DefaultAudioMetadata[] audioMetadataR = new DefaultAudioMetadata[256]; + InMemorySampleData[] sampleDataR = new InMemorySampleData[256]; + List subvoiceID = new ArrayList(); + List zoneOffset = new ArrayList(); + List svID = new ArrayList(); + List svIDA = new ArrayList(); + List svIDB = new ArrayList(); + List svBR = new ArrayList(); + List svSizeA = new ArrayList(); + List svSizeB = new ArrayList(); + List svSR = new ArrayList(); + List svName = new ArrayList(); + List svTune = new ArrayList(); + List svWordA = new ArrayList(); + List svWordB = new ArrayList(); + List svStartA = new ArrayList(); + List svStartB = new ArrayList(); + List svEndA = new ArrayList(); + List svEndB = new ArrayList(); + List svLSA = new ArrayList(); + List svLSB = new ArrayList(); + List svLEA = new ArrayList(); + List svLEB = new ArrayList(); + List svIL = new ArrayList(); + List svLoop = new ArrayList(); + List svReleaseLoop = new ArrayList(); + + + for (int iPre = 0; iPre < 1; iPre++) + { + int checkDone = 0; + if ((header[0] == 3 && header[1] == 0) || (header[0] == 3 && header[1] == 1)) + checkDone++; + else + throw new ParseException (Functions.getMessage ("IDS_NOTIFY_ERR_ILLEGAL_CHARACTER")); + channels = Byte.toUnsignedInt(header[16]) > 127 ? 2 : 1; + voiceFunctionCount = (int)header[19]; + for (int i = 0; i < 128; i++) + { + if (header[i * 4 + 259] == 0 || header[i * 4 + 259] < 0) + { + numSubVoices = i; + this.notifier.logText(Integer.toString(numSubVoices)); + this.notifier.logText("\n"); + break; + } + else + { + this.notifier.logText(Integer.toString(Byte.toUnsignedInt(header[i * 4 + 259]))); + this.notifier.logText("\n"); + subvoiceID.add(Byte.toUnsignedInt(header[i * 4 + 259])); + } + } + + for (int i = 0; i < numSubVoices; i++) + { + zoneOffset.add((int) Byte.toUnsignedInt(header[i*4+256]) * 16777216 + Byte.toUnsignedInt(header[i*4+257]) * 65536 + Byte.toUnsignedInt(header[i*4+258]) * 256); + svID.add((int)header[i*4+259]); + svIDA.add(0); + svIDB.add(0); + svBR.add(16); + svSizeA.add(0); + svSizeB.add(0); + svSR.add(44100); + svName.add(""); + svTune.add(0); + svWordA.add(0); + svWordB.add(0); + svStartA.add(0); + svStartB.add(0); + svEndA.add(0); + svEndB.add(0); + svLSA.add(0); + svLSB.add(0); + svLEA.add(0); + svLEB.add(0); + svLoop.add(false); + svReleaseLoop.add(false); + svIL.add(false); + } + + byte[] headFuncBuff = Arrays.copyOfRange(header, 768, 768 + 2); + int skipped = 0; + while (Byte.toUnsignedInt(headFuncBuff[1]) > 2 && headFuncBuff[1] != 11) + { + int entrySize = Byte.toUnsignedInt(headFuncBuff[1]); + skipped += 2; + headFuncBuff = Arrays.copyOfRange(header, 768 + skipped, 768 + skipped + entrySize); + switch(headFuncBuff[0]) + { + case 6: + { + mappingOffset = 770 + skipped; + break; + } + case 9: + { + if (headFuncBuff[2] == 24 && headFuncBuff[3] == 1) + { + voiceTune = (int) Byte.toUnsignedInt(headFuncBuff[4]) * 256 + Byte.toUnsignedInt(headFuncBuff[5]); + } + break; + } + default: + { + break; + } + } + skipped += entrySize; + headFuncBuff = Arrays.copyOfRange(header, 768 + skipped, 768 + skipped + 2); + } + for (int itera = 0; itera < numSubVoices; itera++) + { + fileSeeker = zoneOffset.get(itera); + byte[] sub = Arrays.copyOfRange(inBytes, fileSeeker, fileSeeker + 768); + svIDA.set(itera, (int)sub[16]); + svBR.set(itera, (int)sub[17] == 2 ? 16 : 8); + svSizeA.set(itera, (int) Byte.toUnsignedInt(sub[18]) * 16777216 + Byte.toUnsignedInt(sub[19]) * 65536 + Byte.toUnsignedInt(sub[20]) * 256 + Byte.toUnsignedInt(sub[21])); + int srTemp = (int) Byte.toUnsignedInt(sub[22]) * 16777216 + Byte.toUnsignedInt(sub[23]) * 65536 + Byte.toUnsignedInt(sub[24]) * 256 + Byte.toUnsignedInt(sub[25]); + svSR.set(itera, srTemp == 0 ? 44100 : srTemp); + if (channels == 2) + { + svIDB.set(itera, (int)sub[33]); + svSizeB.set(itera, (int) Byte.toUnsignedInt(sub[34]) * 16777216 + Byte.toUnsignedInt(sub[35]) * 65536 + Byte.toUnsignedInt(sub[36]) * 256 + Byte.toUnsignedInt(sub[37])); + } + byte[] nameBuff = Arrays.copyOfRange(sub,42,58); + if (nameBuff[0] == 0x00) + { + String tempName = this.name.concat("_").concat(String.valueOf(itera + 1)); + svName.set(itera, tempName); + } + else + { + for (int stringIt = 0; stringIt < 16; stringIt++) + { + if (nameBuff[stringIt] == 0x00) + break; + byte[] tempBytes = new byte[1]; + tempBytes[0] = (byte)(nameBuff[stringIt] & 0x7F); + svName.set(itera, svName.get(itera).concat(new String(tempBytes, "UTF-8"))); + } + } + int subStart = fileSeeker + 256; + byte[] subFuncBuff = Arrays.copyOfRange(inBytes, subStart, subStart + 2); + while (Byte.toUnsignedInt(subFuncBuff[1]) > 2 && subFuncBuff[1] != 11) + { + int subEntrySize = Byte.toUnsignedInt(subFuncBuff[1]); + subStart += 2; + subFuncBuff = Arrays.copyOfRange(inBytes, subStart, subStart + subEntrySize); + switch(subFuncBuff[0]) + { + case 9: + { + if (subFuncBuff[2] == 24 && subFuncBuff[3] == 1) + { + svTune.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + } + if (subFuncBuff[2] == 29) + { + svLoop.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); + } + if (subFuncBuff[2] == 42) + { + svReleaseLoop.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); + } + break; + } + case 13: + { + svWordA.set(itera, (int)subFuncBuff[3]); + svStartA.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[4]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[5]) * 65536 + Byte.toUnsignedInt(subFuncBuff[6]) * 256 + Byte.toUnsignedInt(subFuncBuff[7])); + svEndA.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[8]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[9]) * 65536 + Byte.toUnsignedInt(subFuncBuff[10]) * 256 + Byte.toUnsignedInt(subFuncBuff[11])); + svLSA.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[12]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[13]) * 65536 + Byte.toUnsignedInt(subFuncBuff[14]) * 256 + Byte.toUnsignedInt(subFuncBuff[15])); + svLEA.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[16]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[17]) * 65536 + Byte.toUnsignedInt(subFuncBuff[18]) * 256 + Byte.toUnsignedInt(subFuncBuff[19])); + break; + } + case 18: + { + svWordB.set(itera, (int)subFuncBuff[3]); + svStartB.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[4]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[5]) * 65536 + Byte.toUnsignedInt(subFuncBuff[6]) * 256 + Byte.toUnsignedInt(subFuncBuff[7])); + svEndB.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[8]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[9]) * 65536 + Byte.toUnsignedInt(subFuncBuff[10]) * 256 + Byte.toUnsignedInt(subFuncBuff[11])); + svLSB.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[12]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[13]) * 65536 + Byte.toUnsignedInt(subFuncBuff[14]) * 256 + Byte.toUnsignedInt(subFuncBuff[15])); + svLEB.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[16]) * 16777216 + Byte.toUnsignedInt(subFuncBuff[17]) * 65536 + Byte.toUnsignedInt(subFuncBuff[18]) * 256 + Byte.toUnsignedInt(subFuncBuff[19])); + break; + } + default: + { + break; + } + } + subStart += subEntrySize; + subFuncBuff = Arrays.copyOfRange(inBytes, subStart, subStart + 2); + } + + if (channels == 2 && svWordA.get(itera) == svWordB.get(itera)) + { + if (svStartA.get(itera) - svStartB.get(itera) == 0) + { + if (svEndA.get(itera) - svEndB.get(itera) == 0) + { + if (svLSA.get(itera) - svLSB.get(itera) == 0) + { + if (svLEA.get(itera) - svLEB.get(itera) == 0) + { + svIL.set(itera, true); + } + else + { + this.notifier.logText("K"); + this.notifier.logText("\n"); + } + } + else + { + this.notifier.logText("C"); + this.notifier.logText("\n"); + } + } + else + { + this.notifier.logText("U"); + this.notifier.logText("\n"); + } + } + else + { + this.notifier.logText("F"); + this.notifier.logText("\n"); + } + } + + byte [] data = null; + byte [] data2 = null; + fileSeeker = zoneOffset.get((svIDA).indexOf(svIDA.get(itera))) + 2304; + byte[] sampleBuff1 = Arrays.copyOfRange(inBytes, fileSeeker, fileSeeker + svSizeA.get(itera)); + if (channels == 2 && svIL.get(itera) == true) + { + fileSeeker2 = fileSeeker + svSizeA.get(itera); + if ((svIDB.get(itera) & 127) != svIDA.get(itera)) + { + fileSeeker2 = zoneOffset.get(svIDB.get((svIDB).indexOf(svIDB.get(itera)))) + 2304; + if (svIDB.get((svIDB).indexOf(svIDB.get(itera))) < 0) + fileSeeker2 += svSizeA.get(itera); + } + this.notifier.logText(Integer.toString(fileSeeker2)); + this.notifier.logText("\n"); + byte[] sampleBuff2 = Arrays.copyOfRange(inBytes, fileSeeker2, fileSeeker2 + svSizeA.get(itera)); + + data = new byte[svSizeA.get(itera) * 2]; + + for (int dataCount = 0; dataCount < svSizeA.get(itera) / 2; dataCount++) + { + data[dataCount * 4 + 0] = sampleBuff1[dataCount * 2 + 0]; + data[dataCount * 4 + 1] = sampleBuff1[dataCount * 2 + 1]; + data[dataCount * 4 + 2] = sampleBuff2[dataCount * 2 + 0]; + data[dataCount * 4 + 3] = sampleBuff2[dataCount * 2 + 1]; + } + } + else + { + data = sampleBuff1; + } + + + if (svBR.get(itera) == 16) + flipBytes(data); + else + flipBits(data); + + audioMetadata[itera] = ( (new DefaultAudioMetadata ((svIL.get(itera) == true ? 2 : 1), svSR.get(itera), svBR.get(itera), svSizeA.get(itera) / 2))); + sampleData[itera] = ( (new InMemorySampleData (audioMetadata[itera], data))); + + if (channels == 2 && svIL.get(itera) == false) + { + + if (svIDB.get(itera) % 128 != svIDA.get(itera)) + { + fileSeeker2 = zoneOffset.get((svIDB).indexOf(svIDB.get(itera))) + 2304; + if (svIDB.get((svIDB).indexOf(svIDB.get(itera))) > 127 || svIDB.get((svIDB).indexOf(svIDB.get(itera))) < 0) + { + fileSeeker2 += svSizeA.get(itera); + } + } + else + { + fileSeeker2 = zoneOffset.get((svIDB).indexOf(svIDB.get(itera))) + 2304; + } + byte[] sampleBuff2 = Arrays.copyOfRange(inBytes, fileSeeker2, fileSeeker2 + svSizeA.get(itera)); + data2 = sampleBuff2; + flipBytes(data2); + audioMetadataR[itera] = ( (new DefaultAudioMetadata ((svIL.get(itera) == true ? 2 : 1), svSR.get(itera), svBR.get(itera), svSizeB.get(itera) / 2))); + sampleDataR[itera] = ( (new InMemorySampleData (audioMetadata[itera], data2))); + } + } + byte[] mappingInfo = Arrays.copyOfRange(inBytes, mappingOffset, mappingOffset + 128); + int prevK = -1; + int curr = 0; + for (int key = 0; key < 128; key++) + { + if (Byte.toUnsignedInt(mappingInfo[key]) <= curr || mappingInfo[key] > numSubVoices) + { + continue; + } + if (Byte.toUnsignedInt(mappingInfo[key]) == prevK) + { + continue; + } + if (subvoiceID.indexOf(Byte.toUnsignedInt(mappingInfo[key])) == -1) + { + continue; + } + + DefaultSampleZone newZone = new DefaultSampleZone(); + newZone.setKeyLow(key); + for (int key2 = key; key2 < 128; key2++) + { + if (Byte.toUnsignedInt(mappingInfo[key]) != Byte.toUnsignedInt(mappingInfo[key2])) + { + newZone.setKeyHigh(key2 - 1); + break; + } + } + int firstID = subvoiceID.indexOf(Byte.toUnsignedInt(mappingInfo[key])); + newZone.setName(svName.get(firstID)); + newZone.setSampleData(sampleData[firstID]); + if (svTune.get(firstID) == -1) + { + newZone.setKeyTracking(0); + newZone.setKeyRoot(60); + } + else + { + newZone.setKeyTracking(1); + newZone.setKeyRoot((int)Math.round(pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)))); + newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot()) * 100); + } + if (svLoop.get(firstID) == true) + { + DefaultSampleLoop loop = new DefaultSampleLoop (); + loop.setStart (svLSA.get(firstID)); + loop.setEnd (svLEA.get(firstID)); + newZone.addLoop(loop); + } + group.addSampleZone(newZone); + if (channels == 2 && svIL.get(firstID) == false) + { + DefaultSampleZone newZone2 = new DefaultSampleZone(); + newZone2.setKeyLow(key); + for (int key2 = key; key2 < 128; key2++) + { + if (Byte.toUnsignedInt(mappingInfo[key]) != Byte.toUnsignedInt(mappingInfo[key2])) + { + newZone2.setKeyHigh(key2 - 1); + break; + } + } + int secondID = subvoiceID.indexOf(Byte.toUnsignedInt(mappingInfo[key])); + newZone2.setName(svName.get(secondID)); + newZone2.setSampleData(sampleDataR[secondID]); + if (svTune.get(secondID) == -1) + { + newZone2.setKeyTracking(0); + newZone2.setKeyRoot(60); + } + else + { + newZone2.setKeyTracking(1); + newZone2.setKeyRoot((int)Math.round(pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)))); + newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot()) * 100); + } + if (svLoop.get(secondID) == true) + { + DefaultSampleLoop loop = new DefaultSampleLoop(); + loop.setStart (svLSB.get(secondID)); + loop.setEnd (svLEB.get(secondID)); + newZone2.addLoop(loop); + } + group.addSampleZone(newZone2); + } + prevK = Byte.toUnsignedInt(mappingInfo[key]); + } + } + final String [] parts = AudioFileUtils.createPathParts (sourceFile.getParentFile (), sourceFile.getParentFile (), this.name); + final DefaultMultisampleSource multisampleSource = new DefaultMultisampleSource (sourceFile, parts, this.name, AudioFileUtils.subtractPaths (sourceFile.getParentFile (), sourceFile)); + final IMetadata metadata = multisampleSource.getMetadata(); + try + { + final BasicFileAttributes attrs = Files.readAttributes (sourceFile.toPath (), BasicFileAttributes.class); + final FileTime creationTime = attrs.creationTime (); + final FileTime modifiedTime = attrs.lastModifiedTime (); + final long creationTimeMillis = creationTime.toMillis (); + final long modifiedTimeMillis = modifiedTime.toMillis (); + metadata.setCreationDateTime (new Date (creationTimeMillis < modifiedTimeMillis ? creationTimeMillis : modifiedTimeMillis)); + } + catch (final IOException ex) + { + metadata.setCreationDateTime (new Date ()); + } + if (!group.getSampleZones ().isEmpty ()) + { + groups.add(group); + } + multisampleSource.setGroups(groups); + return Collections.singletonList(multisampleSource); + } + + private double pitchConvert(final int inV, final int gV, final int srV) + { + int outV = inV; + if (outV >= 16384) + outV -= 32768; + int outGV = gV; + if (outGV >= 16384) + outGV -= 32768; + double sr0 = 3072 * Math.log(srV / 44100) / Math.log(2); + return ((-outV - outGV + sr0) / 256 + 65) % 128; + } + + // Flip MSB / LSB + private static void flipBytes (final byte [] data) + { + for (int i = 0; i < data.length; i += 2) + { + byte temp = data[i]; + data[i] = data[i + 1]; + data[i + 1] = temp; + } + } + + // Flip MSB / LSB + private static void flipBits (final byte [] data) + { + for (int i = 0; i < data.length; i += 2) + { + data[i] = (byte)(data[i] ^ 128); + } + } +} From 61a8f28cb9abe4704a5684b7ab70b19a40240fc7 Mon Sep 17 00:00:00 2001 From: Python Blue <29385655+PythonBlue@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:17:22 -0400 Subject: [PATCH 02/12] Update VCFile.java Fixed tuning issues with read voices --- .../convertwithmoss/format/cmi3/VCFile.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index 72aab809..827de95e 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -438,13 +438,16 @@ public List read (final InputStream inputStream, final File if (svTune.get(firstID) == -1) { newZone.setKeyTracking(0); - newZone.setKeyRoot(60); + newZone.setKeyRoot(65); } else { newZone.setKeyTracking(1); newZone.setKeyRoot((int)Math.round(pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)))); - newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot()) * 100); + newZone.setKeyRoot(newZone.getKeyRoot() < 0 ? newZone.getKeyRoot() + 128 : newZone.getKeyRoot()); + this.notifier.logText(Integer.toString(newZone.getKeyRoot())); + this.notifier.logText("\n"); + newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot())); } if (svLoop.get(firstID) == true) { @@ -478,7 +481,8 @@ public List read (final InputStream inputStream, final File { newZone2.setKeyTracking(1); newZone2.setKeyRoot((int)Math.round(pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)))); - newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot()) * 100); + newZone2.setKeyRoot(newZone2.getKeyRoot() < 0 ? newZone2.getKeyRoot() + 128 : newZone2.getKeyRoot()); + newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot())); } if (svLoop.get(secondID) == true) { From e7d89689aa760e30cff69512299cafaa2ec55a9a Mon Sep 17 00:00:00 2001 From: Python Blue <29385655+PythonBlue@users.noreply.github.com> Date: Fri, 11 Jul 2025 20:23:53 -0400 Subject: [PATCH 03/12] Update VCFile.java Tuning issues finally fixed for good --- .../convertwithmoss/format/cmi3/VCFile.java | 32 ++----------------- 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index 827de95e..ca0cc800 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -152,14 +152,10 @@ public List read (final InputStream inputStream, final File if (header[i * 4 + 259] == 0 || header[i * 4 + 259] < 0) { numSubVoices = i; - this.notifier.logText(Integer.toString(numSubVoices)); - this.notifier.logText("\n"); break; } else { - this.notifier.logText(Integer.toString(Byte.toUnsignedInt(header[i * 4 + 259]))); - this.notifier.logText("\n"); subvoiceID.add(Byte.toUnsignedInt(header[i * 4 + 259])); } } @@ -316,28 +312,8 @@ public List read (final InputStream inputStream, final File { svIL.set(itera, true); } - else - { - this.notifier.logText("K"); - this.notifier.logText("\n"); - } - } - else - { - this.notifier.logText("C"); - this.notifier.logText("\n"); } } - else - { - this.notifier.logText("U"); - this.notifier.logText("\n"); - } - } - else - { - this.notifier.logText("F"); - this.notifier.logText("\n"); } } @@ -354,8 +330,6 @@ public List read (final InputStream inputStream, final File if (svIDB.get((svIDB).indexOf(svIDB.get(itera))) < 0) fileSeeker2 += svSizeA.get(itera); } - this.notifier.logText(Integer.toString(fileSeeker2)); - this.notifier.logText("\n"); byte[] sampleBuff2 = Arrays.copyOfRange(inBytes, fileSeeker2, fileSeeker2 + svSizeA.get(itera)); data = new byte[svSizeA.get(itera) * 2]; @@ -444,10 +418,8 @@ public List read (final InputStream inputStream, final File { newZone.setKeyTracking(1); newZone.setKeyRoot((int)Math.round(pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)))); - newZone.setKeyRoot(newZone.getKeyRoot() < 0 ? newZone.getKeyRoot() + 128 : newZone.getKeyRoot()); - this.notifier.logText(Integer.toString(newZone.getKeyRoot())); - this.notifier.logText("\n"); newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot())); + newZone.setKeyRoot(newZone.getKeyRoot() < 0 ? newZone.getKeyRoot() + 128 : newZone.getKeyRoot()); } if (svLoop.get(firstID) == true) { @@ -481,8 +453,8 @@ public List read (final InputStream inputStream, final File { newZone2.setKeyTracking(1); newZone2.setKeyRoot((int)Math.round(pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)))); - newZone2.setKeyRoot(newZone2.getKeyRoot() < 0 ? newZone2.getKeyRoot() + 128 : newZone2.getKeyRoot()); newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot())); + newZone2.setKeyRoot(newZone2.getKeyRoot() < 0 ? newZone2.getKeyRoot() + 128 : newZone2.getKeyRoot()); } if (svLoop.get(secondID) == true) { From 4207736cbc6e83fae4f08b5f264e353afb05e29a Mon Sep 17 00:00:00 2001 From: Python Blue <29385655+PythonBlue@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:30:41 -0400 Subject: [PATCH 04/12] Update VCFile.java Support for CMI 3 voices now includes approximation of amplitude and envelope parameters. With that, support for all universal sample format parameters may be close to complete (filters would be a hassle due to their being unconditionally keytracked to the pitch of the original sample, among other issues). --- .../convertwithmoss/format/cmi3/VCFile.java | 182 ++++++++++++++++-- 1 file changed, 169 insertions(+), 13 deletions(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index ca0cc800..5211d9cb 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -30,6 +30,7 @@ import de.mossgrabers.convertwithmoss.core.model.IMetadata; import de.mossgrabers.convertwithmoss.core.model.ISampleLoop; import de.mossgrabers.convertwithmoss.core.model.ISampleZone; +import de.mossgrabers.convertwithmoss.core.model.IEnvelope; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultGroup; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleLoop; import de.mossgrabers.convertwithmoss.core.model.implementation.DefaultSampleZone; @@ -136,6 +137,16 @@ public List read (final InputStream inputStream, final File List svIL = new ArrayList(); List svLoop = new ArrayList(); List svReleaseLoop = new ArrayList(); + List svAttackF = new ArrayList(); + List svAttackS = new ArrayList(); + List svHold = new ArrayList(); + List svDecay = new ArrayList(); + List svSustain = new ArrayList(); + List svAmp = new ArrayList(); + List svReleaseF = new ArrayList(); + List svReleaseS = new ArrayList(); + List svAttackX = new ArrayList(); + List svReleaseX = new ArrayList(); for (int iPre = 0; iPre < 1; iPre++) @@ -184,6 +195,16 @@ public List read (final InputStream inputStream, final File svLEB.add(0); svLoop.add(false); svReleaseLoop.add(false); + svAttackF.add(0.0); + svAttackS.add(0.0); + svHold.add(0.0); + svDecay.add(0.0); + svSustain.add(0.0); + svAmp.add(0.0); + svReleaseF.add(0.0); + svReleaseS.add(0.0); + svAttackX.add(false); + svReleaseX.add(false); svIL.add(false); } @@ -203,9 +224,16 @@ public List read (final InputStream inputStream, final File } case 9: { - if (headFuncBuff[2] == 24 && headFuncBuff[3] == 1) + switch(headFuncBuff[2]) { - voiceTune = (int) Byte.toUnsignedInt(headFuncBuff[4]) * 256 + Byte.toUnsignedInt(headFuncBuff[5]); + case 24: + { + voiceTune = (int) Byte.toUnsignedInt(headFuncBuff[4]) * 256 + Byte.toUnsignedInt(headFuncBuff[5]); + } + default: + { + break; + } } break; } @@ -259,17 +287,95 @@ public List read (final InputStream inputStream, final File { case 9: { - if (subFuncBuff[2] == 24 && subFuncBuff[3] == 1) - { - svTune.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); - } - if (subFuncBuff[2] == 29) - { - svLoop.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); - } - if (subFuncBuff[2] == 42) + switch(subFuncBuff[2]) { - svReleaseLoop.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); + case 5: + { + svAttackF.set(itera, (double) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + if (svAttackF.get(itera) > 32767) + svAttackF.set(itera, 65536 - svAttackF.get(itera)); + svAttackF.set(itera, svAttackF.get(itera) / 4096); + break; + } + case 6: + { + svHold.set(itera, (double) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + if (svHold.get(itera) > 32767) + svHold.set(itera, 65536 - svHold.get(itera)); + svHold.set(itera, svHold.get(itera) / 4096); + break; + } + case 7: + { + svDecay.set(itera, (double) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + if (svDecay.get(itera) > 32767) + svDecay.set(itera, 65536 - svDecay.get(itera)); + svDecay.set(itera, svDecay.get(itera) / 2048); + break; + } + case 8: + { + svSustain.set(itera, (double) levelConvert(Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5]))); + break; + } + case 9: + { + svAmp.set(itera, (double) levelConvertDB(Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5]))); + break; + } + case 10: + { + svReleaseF.set(itera, (double) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + if (svReleaseF.get(itera) > 32767) + svReleaseF.set(itera, 65536 - svReleaseF.get(itera)); + svReleaseF.set(itera, svReleaseF.get(itera) / 2048); + break; + } + case 16: + { + svAttackS.set(itera, (double) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + if (svAttackS.get(itera) > 32767) + svAttackS.set(itera, 65536 - svAttackS.get(itera)); + svAttackS.set(itera, svAttackS.get(itera) / 4096); + break; + } + case 17: + { + svReleaseS.set(itera, (double) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + if (svReleaseS.get(itera) > 32767) + svReleaseS.set(itera, 65536 - svReleaseS.get(itera)); + svReleaseS.set(itera, svReleaseS.get(itera) / 2048); + break; + } + case 24: + { + svTune.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[4]) * 256 + Byte.toUnsignedInt(subFuncBuff[5])); + break; + } + case 27: + { + svAttackX.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); + break; + } + case 28: + { + svReleaseX.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); + break; + } + case 29: + { + svLoop.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); + break; + } + case 42: + { + svReleaseLoop.set(itera, (int) Byte.toUnsignedInt(subFuncBuff[3]) > 127 ? true : false); + break; + } + default: + { + break; + } } break; } @@ -428,10 +534,26 @@ public List read (final InputStream inputStream, final File loop.setEnd (svLEA.get(firstID)); newZone.addLoop(loop); } - group.addSampleZone(newZone); + + newZone.setGain(svAmp.get(firstID)); + final IEnvelope amplitudeEnvelope = newZone.getAmplitudeEnvelopeModulator ().getSource (); + if (svAttackX.get(firstID) == true) + amplitudeEnvelope.setAttackTime(svAttackS.get(firstID)); + else + amplitudeEnvelope.setAttackTime(svAttackF.get(firstID)); + amplitudeEnvelope.setHoldTime(svHold.get(firstID)); + amplitudeEnvelope.setDecayTime(svDecay.get(firstID)); + amplitudeEnvelope.setSustainLevel(svSustain.get(firstID)); + if (svReleaseX.get(firstID) == true) + amplitudeEnvelope.setReleaseTime(svReleaseS.get(firstID)); + else + amplitudeEnvelope.setReleaseTime(svReleaseF.get(firstID)); + if (channels == 2 && svIL.get(firstID) == false) { + newZone.setPanning(-1); DefaultSampleZone newZone2 = new DefaultSampleZone(); + newZone2.setPanning(1); newZone2.setKeyLow(key); for (int key2 = key; key2 < 128; key2++) { @@ -463,8 +585,24 @@ public List read (final InputStream inputStream, final File loop.setEnd (svLEB.get(secondID)); newZone2.addLoop(loop); } + + newZone2.setGain(svAmp.get(secondID)); + final IEnvelope amplitudeEnvelope2 = newZone2.getAmplitudeEnvelopeModulator ().getSource (); + if (svAttackX.get(secondID) == true) + amplitudeEnvelope2.setAttackTime(svAttackS.get(secondID)); + else + amplitudeEnvelope2.setAttackTime(svAttackF.get(secondID)); + amplitudeEnvelope2.setHoldTime(svHold.get(secondID)); + amplitudeEnvelope2.setDecayTime(svDecay.get(secondID)); + amplitudeEnvelope2.setSustainLevel(svSustain.get(secondID)); + if (svReleaseX.get(secondID) == true) + amplitudeEnvelope2.setReleaseTime(svReleaseS.get(secondID)); + else + amplitudeEnvelope2.setReleaseTime(svReleaseF.get(secondID)); + group.addSampleZone(newZone2); } + group.addSampleZone(newZone); prevK = Byte.toUnsignedInt(mappingInfo[key]); } } @@ -503,6 +641,24 @@ private double pitchConvert(final int inV, final int gV, final int srV) double sr0 = 3072 * Math.log(srV / 44100) / Math.log(2); return ((-outV - outGV + sr0) / 256 + 65) % 128; } + + private double levelConvert(final int inV) + { + if (inV == 0) + return 1; + double outV = (double) inV; + if (outV >= 32768) + outV -= 65536; + return Math.max(0, 1.01 - Math.pow(10, outV / 256) / 100); + } + + private double levelConvertDB(final int inV) + { + double outV = inV; + if (outV >= 32768) + outV -= 65536; + return outV / 512; + } // Flip MSB / LSB private static void flipBytes (final byte [] data) From e3a79d33dc278c482f1af5d47c98d60da3f396c7 Mon Sep 17 00:00:00 2001 From: Python Blue <29385655+PythonBlue@users.noreply.github.com> Date: Wed, 10 Sep 2025 22:39:28 -0400 Subject: [PATCH 05/12] Update VCFile.java Fixes for channel count, sample naming, and pitch detection for atypical sample rates. --- .../convertwithmoss/format/cmi3/VCFile.java | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index 5211d9cb..e4135693 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -156,7 +156,7 @@ public List read (final InputStream inputStream, final File checkDone++; else throw new ParseException (Functions.getMessage ("IDS_NOTIFY_ERR_ILLEGAL_CHARACTER")); - channels = Byte.toUnsignedInt(header[16]) > 127 ? 2 : 1; + channels = Byte.toUnsignedInt(header[16]) >= 127 ? 2 : 1; voiceFunctionCount = (int)header[19]; for (int i = 0; i < 128; i++) { @@ -275,7 +275,8 @@ public List read (final InputStream inputStream, final File tempBytes[0] = (byte)(nameBuff[stringIt] & 0x7F); svName.set(itera, svName.get(itera).concat(new String(tempBytes, "UTF-8"))); } - } + } + svName.set(itera, svName.get(itera).concat("_").concat(String.format("%03d", itera))); int subStart = fileSeeker + 256; byte[] subFuncBuff = Arrays.copyOfRange(inBytes, subStart, subStart + 2); while (Byte.toUnsignedInt(subFuncBuff[1]) > 2 && subFuncBuff[1] != 11) @@ -524,7 +525,7 @@ public List read (final InputStream inputStream, final File { newZone.setKeyTracking(1); newZone.setKeyRoot((int)Math.round(pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)))); - newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot())); + newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot()) / -100.0); newZone.setKeyRoot(newZone.getKeyRoot() < 0 ? newZone.getKeyRoot() + 128 : newZone.getKeyRoot()); } if (svLoop.get(firstID) == true) @@ -551,7 +552,8 @@ public List read (final InputStream inputStream, final File if (channels == 2 && svIL.get(firstID) == false) { - newZone.setPanning(-1); + newZone.setPanning(-1); + newZone.setName(newZone.getName().concat("_L")); DefaultSampleZone newZone2 = new DefaultSampleZone(); newZone2.setPanning(1); newZone2.setKeyLow(key); @@ -564,7 +566,7 @@ public List read (final InputStream inputStream, final File } } int secondID = subvoiceID.indexOf(Byte.toUnsignedInt(mappingInfo[key])); - newZone2.setName(svName.get(secondID)); + newZone2.setName(svName.get(secondID).concat("_R")); newZone2.setSampleData(sampleDataR[secondID]); if (svTune.get(secondID) == -1) { @@ -575,7 +577,7 @@ public List read (final InputStream inputStream, final File { newZone2.setKeyTracking(1); newZone2.setKeyRoot((int)Math.round(pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)))); - newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot())); + newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot()) / -100.0); newZone2.setKeyRoot(newZone2.getKeyRoot() < 0 ? newZone2.getKeyRoot() + 128 : newZone2.getKeyRoot()); } if (svLoop.get(secondID) == true) @@ -638,8 +640,9 @@ private double pitchConvert(final int inV, final int gV, final int srV) int outGV = gV; if (outGV >= 16384) outGV -= 32768; - double sr0 = 3072 * Math.log(srV / 44100) / Math.log(2); - return ((-outV - outGV + sr0) / 256 + 65) % 128; + double sr0 = Math.log((double)(srV) / 44100.0) / Math.log(2); +this.notifier.logText(String.valueOf(sr0)); + return ((-outV - outGV) / 256.0 + (sr0 * 12) + 65) % 128; } private double levelConvert(final int inV) From b8ea1d13e0e4dc2c577202acb28899c45b0b4eb2 Mon Sep 17 00:00:00 2001 From: Python Blue <29385655+PythonBlue@users.noreply.github.com> Date: Thu, 11 Sep 2025 15:46:05 -0400 Subject: [PATCH 06/12] Update VCFile.java Disable extra debug logging for CMI voice reading --- .../de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index e4135693..a76824d2 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -640,8 +640,7 @@ private double pitchConvert(final int inV, final int gV, final int srV) int outGV = gV; if (outGV >= 16384) outGV -= 32768; - double sr0 = Math.log((double)(srV) / 44100.0) / Math.log(2); -this.notifier.logText(String.valueOf(sr0)); + double sr0 = Math.log((double)(srV) / 44100.0) / Math.log(2); return ((-outV - outGV) / 256.0 + (sr0 * 12) + 65) % 128; } From a99bbdc1a72f3e9fe0799a92f71c5c8423fac992 Mon Sep 17 00:00:00 2001 From: Python Blue <29385655+PythonBlue@users.noreply.github.com> Date: Thu, 11 Sep 2025 16:03:24 -0400 Subject: [PATCH 07/12] Update VCFile.java Minor fine tune fix --- .../java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index a76824d2..69586c53 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -640,7 +640,7 @@ private double pitchConvert(final int inV, final int gV, final int srV) int outGV = gV; if (outGV >= 16384) outGV -= 32768; - double sr0 = Math.log((double)(srV) / 44100.0) / Math.log(2); + double sr0 = Math.log((double)(srV) / 44701.0) / Math.log(2); return ((-outV - outGV) / 256.0 + (sr0 * 12) + 65) % 128; } From 1ddf9de7744bceb2d57a623dfb95b23775b2ea66 Mon Sep 17 00:00:00 2001 From: Python Blue Date: Tue, 24 Mar 2026 10:19:12 -0400 Subject: [PATCH 08/12] Updates Updates for main repo commit compatibility --- .../convertwithmoss/core/ConverterBackend.java | 2 ++ .../mossgrabers/convertwithmoss/format/cmi3/VCFile.java | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java b/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java index ab68e809..af5e927c 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java @@ -46,6 +46,7 @@ import de.mossgrabers.convertwithmoss.format.kmp.KMPDetector; import de.mossgrabers.convertwithmoss.format.korgmultisample.KorgmultisampleCreator; import de.mossgrabers.convertwithmoss.format.korgmultisample.KorgmultisampleDetector; +import de.mossgrabers.convertwithmoss.format.cmi3.VCDetector; import de.mossgrabers.convertwithmoss.format.music1010.bento.BentoCreator; import de.mossgrabers.convertwithmoss.format.music1010.bento.BentoDetector; import de.mossgrabers.convertwithmoss.format.music1010.blackbox.Music1010Creator; @@ -107,6 +108,7 @@ public ConverterBackend (final INotifier notifier) this.detectors = new IDetector [] { + new VCDetector (notifier), new BentoDetector (notifier), new Music1010Detector (notifier), new AbletonDetector (notifier), diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index 69586c53..c8a0935b 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -275,7 +275,7 @@ public List read (final InputStream inputStream, final File tempBytes[0] = (byte)(nameBuff[stringIt] & 0x7F); svName.set(itera, svName.get(itera).concat(new String(tempBytes, "UTF-8"))); } - } + } svName.set(itera, svName.get(itera).concat("_").concat(String.format("%03d", itera))); int subStart = fileSeeker + 256; byte[] subFuncBuff = Arrays.copyOfRange(inBytes, subStart, subStart + 2); @@ -525,7 +525,7 @@ public List read (final InputStream inputStream, final File { newZone.setKeyTracking(1); newZone.setKeyRoot((int)Math.round(pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)))); - newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot()) / -100.0); + newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot()) / -1.0); newZone.setKeyRoot(newZone.getKeyRoot() < 0 ? newZone.getKeyRoot() + 128 : newZone.getKeyRoot()); } if (svLoop.get(firstID) == true) @@ -552,7 +552,7 @@ public List read (final InputStream inputStream, final File if (channels == 2 && svIL.get(firstID) == false) { - newZone.setPanning(-1); + newZone.setPanning(-1); newZone.setName(newZone.getName().concat("_L")); DefaultSampleZone newZone2 = new DefaultSampleZone(); newZone2.setPanning(1); @@ -577,7 +577,7 @@ public List read (final InputStream inputStream, final File { newZone2.setKeyTracking(1); newZone2.setKeyRoot((int)Math.round(pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)))); - newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot()) / -100.0); + newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot()) / -1.0); newZone2.setKeyRoot(newZone2.getKeyRoot() < 0 ? newZone2.getKeyRoot() + 128 : newZone2.getKeyRoot()); } if (svLoop.get(secondID) == true) From ec6b2448f5597cdabeb8afa2b11f8dbf4d11d070 Mon Sep 17 00:00:00 2001 From: Python Blue Date: Tue, 24 Mar 2026 10:26:26 -0400 Subject: [PATCH 09/12] Update VCFile.java Compilation fix for the tuning functions --- .../de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java index c8a0935b..427777ec 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/cmi3/VCFile.java @@ -525,7 +525,7 @@ public List read (final InputStream inputStream, final File { newZone.setKeyTracking(1); newZone.setKeyRoot((int)Math.round(pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)))); - newZone.setTune((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot()) / -1.0); + newZone.setTuning((pitchConvert(svTune.get(firstID), voiceTune, svSR.get(firstID)) - newZone.getKeyRoot()) / -1.0); newZone.setKeyRoot(newZone.getKeyRoot() < 0 ? newZone.getKeyRoot() + 128 : newZone.getKeyRoot()); } if (svLoop.get(firstID) == true) @@ -577,7 +577,7 @@ public List read (final InputStream inputStream, final File { newZone2.setKeyTracking(1); newZone2.setKeyRoot((int)Math.round(pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)))); - newZone2.setTune((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot()) / -1.0); + newZone2.setTuning((pitchConvert(svTune.get(secondID), voiceTune, svSR.get(secondID)) - newZone2.getKeyRoot()) / -1.0); newZone2.setKeyRoot(newZone2.getKeyRoot() < 0 ? newZone2.getKeyRoot() + 128 : newZone2.getKeyRoot()); } if (svLoop.get(secondID) == true) From 9f04c411cf79bc1b588d54c19ccba335a722cb42 Mon Sep 17 00:00:00 2001 From: Python Blue Date: Tue, 24 Mar 2026 10:29:23 -0400 Subject: [PATCH 10/12] Update ConverterBackend.java Reorganized for CMI 3 voices to be in right place --- .../de/mossgrabers/convertwithmoss/core/ConverterBackend.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java b/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java index af5e927c..956796d0 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/core/ConverterBackend.java @@ -108,7 +108,6 @@ public ConverterBackend (final INotifier notifier) this.detectors = new IDetector [] { - new VCDetector (notifier), new BentoDetector (notifier), new Music1010Detector (notifier), new AbletonDetector (notifier), @@ -121,6 +120,7 @@ public ConverterBackend (final INotifier notifier) new TX16WxDetector (notifier), new DecentSamplerDetector (notifier), new DistingExDetector (notifier), + new VCDetector (notifier), new IsoDetector (notifier), new KontaktDetector (notifier), new KMPDetector (notifier), From 10a3bd3b5bf1521bb74714fd9d512e9849e7176a Mon Sep 17 00:00:00 2001 From: Python Blue Date: Wed, 25 Mar 2026 09:39:41 -0400 Subject: [PATCH 11/12] Update README-FORMATS.md Update documentation at last to mention the CMI format --- documentation/README-FORMATS.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/documentation/README-FORMATS.md b/documentation/README-FORMATS.md index 725b444a..1deb0fa7 100644 --- a/documentation/README-FORMATS.md +++ b/documentation/README-FORMATS.md @@ -44,6 +44,7 @@ The following multi-sample formats are supported: * [DecentSampler](#decentsampler) * [discoDSP Bliss](#discodsp-bliss) * [Expert Sleepers disting EX](#expert-sleepers-disting-ex) +* [Fairlight CMI 3](#kontakt-nkinkm) * [Kontakt NKI/NKM](#kontakt-nkinkm) * [Korg KSC/KMP/KSF](#korg-ksckmpksf) * [Korg wavestate/modwave](#korg-wavestatemodwave) @@ -216,6 +217,12 @@ The basic multi-sample setup is encoded in the file-names of the samples. Furthe * Re-sample to 16bit/44.1kHz: If enabled, samples 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. * Trim sample to range of zone start to end: Since the format does not support a sample start attribute, this fixes the issue. +## Fairlight CMI 3 + +We've all heard stories about and uses of the Fairlight CMI IIx and earlier. The Series III was one of the first 16-bit samplers, second only to the Synclavier. Extensive reverse engineering effort of the self-contained voice format was applied to make it available as a source format, but unfortunately, current constraints combined with the strict third party user community makes it unfeasible to use as a destination format at this time. + +Note that this will not work with IIx or earlier voices despite the same VC extension used. + ## Kontakt NKI/NKM Kontakt is a sampler from Native Instruments which uses a plethora of file formats which all are sadly proprietary and therefore no documentation is publicly available. Nevertheless, several people analyzed the format and by now sufficient information is available to provide the support as the source. From 92ab94f313fded1d73615bd48aa6d7040e1fd1d8 Mon Sep 17 00:00:00 2001 From: Python Blue Date: Wed, 25 Mar 2026 09:44:27 -0400 Subject: [PATCH 12/12] Update README-FORMATS.md --- documentation/README-FORMATS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/documentation/README-FORMATS.md b/documentation/README-FORMATS.md index 1deb0fa7..7b985d78 100644 --- a/documentation/README-FORMATS.md +++ b/documentation/README-FORMATS.md @@ -223,6 +223,8 @@ We've all heard stories about and uses of the Fairlight CMI IIx and earlier. The Note that this will not work with IIx or earlier voices despite the same VC extension used. +Filter parameters are currently not supported, but then, as a variable clock DAC sampler, the original hardware had no interpolation anyway. + ## Kontakt NKI/NKM Kontakt is a sampler from Native Instruments which uses a plethora of file formats which all are sadly proprietary and therefore no documentation is publicly available. Nevertheless, several people analyzed the format and by now sufficient information is available to provide the support as the source.