diff --git a/documentation/CHANGELOG.md b/documentation/CHANGELOG.md index 718eb70c..d5acebc0 100644 --- a/documentation/CHANGELOG.md +++ b/documentation/CHANGELOG.md @@ -24,6 +24,7 @@ * Fixed: A mapping slot without explicit sample-trim points read a sample start and end of -1 instead of the whole sample (e.g. a converted Waldorf QPAT then showed a sample start and end of -1 on the device). * EXS24 * Fixed: Loop type was not applied. + * Fixed: Envelope times were converted from the EXS24 parameter linearly, but the device applies a fourth-power curve, so short times were greatly overstated - and the attack stage additionally skipped even the linear scaling, coming out about 12.7 times too long on top of that. A quick attack (e.g. 7.5 ms) was read as over a second, so plucked and struck instruments faded in too slowly to be heard and appeared silent. Envelope times are now converted with the hardware-calibrated curve seconds = 10 * (parameter / 127)^4, matching Logic to within one percent (thanks to Douglas Carmichael). * FLAC/OGG * Fixed: FLAC or OGG samples stored inside a ZIP archive (e.g. discoDSP Bliss or DecentSampler libraries) could fail to decompress. * Fixed: Stereo (multi-channel) samples stored in a compressed format were truncated to half their length when decompressed while writing to an uncompressed destination. diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Creator.java b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Creator.java index 13e0e729..5c81eaa7 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Creator.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Creator.java @@ -288,8 +288,12 @@ private static void applyFilterParameters (final EXS24Parameters parameters, fin private static int formatEnvTime (final double time) { - // Maximum time for each step are 10 seconds - return time < 0 ? 0 : (int) Math.round (time / 10.0 * 127.0); + // The device maps the 0..127 parameter to a maximum of 10 seconds with a fourth-power + // curve (see EXS24Detector.envelopeTimeToSeconds), so invert it: + // parameter = 127 * (seconds / 10) ^ (1/4). + if (time <= 0) + return 0; + return (int) Math.round (127.0 * Math.pow (Math.min (time, 10.0) / 10.0, 0.25)); } diff --git a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Detector.java b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Detector.java index e331854a..2807debb 100644 --- a/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Detector.java +++ b/src/main/java/de/mossgrabers/convertwithmoss/format/exs/EXS24Detector.java @@ -338,13 +338,12 @@ private static IEnvelope createEnvelope (final EXS24Parameters parameters, final final Integer attackCurve = parameters.get (envelopeIndex == 1 ? EXS24Parameters.ENV1_ATK_CURVE : EXS24Parameters.ENV2_ATK_CURVE); final IEnvelope envelope = new DefaultEnvelope (); - // Maximum time for each step are 10 seconds - envelope.setDelayTime (delay == null ? 0 : delay.doubleValue () / 127.0 * 10.0); - envelope.setAttackTime (attack == null ? 0 : attack.intValue ()); - envelope.setHoldTime (hold == null ? 0 : hold.doubleValue () / 127.0 * 10.0); - envelope.setDecayTime (decay == null ? 0 : decay.doubleValue () / 127.0 * 10.0); + envelope.setDelayTime (envelopeTimeToSeconds (delay)); + envelope.setAttackTime (envelopeTimeToSeconds (attack)); + envelope.setHoldTime (envelopeTimeToSeconds (hold)); + envelope.setDecayTime (envelopeTimeToSeconds (decay)); envelope.setSustainLevel (sustain == null ? 1.0 : sustain.doubleValue () / 127.0); - envelope.setReleaseTime (release == null ? 0 : release.doubleValue () / 127.0 * 10.0); + envelope.setReleaseTime (envelopeTimeToSeconds (release)); if (attackCurve != null) { @@ -358,6 +357,27 @@ private static IEnvelope createEnvelope (final EXS24Parameters parameters, final } + /** + * Convert an EXS24 envelope time parameter to seconds. The parameter ranges from 0 to 127 and + * maps to a maximum of 10 seconds, but the device applies a fourth-power (not linear) curve, so + * low values are far shorter than a linear reading suggests. Calibrated against Logic reference + * instruments (parameter 19 = 5 ms, 27 = 20 ms, 53 = 303 ms, 71 = 977 ms, 94 = 3 s, 127 = 10 s), + * which fit seconds = 10 * (parameter / 127)^4 to within 1 percent. Reading these times linearly + * made e.g. a 7.5 ms attack come out as 1.65 seconds, so the note faded in too slowly to be + * heard. + * + * @param parameter The raw envelope time parameter (0..127), or null when not present + * @return The time in seconds + */ + private static double envelopeTimeToSeconds (final Integer parameter) + { + if (parameter == null) + return 0; + final double normalized = parameter.doubleValue () / 127.0; + return 10.0 * normalized * normalized * normalized * normalized; + } + + private static void applyFilterParameters (final IMultisampleSource multisampleSource, final EXS24Parameters parameters) { final Integer isFilterEnabled = parameters.get (EXS24Parameters.FILTER1_TOGGLE);