Skip to content

Releases: keithadler/hexter

hexter 2.8.0

Choose a tag to compare

@keithadler keithadler released this 21 Sep 20:27

Sean Bolton, who wrote hexter, sent the converter he built in 1986 and 56 real FB-01 banks.
Both changed this release.

FB-01 banks are converted his way now

He wrote that conversion by ear, sitting between an FB-01 and a TX7, and put it in the public
domain. It is what produced the fb01_roms_converted_*.dx7 banks hexter has shipped for years.
hexter had been deriving those values from the format instead, which was a correct reading of
the bytes and a wrong sound.

Running both converters over 2688 voices from 56 real banks showed where:

  • The LFO ran about three times too fast. Its speed is a whole byte, 0 to 255, and hexter
    was reading seven bits of it and stretching that to the DX7's full range. Mean speed across
    the collection was 90 of 99 where it should be 30.
  • Pitch modulation was halved, for a reason that was a guess.
  • The LFO key sync bit was inverted.
  • Envelope rates were linear where the hardware's are curves, output level was rescaled from a
    range of 127 rather than subtracted from 99, and sustain fell to silence where it should stop
    at 35.
  • FB-01 algorithm 2 puts its operators in a different order than hexter had, and algorithm 8
    maps to DX7 29 rather than 31.

The DX21 family keeps its own algorithm table, because no hardware has been put in front of
that one and changing four other synths on evidence from a fifth would be a guess.

Three things hexter keeps doing differently

Each checked over the same 2688 voices. hexter honours the operator enable bits, so an operator
the FB-01 switched off stays silent: 920 of those voices, 34%, have one. It also carries the
keyboard level scaling depth and the amplitude modulation sensitivity.

Tests: 236 checks become 445

Three new suites, pinning the conversions value by value rather than sampling one voice: the
FB-01 conversion, the DX21 family's, and the operator waveforms. Every envelope table is walked
entry by entry, every algorithm has its routing checked and its spare operators checked silent.

Both converters get 4000 random voices each, with every output byte checked against what a
DX7 voice allows. Every check was confirmed by breaking the behaviour it names and watching it
fail.

Corrected

2.7.3 claimed the FB-01's user bank is the form you are most likely to be sent. The manual lists
both dumps as things the instrument sends, which is what that rested on, but Sean remembers the
short form as one it would receive and never transmit, and all 56 of his banks are the long one.
Reading both is still right. The reasoning was not.

DX7 output is unchanged.

hexter 2.7.3

Choose a tag to compare

@keithadler keithadler released this 21 Sep 00:27

The FB-01's user bank loads now

The manual gives two bank dumps, and the FB-01 support that shipped yesterday read only
one of them.

  • voice bank x, F0 43 75 0n 00 00 0x, 6363 bytes. The read-only banks. This is what
    hexter read.
  • voice bank 0, F0 43 0n 0C, 6360 bytes. The bank an owner can actually write to, and
    therefore the one they are most likely to dump and send you. This was ignored.

Everything after the header is the same 49 packets, so the difference is three bytes and
where the voices begin, 71 instead of 74. hexter reads both now, and a bank in either form
converts to exactly the same voices, which is checked rather than assumed.

How it was found

By reading Edisyn, which accepts both and cites the page
of the manual that says so. Yamaha's own Transmitted Dumps table then confirmed it down to
the byte counts. Reaper10 pointed at Edisyn.

Both new behaviors were confirmed by removing them: not recognizing the short header, and
reading its voices from the long header's offset. The second fails at recognition, because
all 48 voices have to announce their own length before a bank is accepted, and from the
wrong offset they do not.

DX7 output is unchanged, byte for byte against 2.5.1, in both the fixed-point and the
floating-point engine.

hexter 2.7.2

Choose a tag to compare

@keithadler keithadler released this 20 Sep 04:36

An FB-01 bank saved as a MIDI file now loads

Every format hexter reads is looked for inside a .mid as well as in a bare .syx, because
saving a dump into a MIDI file is a normal thing to do and always has been. The FB-01 support
that shipped in 2.7.0 checked whole files only, so a bank wrapped that way was quietly
ignored: no error, just nothing.

It is recognized in the same scan as every other dump now, allowing for the length bytes a
MIDI file writes after the F0. That also means an FB-01 bank works in a file holding more than
one dump, and that its 48 voices arrive through the same path every other format uses.

Ignoring the shift fails only the MIDI-file checks; taking the FB-01 branch out of the scan
fails the FB-01 checks wholesale, including a load that quietly yields 49 voices of nothing.

DX7 output is unchanged, byte for byte against 2.5.1, in both the fixed-point and the
floating-point engine.

hexter 2.7.1

Choose a tag to compare

@keithadler keithadler released this 20 Sep 02:48

