Skip to content

Convert the pitch and volume LFO of DirectWave zones - #304

Merged
git-moss merged 21 commits into
git-moss:mainfrom
douglas-carmichael:directwave-lfo
Aug 2, 2026
Merged

git-moss merged 21 commits into
git-moss:mainfrom
douglas-carmichael:directwave-lfo

Conversation

@douglas-carmichael

Copy link
Copy Markdown
Contributor

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.

  • Rate: 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.
  • Modulation amount: bipolar around 0.5, strength = (2 * |amount - 0.5|)^3. That is why the 22 factory programs which carry a Zone LFO 2 -> Voice Pitch slot at exactly 0.5 do not wobble.
  • Pitch depth: 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.
  • Volume depth: the strength is the modulation index of the linear gain, not a value in decibels, so a strength of 1 lets the gain reach zero. A depth in decibels maps to it the way the Decent Sampler support already does, index = 1 - 10^(-dB/20).

Verified by a round trip through SFZ: pitchlfo_freq=5 pitchlfo_depth=50 amplfo_freq=3 amplfo_depth=6 writes 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.

douglas-carmichael and others added 21 commits August 1, 2026 10:10
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
git-moss merged commit 9bffb43 into git-moss:main Aug 2, 2026
@douglas-carmichael
douglas-carmichael deleted the directwave-lfo branch August 4, 2026 09:47
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants