Repository navigation
Convert the pitch and volume LFO of DirectWave zones - #304
Merged
Merged
Conversation
Reads non-monolithic DWP programs from their binary block structure and falls back to the sampled/Automap file name conventions for DWB banks. Writes a program folder (DWP plus 16-bit WAV files) which FL Studio Desktop loads directly and FL Studio Mobile imports. The DWP writer reproduces a real FL Studio Desktop export byte-identically when fed its own detected data. Implements the request of issue git-moss#70.
The dwsanitizer project proves that monolithic files keep the same preamble and block structure (its hardcoded offsets and its path scan pattern are exactly the block envelope at 0x5A). The audio format block fully describes the audio, therefore the embedded data is identified as the unknown block whose size is exactly frameCount times bytesPerFrame. 16/24-bit integer data is taken as-is, 32-bit float is converted to 24-bit. Verified against synthesized monolithic structures; no real monolithic specimen was available.
All six factory programs (format version 0x25) tokenize completely and confirm the preamble size rule, the zone mapping layout and the zone gain float. They also show that the bytes-per-frame field of the audio format block is not reliable in that version, therefore the embedded audio of monolithic files is now identified by frame count times channels times 2, 3 or 4 bytes per sample instead.
The preamble field at 0x4A is the number of sample containers - proven by all seven specimens - and DirectWave crashes with an access violation when it does not match (observed with FL Studio 2026). The creator wrote the template value into every file; it now patches the real count. The writer is also remodeled on the FL Studio Mobile factory programs: version 0x25, 100 zero-based parameter slots, fixed shadow block sizes and all opaque container blocks copied verbatim from a factory program instead of a user export with uninitialized data. The factory programs also revealed the loop fields: the audio format block holds loop mode, start and end at offsets 20/24/28 (all sustained factory programs are looped there; their WAV files carry no smpl chunks). The detector now reads the program loop (WAV chunks remain the fall-back) and the creator writes it.
Differential analysis of the factory programs identified block 0x01FD as the amplitude envelope (four floats: attack, decay, sustain and release knob positions): the looped 'Electric' piano is the only program with sustain 0 and a long decay - the classic way to make a looped e-piano die away - and the release is nudged up on exactly the two sustained instruments. The sustain level converts exactly; the time knobs are mapped with a provisional cubic law (10 s * position^3) until the DirectWave time hints can be measured. Envelopes which equal the version defaults are skipped so the category default applies. Also documents the localized but unverified filter blocks (0x01FB, the type field is Off in every available specimen) and the 4x4 modulation matrix (0x0204, enums unknown).
The field at offset 12 of the audio format block held 2 (bytes per frame of a mono sample) in written files - a value which no real DirectWave file ever shows (the factory population is powers of two from 4 to 128, loosely following the frame count). Write the same power-of-two law instead so written files stay inside the observed value population; the DirectWave editor is suspected to use this field for its waveform display.
The gain float at offset 9 of the zone mapping is a linear amplitude (1.0 = 0 dB, the 0.675 of a factory zone is ca. -3.4 dB) and the float at offset 13 is the panning with 0.5 as the center. Not converting the gain made converted E-mu programs unusable: EOS presets park spare layers at -96 dB, which then played at full volume (a parked buzz-loop layer turned a converted pad into a rattle). Gains above 0 dB are clamped to 1.0 since higher values were never observed.
The DirectWave plug-in binary carries its enumerations as string tables and its editor library carries the same lists as menu items with their enum value in the VCL Tag property. The loop mode table explains the 0 and 2 of the specimens as 'Disabled' and 'Forward' and adds 'One-Shot', 'Sustained' (= loop until release) and 'Bounce' (= alternating), which are now all converted in both directions. The filter type enum is documented as well (0 = Off, 1 = Lowpass ... 7 = Vox), but the filter stays unwritten until the position of the type field is verified.
The filter block starts with the type as a 32-bit integer followed by the cutoff and the resonance as knob positions. Since every specimen has its filters off, the layout was settled by experiment: of three probe programs which put a low-pass into the candidate fields, only the integer-at-offset-0 variant silenced its zone (a low-pass at a near-minimum cutoff mutes a saw wave) while its control zone stayed audible. The cutoff is mapped exponentially from 20 Hz to 20 kHz, which is consistent with the probe but not calibrated against the frequency display of DirectWave. Not converting the filter was the second half of the E-mu conversion problem: the layers which the source filters down to a sub-bass thump played back in full bandwidth.
Searched for the cutoff and envelope time laws: there are no lookup tables and no minimum/maximum constant pairs; the parameter ranges are built in code (the binary carries DirectWaveParameters.cpp and a 'RangeMin < RangeMax' assertion but no descriptor table which references the parameter names). The search did yield the display formats, which confirm the gain, panning and 'Ticks' decodings.
The knob laws are not stored in the plug-in binary, so the cutoff range was measured by listening: a program with ten zones which play the same 220 Hz saw wave through a low-pass filter, with the cutoff knob stepped from 0.10 to 1.0, was played from the on-screen keyboard of the plug-in. The fundamental starts to pass at knob 0.26 and the wave is fully open at 0.66. An exponential range of 45 Hz to 20 kHz reproduces the first at 219.7 Hz and puts the second at ca. 2.5 kHz, and it explains the two quiet zones below it as well. The previously assumed 20 Hz bottom is ruled out - it would have kept the 220 Hz fundamental muted at the zone where it audibly passes.
Measured like the cutoff law: a program whose six zones differ only in the decay knob (sustain 0, so the note dies while the key is held) next to five reference zones whose fade-out is baked into their audio at 0.5, 1, 2, 4 and 8 seconds. The reference zones show the measurement to be 3 % short, and after that correction the six decay times fit seconds = 9.07 * position ^ 3.96 within 3 %. The previously assumed cubic taper up to 10 seconds was wrong by up to a factor of three at short times.
A monolithic program keeps the same structure and adds one block per sample container - tag 0x0206, the last block before the container terminator - whose payload is the length of the audio, four unused bytes and the audio as a FLAC stream. The tag was located in the block writer of the plug-in binary (the payload length is a constant 8 bytes before the tag for every fixed-size block, so the blocks without such a constant are the variable-length ones) and then confirmed on 85 monolithic programs: in all 288 sample containers the length field equals the payload size minus 8, the field at offset 4 is 0 and the block sits between the last 0x0204 block and the terminator. Reading decodes the FLAC stream, writing is a new 'Monolithic file' option of the creator which embeds the samples instead of writing a folder of WAV files - a single file is what FL Studio Mobile is easiest to transfer to, which is what issue git-moss#70 asked for. A written program is byte-structurally identical to a program saved by DirectWave itself. Also adds the format version 0x24 of the legacy patch pack of FL Studio, whose preamble is 4 bytes longer: the start of the block stream is now located instead of assumed. Those files contain the only zones with an engaged filter found anywhere - 111 low-pass zones - which confirm the filter type encoding.
Issue git-moss#70 asks for monolithic programs and its use case is FL Studio Mobile, where a single file per instrument is the point. A monolithic program is also smaller than the same instrument as a folder of WAV files, since the embedded audio is FLAC compressed. The folder layout stays available for the case that the samples should be editable.
Writing the samples as a folder of WAV files next to the DWP file had one advantage - the samples stay editable - but that is what the WAV and SFZ destinations are for. A monolithic program is a single self-contained file, which is the point of git-moss#70 and of the FL Studio Mobile use case, it is smaller because the embedded audio is FLAC compressed, and it keeps up to 24 bit where the folder layout was capped at 16. Writing only one layout removes the option and the second code path with it. Verified in DirectWave: a written monolithic program loads and all of its zones play correctly.
The tab was labeled only 'DirectWave', which does not say what the format is; all other formats name their vendor or application. Use 'FL Studio DirectWave' and sort the entry into the format lists accordingly (after Fairlight, which is where the documentation and the feature sheet already list it). The CLI format token stays 'DirectWave'.
The creator built its programs from the sample container of an FL Studio Mobile factory program and zeroed the four program parameter blocks 0x0069 to 0x006C. Those blocks are not metadata: all 117 available programs carry the same non-zero payloads, and the Mobile container brings the Mobile defaults of the modulation matrix (Zone LFO 2 to Voice Pitch instead of Pitch Bend to Voice Pitch), of the two LFOs and of the disabled filter. Written programs loaded and played, but lost the attack of the source instrument. Take the preamble and the container template from a real monolithic desktop program instead, so a written file equals a real program byte for byte except for the six blocks which carry the converted content, and write the observed payloads of the program parameter blocks. The modulation matrix indices are decoded in the design document; the path of the program itself uses a single backslash like the real files. Also pad the embedded audio to a full block: the frame count of every sample of every monolithic program is a multiple of 512 and its FLAC stream is one block longer. A sample which does not fill its last block crashes the plug-in while loading - converting an instrument with resampling produces such lengths, which is how it was found.
A DirectWave zone has two LFOs which are routed to their target in the modulation matrix of the zone. Convert both directions for the two targets the model knows: LFO 1 modulates the pitch (vibrato) and LFO 2 the volume (tremolo). The blocks were identified from the corpus - 'Electric' is the only program which routes a zone LFO to something audible and also the only one whose LFO rate differs from the default, and the four Acoustic Guitar programs with a filter envelope show that the neighboring block is the pair of zone envelopes, not the LFOs. The three laws are measured, since they are not in the plug-in binary: the rate knob is 20 * position^2 Hertz, the modulation amount is bipolar around 0.5 with the strength being the cube of the distance from it, and a pitch modulation reaches 2400 cent at the full strength while a gain modulation is the modulation index of the linear gain. Each was measured with a probe program of eight zones which differ in exactly one value; the measurements and the fits are in the design document.
The volume law rested on a single usable measurement; a probe with eight strengths spread over the measurable range confirms it across the whole range. A converted program played back by DirectWave confirms the rate and the pitch law end to end and reproduces the written modulation index.
git-moss
added a commit
that referenced
this pull request
Sep 25, 2026
* Add support for the FL Studio DirectWave format Reads non-monolithic DWP programs from their binary block structure and falls back to the sampled/Automap file name conventions for DWB banks. Writes a program folder (DWP plus 16-bit WAV files) which FL Studio Desktop loads directly and FL Studio Mobile imports. The DWP writer reproduces a real FL Studio Desktop export byte-identically when fed its own detected data. Implements the request of issue #70. * Read the embedded samples of monolithic DWP files The dwsanitizer project proves that monolithic files keep the same preamble and block structure (its hardcoded offsets and its path scan pattern are exactly the block envelope at 0x5A). The audio format block fully describes the audio, therefore the embedded data is identified as the unknown block whose size is exactly frameCount times bytesPerFrame. 16/24-bit integer data is taken as-is, 32-bit float is converted to 24-bit. Verified against synthesized monolithic structures; no real monolithic specimen was available. * Validate the DWP structure against the FL Studio Mobile factory programs All six factory programs (format version 0x25) tokenize completely and confirm the preamble size rule, the zone mapping layout and the zone gain float. They also show that the bytes-per-frame field of the audio format block is not reliable in that version, therefore the embedded audio of monolithic files is now identified by frame count times channels times 2, 3 or 4 bytes per sample instead. * Add the FL Studio DirectWave rows to the feature support sheet * Fix the crash on loading written DWP files and add loop support The preamble field at 0x4A is the number of sample containers - proven by all seven specimens - and DirectWave crashes with an access violation when it does not match (observed with FL Studio 2026). The creator wrote the template value into every file; it now patches the real count. The writer is also remodeled on the FL Studio Mobile factory programs: version 0x25, 100 zero-based parameter slots, fixed shadow block sizes and all opaque container blocks copied verbatim from a factory program instead of a user export with uninitialized data. The factory programs also revealed the loop fields: the audio format block holds loop mode, start and end at offsets 20/24/28 (all sustained factory programs are looped there; their WAV files carry no smpl chunks). The detector now reads the program loop (WAV chunks remain the fall-back) and the creator writes it. * Read and write the DirectWave amplitude envelope Differential analysis of the factory programs identified block 0x01FD as the amplitude envelope (four floats: attack, decay, sustain and release knob positions): the looped 'Electric' piano is the only program with sustain 0 and a long decay - the classic way to make a looped e-piano die away - and the release is nudged up on exactly the two sustained instruments. The sustain level converts exactly; the time knobs are mapped with a provisional cubic law (10 s * position^3) until the DirectWave time hints can be measured. Envelopes which equal the version defaults are skipped so the category default applies. Also documents the localized but unverified filter blocks (0x01FB, the type field is Off in every available specimen) and the 4x4 modulation matrix (0x0204, enums unknown). * Write the observed value population into the audio format stride field The field at offset 12 of the audio format block held 2 (bytes per frame of a mono sample) in written files - a value which no real DirectWave file ever shows (the factory population is powers of two from 4 to 128, loosely following the frame count). Write the same power-of-two law instead so written files stay inside the observed value population; the DirectWave editor is suspected to use this field for its waveform display. * Read and write the zone gain and panning The gain float at offset 9 of the zone mapping is a linear amplitude (1.0 = 0 dB, the 0.675 of a factory zone is ca. -3.4 dB) and the float at offset 13 is the panning with 0.5 as the center. Not converting the gain made converted E-mu programs unusable: EOS presets park spare layers at -96 dB, which then played at full volume (a parked buzz-loop layer turned a converted pad into a rattle). Gains above 0 dB are clamped to 1.0 since higher values were never observed. * Convert all five DirectWave loop modes The DirectWave plug-in binary carries its enumerations as string tables and its editor library carries the same lists as menu items with their enum value in the VCL Tag property. The loop mode table explains the 0 and 2 of the specimens as 'Disabled' and 'Forward' and adds 'One-Shot', 'Sustained' (= loop until release) and 'Bounce' (= alternating), which are now all converted in both directions. The filter type enum is documented as well (0 = Off, 1 = Lowpass ... 7 = Vox), but the filter stays unwritten until the position of the type field is verified. * Read and write the zone filter The filter block starts with the type as a 32-bit integer followed by the cutoff and the resonance as knob positions. Since every specimen has its filters off, the layout was settled by experiment: of three probe programs which put a low-pass into the candidate fields, only the integer-at-offset-0 variant silenced its zone (a low-pass at a near-minimum cutoff mutes a saw wave) while its control zone stayed audible. The cutoff is mapped exponentially from 20 Hz to 20 kHz, which is consistent with the probe but not calibrated against the frequency display of DirectWave. Not converting the filter was the second half of the E-mu conversion problem: the layers which the source filters down to a sub-bass thump played back in full bandwidth. * Document that the knob laws are not stored in the plug-in binary Searched for the cutoff and envelope time laws: there are no lookup tables and no minimum/maximum constant pairs; the parameter ranges are built in code (the binary carries DirectWaveParameters.cpp and a 'RangeMin < RangeMax' assertion but no descriptor table which references the parameter names). The search did yield the display formats, which confirm the gain, panning and 'Ticks' decodings. * Calibrate the filter cutoff knob law by measurement The knob laws are not stored in the plug-in binary, so the cutoff range was measured by listening: a program with ten zones which play the same 220 Hz saw wave through a low-pass filter, with the cutoff knob stepped from 0.10 to 1.0, was played from the on-screen keyboard of the plug-in. The fundamental starts to pass at knob 0.26 and the wave is fully open at 0.66. An exponential range of 45 Hz to 20 kHz reproduces the first at 219.7 Hz and puts the second at ca. 2.5 kHz, and it explains the two quiet zones below it as well. The previously assumed 20 Hz bottom is ruled out - it would have kept the 220 Hz fundamental muted at the zone where it audibly passes. * Calibrate the envelope knob time law by measurement Measured like the cutoff law: a program whose six zones differ only in the decay knob (sustain 0, so the note dies while the key is held) next to five reference zones whose fade-out is baked into their audio at 0.5, 1, 2, 4 and 8 seconds. The reference zones show the measurement to be 3 % short, and after that correction the six decay times fit seconds = 9.07 * position ^ 3.96 within 3 %. The previously assumed cubic taper up to 10 seconds was wrong by up to a factor of three at short times. * Read and write monolithic DirectWave programs A monolithic program keeps the same structure and adds one block per sample container - tag 0x0206, the last block before the container terminator - whose payload is the length of the audio, four unused bytes and the audio as a FLAC stream. The tag was located in the block writer of the plug-in binary (the payload length is a constant 8 bytes before the tag for every fixed-size block, so the blocks without such a constant are the variable-length ones) and then confirmed on 85 monolithic programs: in all 288 sample containers the length field equals the payload size minus 8, the field at offset 4 is 0 and the block sits between the last 0x0204 block and the terminator. Reading decodes the FLAC stream, writing is a new 'Monolithic file' option of the creator which embeds the samples instead of writing a folder of WAV files - a single file is what FL Studio Mobile is easiest to transfer to, which is what issue #70 asked for. A written program is byte-structurally identical to a program saved by DirectWave itself. Also adds the format version 0x24 of the legacy patch pack of FL Studio, whose preamble is 4 bytes longer: the start of the block stream is now located instead of assumed. Those files contain the only zones with an engaged filter found anywhere - 111 low-pass zones - which confirm the filter type encoding. * Write monolithic DirectWave programs by default Issue #70 asks for monolithic programs and its use case is FL Studio Mobile, where a single file per instrument is the point. A monolithic program is also smaller than the same instrument as a folder of WAV files, since the embedded audio is FLAC compressed. The folder layout stays available for the case that the samples should be editable. * Always write monolithic DirectWave programs Writing the samples as a folder of WAV files next to the DWP file had one advantage - the samples stay editable - but that is what the WAV and SFZ destinations are for. A monolithic program is a single self-contained file, which is the point of #70 and of the FL Studio Mobile use case, it is smaller because the embedded audio is FLAC compressed, and it keeps up to 24 bit where the folder layout was capped at 16. Writing only one layout removes the option and the second code path with it. Verified in DirectWave: a written monolithic program loads and all of its zones play correctly. * Name the DirectWave format after its host application The tab was labeled only 'DirectWave', which does not say what the format is; all other formats name their vendor or application. Use 'FL Studio DirectWave' and sort the entry into the format lists accordingly (after Fairlight, which is where the documentation and the feature sheet already list it). The CLI format token stays 'DirectWave'. * Build written DirectWave programs from a real monolithic program The creator built its programs from the sample container of an FL Studio Mobile factory program and zeroed the four program parameter blocks 0x0069 to 0x006C. Those blocks are not metadata: all 117 available programs carry the same non-zero payloads, and the Mobile container brings the Mobile defaults of the modulation matrix (Zone LFO 2 to Voice Pitch instead of Pitch Bend to Voice Pitch), of the two LFOs and of the disabled filter. Written programs loaded and played, but lost the attack of the source instrument. Take the preamble and the container template from a real monolithic desktop program instead, so a written file equals a real program byte for byte except for the six blocks which carry the converted content, and write the observed payloads of the program parameter blocks. The modulation matrix indices are decoded in the design document; the path of the program itself uses a single backslash like the real files. Also pad the embedded audio to a full block: the frame count of every sample of every monolithic program is a multiple of 512 and its FLAC stream is one block longer. A sample which does not fill its last block crashes the plug-in while loading - converting an instrument with resampling produces such lengths, which is how it was found. * Convert the pitch and the volume LFO of DirectWave zones A DirectWave zone has two LFOs which are routed to their target in the modulation matrix of the zone. Convert both directions for the two targets the model knows: LFO 1 modulates the pitch (vibrato) and LFO 2 the volume (tremolo). The blocks were identified from the corpus - 'Electric' is the only program which routes a zone LFO to something audible and also the only one whose LFO rate differs from the default, and the four Acoustic Guitar programs with a filter envelope show that the neighboring block is the pair of zone envelopes, not the LFOs. The three laws are measured, since they are not in the plug-in binary: the rate knob is 20 * position^2 Hertz, the modulation amount is bipolar around 0.5 with the strength being the cube of the distance from it, and a pitch modulation reaches 2400 cent at the full strength while a gain modulation is the modulation index of the linear gain. Each was measured with a probe program of eight zones which differ in exactly one value; the measurements and the fits are in the design document. * Confirm the LFO laws against the plug-in and a second volume probe The volume law rested on a single usable measurement; a probe with eight strengths spread over the measurable range confirms it across the whole range. A converted program played back by DirectWave confirms the rate and the pitch law end to end and reproduces the written modulation index. --------- Co-authored-by: Jürgen Moßgraber <juergen.mossgraber@googlemail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on #301, which has to go in first.
A DirectWave zone has two LFOs (block 0x0203, twice) which do nothing on their own - they have to be routed to a target in the per-zone modulation matrix (0x0204). This converts both directions for the two targets the model knows: Zone LFO 1 to the pitch (vibrato) and Zone LFO 2 to the gain (tremolo).
Identifying the blocks was a corpus job. Of the 117 available programs exactly one, the FL Studio Mobile 'Electric', routes a zone LFO to something audible (Zone LFO 1 to Voice Pan), and it is also the only one whose LFO 1 rate differs from the default - which pins the rate field. The neighbouring block 0x0203 was documented as an LFO candidate as well; it is not, it is the pair of zone envelopes, which the four Acoustic Guitar programs that route Zone Env 1 to the filter cutoff show.
The three laws are not in the plug-in binary (the knob ranges are code immediates there, same as the filter cutoff and the envelope times), so they were measured with three probe programs, each with eight zones on the eight white keys of one octave, all playing the same 220 Hz sine and differing in exactly one value. A recording of the eight notes gives the law directly: the modulation rate is the cycle rate of the recorded envelope, the pitch depth is the deviation of the recorded sine in cent, and the volume depth is the modulation index of a sinusoid fitted to the envelope.
Hertz = 20 * position^2. Measured 1.79 / 4.07 / 7.19 / 11.23 / 16.20 / 19.98 Hz at the positions 0.3 to 1.0, against 1.80 / 4.05 / 7.20 / 11.25 / 16.20 / 20.00 from the law. It also predicts the 2.52 Hz which both depth probes show at their position of 0.355, which were recorded for a different purpose - an independent check. A position of 0 is off.strength = (2 * |amount - 0.5|)^3. That is why the 22 factory programs which carry aZone LFO 2 -> Voice Pitchslot at exactly 0.5 do not wobble.cent = 2400 * strength. Measured 19.3 / 19.6, 299.3 / 298.8 and 1220.7 cent at the strengths 0.008, 0.125 and 0.512, against 19.2, 300.0 and 1228.8 from the law - within 2 % over a range of 60:1.index = 1 - 10^(-dB/20).Verified by a round trip through SFZ:
pitchlfo_freq=5 pitchlfo_depth=50 amplfo_freq=3 amplfo_depth=6writes the rate positions 0.5 and 0.3873 and the amounts 0.63758 and 0.89654, and reads back as 5 Hz / 50 cent and 3 Hz / 6 dB. Programs whose routings are neutral or point at a target which is not converted produce nothing, which the six FL Studio Mobile factory programs confirm, and a conversion without LFOs is byte identical to before.All measurements, the fits and the block layout are in
documentation/design/DIRECTWAVE_DWP_FORMAT.md.