Activating the plugin now actually resets the engine

A voice keeps its oscillator phase between notes when a patch has key sync switched off.
That is what a DX7 does, and it is right while the synth is playing.

But a host calls the same reset when it activates the plugin, and there it has to mean
start over. It did not. An engine that had already played rendered slightly differently
from a fresh one handed the same work, which meant an instance restored from a saved
session did not sound quite like the one it was saved from.

How it stayed hidden

The check that should have caught it accepted any two renders within two percent of each
other. In the fixed-point engine the difference landed just inside that window. In floating
point it landed just outside.

And nothing ever built the floating-point engine. -DHEXTER_FLOATING_POINT=ON is offered
in the README, no workflow had ever configured it, and so it had been carrying a failing
test that nobody was in a position to see.

What changed

  • Reset clears each voice's oscillator phase and its feedback memory.
  • The state check compares a restored instance against the original sample for sample,
    not by level. Taking the phase clearing back out now fails it in both engines.
  • CI builds and tests both engines, so the floating-point option is no longer something
    that only compiles in theory.

Existing patches are unaffected: DX7 output is still byte for byte what 2.5.1 renders, since
a fresh engine always started from phase zero anyway.

hexter 2.7.0

Choose a tag to compare

@keithadler keithadler released this 20 Sep 02:19

FB-01 banks load

The Yamaha FB-01 was the last four-operator machine hexter could not read, and the README
has listed it as out of reach for as long as there has been a README. It reads them now.

The FB-01's bank is not the DX7 message under a different format byte, the way the
DX21/DX27/DX100/TX81Z dumps are. It is a message of its own:

  • 6363 bytes, holding 48 voices rather than 32
  • seven-character names rather than ten
  • every value stored as a pair of nibbles, low one first

Each voice is converted the same way a DX100 or TX81Z voice is: the four operators become
four of the DX7's six, wired with whichever DX7 algorithm stands closest, and the other two
stay silent. Envelopes, ratios, detune, feedback, the LFO, level scaling and the name come
across.

The part that would have been silently wrong

Two of the FB-01's values, an operator's level and its sustain, are stored as
attenuation: zero is loudest and the maximum is silence, the opposite way round from
the DX7. They are turned round on the way in.

Read the other way a bank would load without complaint, name its patches correctly, and
every one of them would come out inside out and silent. So besides checking the converted
bytes there is a check that plays a converted voice and compares it against a bank with
every operator wide open. With the inversion removed, that check renders an actual zero.

Every new check was confirmed by removing the behavior it names: both inversions, the
reversed operator storage, the operator enable bits, the signed detune, and the bank
identification, each in turn.

Credit, and a licensing note

The format was read from the FB01 Sound Editor
by Frederic Meslin, which Reaper10 pointed at. That program is GPLv3 and hexter is
GPL-2.0-or-later, so none of its code is here: what was taken is the description of a file
format, which is a fact about the hardware rather than anyone's code. It is credited in
AUTHORS.

Caveats, and a request

Like the TX81Z support, this is built from the format rather than from a real dump. The
FB-01 banks shipping in banks/ are conversions somebody made long ago, not originals, so
they do not exercise any of it. If you have real FB-01 bank files, they would be worth
more to this than any amount of re-reading.

DX7 output is unchanged, byte for byte against 2.5.1.

Also in this release: the engine's sources are listed in three separate builds, and a new
check makes CI fail immediately when they disagree, rather than letting a linker discover
it on a runner half an hour later.

hexter 2.6.1

Choose a tag to compare

@keithadler keithadler released this 20 Sep 01:15

Two things, both about banks that arrive after the first one.

Fixed: a bank loaded over a TX81Z bank kept the old waveforms

Load a TX81Z bank, then load anything else over it, and the patch you were playing kept
the TX81Z's operator shapes until you changed program and came back. The new bank's patch
bytes were re-read; the shapes beside them were not.

That is a 2.6.0 bug, about an hour old, found while writing the MIDI support below.

Added: four-operator bulk dumps arrive over MIDI

hexter has always accepted a DX7 32-voice dump sent the way you would send one to the
hardware, and the README told you a bank could arrive that way. For a DX21, DX27, DX100 or
TX81Z that was not true: the dump was quietly ignored, and only a file would do.

It converts now, format 0x03 for the four-operator machines and 0x04 for the TX81Z,
exactly as the same dump does when it comes from a file, operator waveforms and all. A
DX7 dump also clears the per-patch waveforms, because a DX7 dump has nothing but sines in
it.

All three behaviors were confirmed by removing each one in turn and watching the matching
check fail. DX7 output is still byte for byte what 2.5.1 renders.

hexter 2.6.0

Choose a tag to compare

@keithadler keithadler released this 20 Sep 00:59

