-
Notifications
You must be signed in to change notification settings - Fork 22
Adding support for Yamaha MOXF - X3A files #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,3 +4,7 @@ | |
/.classpath | ||
/.project | ||
/.DS_Store | ||
|
||
/.idea | ||
*.iml | ||
*.ipr |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package de.mossgrabers.convertwithmoss.format.yamaha.ysfc; | ||
|
||
import de.mossgrabers.convertwithmoss.file.StreamUtils; | ||
|
||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
|
||
public class YamahaMOXFEWFMEntry extends YamahaYsfcEntry { | ||
|
||
@Override | ||
protected byte[] createContent() throws IOException { | ||
final ByteArrayOutputStream contentStream = new ByteArrayOutputStream (); | ||
|
||
StreamUtils.padBytes(contentStream, 4); | ||
|
||
// Size of the item corresponding to this entry | ||
StreamUtils.writeUnsigned32(contentStream, this.correspondingDataSize, true); | ||
StreamUtils.padBytes(contentStream, 4); | ||
// Offset of the item chunk within the data block | ||
StreamUtils.writeUnsigned32(contentStream, this.correspondingDataOffset, true); | ||
// Type specific - e.g. Program number | ||
StreamUtils.writeUnsigned32(contentStream, this.specificValue, true); | ||
|
||
StreamUtils.padBytes(contentStream, 2); | ||
|
||
StreamUtils.writeNullTerminatedASCII (contentStream, this.itemName); | ||
StreamUtils.writeNullTerminatedASCII (contentStream, this.itemTitle); | ||
|
||
// Optional additional data - type specific, only used by EPFM | ||
contentStream.write (this.additionalData); | ||
|
||
// Finally, write the chunk | ||
final byte [] content = contentStream.toByteArray (); | ||
this.length = content.length; | ||
return content; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package de.mossgrabers.convertwithmoss.format.yamaha.ysfc; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above: To not pollute the main code with changes to this block, the entry for the section was subclassed. Not an ideal solution from theoretical point of view, but works. |
||
|
||
import de.mossgrabers.convertwithmoss.file.StreamUtils; | ||
|
||
import java.io.ByteArrayOutputStream; | ||
import java.io.IOException; | ||
|
||
public class YamahaMOXFEWIMEntry extends YamahaYsfcEntry { | ||
|
||
@Override | ||
protected byte[] createContent() throws IOException { | ||
final ByteArrayOutputStream contentStream = new ByteArrayOutputStream (); | ||
|
||
StreamUtils.padBytes(contentStream, 4); | ||
|
||
// Size of the item corresponding to this entry | ||
StreamUtils.writeUnsigned32 (contentStream, this.correspondingDataSize, true); | ||
|
||
StreamUtils.padBytes(contentStream, 4); | ||
|
||
// Offset of the item chunk within the data block | ||
StreamUtils.writeUnsigned32 (contentStream, this.correspondingDataOffset, true); | ||
// Type specific - e.g. Program number | ||
StreamUtils.writeUnsigned32 (contentStream, this.specificValue, true); | ||
|
||
// Flags - type specific | ||
// contentStream.write (this.flags); | ||
|
||
// ID of the entry object for ordering | ||
// StreamUtils.writeUnsigned32 (contentStream, this.entryID, true); | ||
|
||
StreamUtils.padBytes(contentStream, 2); | ||
|
||
StreamUtils.writeNullTerminatedASCII (contentStream, this.itemName); | ||
StreamUtils.writeNullTerminatedASCII (contentStream, this.itemTitle); | ||
|
||
// Optional additional data - type specific, only used by EPFM | ||
contentStream.write (this.additionalData); | ||
|
||
// Finally, write the chunk | ||
final byte [] content = contentStream.toByteArray (); | ||
this.length = content.length; | ||
return content; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,11 +7,7 @@ | |
import java.io.File; | ||
import java.io.FileOutputStream; | ||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.EnumMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.*; | ||
|
||
import de.mossgrabers.convertwithmoss.core.IMultisampleSource; | ||
import de.mossgrabers.convertwithmoss.core.INotifier; | ||
|
@@ -35,6 +31,8 @@ | |
import javafx.scene.control.ToggleGroup; | ||
|
||
|
||
record MOXFData(int numberOfSamplesWritten, int numberOfChannelsWritten) {} | ||
|
||
/** | ||
* Creator for Yamaha YSFC files. | ||
* | ||
|
@@ -55,7 +53,8 @@ private enum OutputFormat | |
MONTAGE_USER, | ||
MONTAGE_LIBRARY, | ||
MODX_USER, | ||
MODX_LIBRARY | ||
MODX_LIBRARY, | ||
MOXF_LIBRARY | ||
} | ||
|
||
|
||
|
@@ -68,11 +67,13 @@ private enum OutputFormat | |
ENDING_MAP.put (OutputFormat.MONTAGE_LIBRARY, ".X7L"); | ||
ENDING_MAP.put (OutputFormat.MODX_USER, ".X8U"); | ||
ENDING_MAP.put (OutputFormat.MODX_LIBRARY, ".X8L"); | ||
ENDING_MAP.put (OutputFormat.MOXF_LIBRARY, ".X3A"); | ||
|
||
VERSION_MAP.put (OutputFormat.MONTAGE_USER, "4.0.5"); | ||
VERSION_MAP.put (OutputFormat.MONTAGE_LIBRARY, "4.0.5"); | ||
VERSION_MAP.put (OutputFormat.MODX_USER, "5.0.1"); | ||
VERSION_MAP.put (OutputFormat.MODX_LIBRARY, "5.0.1"); | ||
VERSION_MAP.put (OutputFormat.MOXF_LIBRARY, "1.0.2"); | ||
} | ||
|
||
private ToggleGroup outputFormatGroup; | ||
|
@@ -97,7 +98,7 @@ public Node getEditPane () | |
|
||
panel.createSeparator ("@IDS_YSFC_LIBRARY_FORMAT"); | ||
this.outputFormatGroup = new ToggleGroup (); | ||
for (int i = 0; i < 4; i++) | ||
for (int i = 0; i < 5; i++) | ||
{ | ||
final RadioButton order = panel.createRadioButton ("@IDS_YSFC_OUTPUT_FORMAT_OPTION" + i); | ||
order.setAccessibleHelp (Functions.getMessage ("IDS_YSFC_LIBRARY_FORMAT")); | ||
|
@@ -173,11 +174,29 @@ public void create (final File destinationFolder, final List<IMultisampleSource> | |
*/ | ||
private static void storeMultisamples (final List<IMultisampleSource> multisampleSources, final File multiFile, final OutputFormat outputFormat) throws IOException | ||
{ | ||
final YsfcFile ysfcFile = new YsfcFile (); | ||
final YsfcFile ysfcFile; | ||
if (!outputFormat.equals(OutputFormat.MOXF_LIBRARY)) { | ||
ysfcFile = YsfcFile.withChunks ("EWFM", "DWFM", "EWIM", "DWIM"); | ||
} else { | ||
ysfcFile = YsfcFile.withChunks ("EWFM", "DWFM", "EWIM", "DWIM", "EARP", "DARP", "EVCE", "DVCE"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was not sure if the chunks in X3A file can be skipped, so added all of them and left them empty. |
||
} | ||
|
||
ysfcFile.setVersionStr (VERSION_MAP.get (outputFormat)); | ||
|
||
final int libraryID = 0x10001; | ||
|
||
Optional<MOXFData> moxfData; | ||
if (outputFormat.equals(OutputFormat.MOXF_LIBRARY)) { | ||
// Yamaha MOXF has following additional fields: | ||
// - a field that for each keygroup (across all keybanks) stores total index of the samples in the library | ||
// starting at 0x10. | ||
// - a field that for each channel data stores its total index in DWIM block (counting channels of all | ||
// samples in the library starting at 1. | ||
moxfData = Optional.of(new MOXFData(16, 0)); | ||
} else { | ||
moxfData = Optional.empty(); | ||
} | ||
|
||
// Numbering covers all(!) samples | ||
int sampleNumber = 1; | ||
|
||
|
@@ -212,24 +231,36 @@ private static void storeMultisamples (final List<IMultisampleSource> multisampl | |
throw new IOException (ex); | ||
} | ||
|
||
final byte [] data = dataChunk.getData (); | ||
final byte[] data = dataChunk.getData(); | ||
final boolean isStereo = numberOfChannels == 2; | ||
|
||
for (int channel = 0; channel < numberOfChannels; channel++) | ||
{ | ||
keybankList.add (createKeybank (sampleNumber, zone, formatChunk, numSamples)); | ||
keybankList.add(createKeybank(moxfData, sampleNumber, zone, formatChunk, numSamples)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please have a look at this change. To my understanding, the keygroup in MOXF can contain stereo files. |
||
for (int channel = 0; channel < numberOfChannels; channel++) { | ||
final YamahaYsfcWaveData waveData = new YamahaYsfcWaveData(); | ||
waveDataList.add(waveData); | ||
waveData.setData(isStereo ? getChannelData(channel, data) : data); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please review closely as well. Not sure if this handles stereo files correctly. On my testing material, it seems to sound okay (definitely not missing a channel). |
||
} | ||
sampleNumber++; | ||
|
||
final YamahaYsfcWaveData waveData = new YamahaYsfcWaveData (); | ||
waveDataList.add (waveData); | ||
waveData.setData (isStereo ? getChannelData (channel, data) : data); | ||
// For MOXF: | ||
// - calculating total count of the samples + 0x10 | ||
// - calculating total count of the channels across all samples | ||
moxfData = moxfData.map(storage -> new MOXFData( | ||
storage.numberOfSamplesWritten() + numSamples * numberOfChannels, | ||
storage.numberOfChannelsWritten() + numberOfChannels) | ||
); | ||
|
||
sampleNumber++; | ||
} | ||
} | ||
|
||
final int sampleIndex = libraryID + i; | ||
final YamahaYsfcEntry keyBankEntry = new YamahaYsfcEntry (); | ||
keyBankEntry.setSpecificValue (sampleIndex); | ||
final YamahaYsfcEntry keyBankEntry; | ||
if (!outputFormat.equals(OutputFormat.MOXF_LIBRARY)) { | ||
keyBankEntry = new YamahaYsfcEntry (); | ||
keyBankEntry.setSpecificValue (sampleIndex); | ||
} else { | ||
keyBankEntry = new YamahaMOXFEWFMEntry(); | ||
keyBankEntry.setSpecificValue (sampleIndex - libraryID + 1); | ||
} | ||
|
||
// Set the category | ||
String n = multisampleName; | ||
|
@@ -241,10 +272,22 @@ private static void storeMultisamples (final List<IMultisampleSource> multisampl | |
n = categoryID.toString () + ":" + n; | ||
} | ||
keyBankEntry.setItemName (n); | ||
if (outputFormat.equals(OutputFormat.MOXF_LIBRARY)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Item titles need to be present for MOXF. |
||
keyBankEntry.setItemTitle(String.format("%04d-Waveform.wfm", i + 1)); | ||
} | ||
|
||
final YamahaYsfcEntry waveDataEntry = new YamahaYsfcEntry (); | ||
|
||
final YamahaYsfcEntry waveDataEntry; | ||
if (!outputFormat.equals(OutputFormat.MOXF_LIBRARY)) { | ||
waveDataEntry = new YamahaYsfcEntry (); | ||
waveDataEntry.setSpecificValue (sampleIndex); | ||
} else { | ||
waveDataEntry = new YamahaMOXFEWIMEntry(); | ||
waveDataEntry.setSpecificValue (sampleIndex - libraryID + 1); | ||
waveDataEntry.setItemTitle(String.format("%04d-Waveform.wim", i + 1)); | ||
} | ||
waveDataEntry.setItemName (multisampleName); | ||
waveDataEntry.setSpecificValue (sampleIndex); | ||
|
||
|
||
ysfcFile.fillWaveChunks (keyBankEntry, keybankList, waveDataEntry, waveDataList); | ||
} | ||
|
@@ -256,11 +299,16 @@ private static void storeMultisamples (final List<IMultisampleSource> multisampl | |
} | ||
|
||
|
||
private static YamahaYsfcKeybank createKeybank (final int sampleNumber, final ISampleZone zone, final FormatChunk formatChunk, final int numSamples) | ||
private static YamahaYsfcKeybank createKeybank (Optional<MOXFData> moxfData, int sampleNumber, final ISampleZone zone, final FormatChunk formatChunk, final int numSamples) | ||
{ | ||
final YamahaYsfcKeybank keybank = new YamahaYsfcKeybank (); | ||
keybank.setNumber (sampleNumber); | ||
|
||
moxfData.ifPresent(data -> { | ||
keybank.setVersion1TotalSampleOffset(data.numberOfSamplesWritten()); | ||
keybank.setVersion1TotalChannelOffset(data.numberOfChannelsWritten()); | ||
}); | ||
|
||
final int numberOfChannels = formatChunk.getNumberOfChannels (); | ||
keybank.setChannels (numberOfChannels); | ||
keybank.setSampleFrequency (formatChunk.getSampleRate ()); | ||
|
@@ -280,11 +328,11 @@ private static YamahaYsfcKeybank createKeybank (final int sampleNumber, final IS | |
final double gain = zone.getGain (); | ||
keybank.setLevel (gain < -95.25 ? 0 : (int) Math.round ((Math.clamp (gain, -95.25, 0) + 95.25) / 0.375) + 1); | ||
|
||
if (numberOfChannels == 1) | ||
{ | ||
final double panorama = zone.getPanorama (); | ||
keybank.setPanorama ((int) (panorama < 0 ? panorama * 64 : panorama * 63)); | ||
} | ||
// if (numberOfChannels == 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please review closely. I hope this should work for stereo samples as well. Hopefully for Montage as well. |
||
// { | ||
final double panorama = zone.getPanorama (); | ||
keybank.setPanorama ((int) (panorama < 0 ? panorama * 64 : panorama * 63)); | ||
// } | ||
|
||
keybank.setPlayStart (zone.getStart ()); | ||
keybank.setPlayEnd (zone.getStop ()); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,15 +21,15 @@ | |
*/ | ||
public class YamahaYsfcEntry | ||
{ | ||
private int length; | ||
private byte [] flags = new byte [6]; | ||
private String itemName = ""; | ||
private String itemTitle = ""; | ||
private byte [] additionalData = new byte [0]; | ||
private int correspondingDataSize = 0; | ||
private int correspondingDataOffset = 0; | ||
private int specificValue = 0; | ||
private int entryID = 0xFFFFFFFF; | ||
protected int length; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Setting the fields to protected in order to subclass this class for MOXF-specific entries. The other approach would be making this entry class aware if it is for MOXF or not. Subclassing seemed to introduce less changes to the code. |
||
private byte [] flags = new byte [6]; | ||
protected String itemName = ""; | ||
protected String itemTitle = ""; | ||
protected byte [] additionalData = new byte [0]; | ||
protected int correspondingDataSize = 0; | ||
protected int correspondingDataOffset = 0; | ||
protected int specificValue = 0; | ||
protected int entryID = 0xFFFFFFFF; | ||
|
||
|
||
/** | ||
|
@@ -111,7 +111,7 @@ public void write (final OutputStream out) throws IOException | |
} | ||
|
||
|
||
private byte [] createContent () throws IOException | ||
protected byte [] createContent () throws IOException | ||
{ | ||
final ByteArrayOutputStream contentStream = new ByteArrayOutputStream (); | ||
|
||
|
@@ -229,6 +229,16 @@ public String getItemTitle () | |
return this.itemTitle; | ||
} | ||
|
||
/** | ||
* Set the title of the item. Used by Yamaha MOXF | ||
* | ||
* @param itemTitle The filename | ||
*/ | ||
public void setItemTitle (final String itemTitle) | ||
{ | ||
this.itemTitle = itemTitle; | ||
} | ||
|
||
|
||
/** | ||
* Get the flags. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To not pollute the main code with changes to this block, the entry for the section was subclassed.
Not an ideal solution from theoretical point of view, but works.