A waveform for each operator

A DX7 operator can only play a sine. The four-operator machines from the TX81Z on let
each operator pick one of eight shapes, and that is most of why those synths sound like
themselves rather than like a small DX7.

All six operators get the same choice now, as six parameters: OP1 wave through
OP6 wave. Each one rests at Patch and leaves that operator alone. W1 is the
sine hexter has always played, W2 is that sine squared, and the rest silence part of the
cycle or run it at double speed. Anything but W1 puts harmonics in before the FM starts,
which takes a familiar patch somewhere a DX7 cannot go: a squared carrier is brighter and
reedier, a half-cycle modulator gets buzzy fast.

They behave like the algorithm knob. Move one and you hear it on notes you are already
holding, not only on the next one. Move it back to Patch, or select a program, and the
patch's own shapes return. They are saved with your session.

The eight shapes are derived the way ymfm derives
them, BSD-3-Clause, now credited in AUTHORS.

TX81Z banks bring their waveforms with them

A TX81Z voice carries a shape per operator in bytes the converter used to throw away.
They are read now, and each one lands on the DX7 operator that four-operator one became,
so a TX81Z patch that leans on a shape other than the sine still sounds like itself. A
DX21, DX27 or DX100 bank has nothing in those bytes, so it stays all sines, which is what
that hardware is.

Nothing about an existing patch changes

Shape W1 is the same table hexter has used since the beginning. Five ROM patches render
byte for byte identically against a build of 2.5.1. A session saved by an older hexter
still loads; it simply has no waveforms in it and every operator comes back a sine.

Every new check was confirmed to fail with the behavior it tests removed: the note-on
lookup, the bank converter, the state section, the live update under a held note, the
CLAP knob, the CLAP state trailer and the LV2 ports, each broken in turn.

hexter 2.5.1

Choose a tag to compare

@keithadler keithadler released this 20 Sep 00:09

Fixes a regression in 2.5.0 on Linux. If you are on Linux, take this one.

2.5.0 added JACK to the standalone by linking it, which means that build refuses to start at all on a machine with no JACK library installed. Every release before it needed only libraries every Linux has. Most desktop systems do have a JACK library, and PipeWire provides one, but a bare system does not, and the failure is the program not opening.

The Linux package now holds two standalones:

  • hexter speaks ALSA and PulseAudio and runs anywhere, exactly as 2.4.1 did.
  • hexter-jack speaks JACK as well, and needs a JACK library present to start, which is why it is a separate file.

Pick your backend in Audio/MIDI Settings either way. Both workflows now build and check both binaries, so neither can quietly turn into the other.

Nothing changes on macOS, Windows, Android or iOS. Downloads for each platform are attached below. The Android APK is signed with a per-release key, so uninstall the previous version before installing this one.

Full details in CHANGELOG.md.

hexter 2.5.0

Choose a tag to compare

@keithadler keithadler released this 19 Sep 22:11

The Linux standalone speaks JACK now, alongside ALSA and PulseAudio. Pick one in Audio/MIDI Settings.

It always could in principle: the audio layer compiles in whichever backends it finds when it is built. The build machines had no JACK development package, so every Linux release up to and including 2.4.1 shipped with ALSA and PulseAudio only, and nothing anywhere said so. Checking 2.4.1's binary makes it plain: 80 ALSA symbols, zero JACK ones.

Both workflows now fail the build if the finished standalone is missing a backend, because this is exactly the sort of gap that stays quiet for months. Asked for on theabolton#18.

If you build it yourself, install JACK's development package first (libjack-jackd2-dev on Debian and Ubuntu) or the standalone will come out with ALSA alone. The README says so too.

Nothing else changes from 2.4.1. Downloads for each platform are attached below. The Android APK is signed with a per-release key, so uninstall the previous version before installing this one.

Full details in CHANGELOG.md.

hexter 2.4.1

Choose a tag to compare

@keithadler keithadler released this 19 Sep 18:33

Fixes a claim 2.4.0 made and did not deliver: TX81Z banks now actually load.

A TX81Z 32-voice bank is the same dump as a DX100's, under format byte 0x04 instead of 0x03. Only 0x03 was recognized, so the TX81Z support named in the 2.4.0 README and release notes did not exist. Both are accepted now. Its voices sit the same way in the bytes hexter reads, so they convert like the others, and the TX81Z's extras, the operator waveforms above all, are ignored.

Being straight about it: that part is built from the format rather than from a real dump, and nobody has tried it against hardware. If you have a TX81Z, send a bank and say what comes out wrong.

Nothing else changes from 2.4.0. Downloads for each platform are attached below. The Android APK is signed with a per-release key, so uninstall the previous version before installing this one.

Full details in CHANGELOG.md.