Foreword

Thank you for choosing fortISSimO! First off, please select one of our pricing plans:

TierhUGETracker compatSmallFastCoffee
Free✅ Yes✅ Yes✅ Yes❌ Not included

Jokes aside, please browse the side bar (tap the burger menu at the top-left on small screens) to flip through this manual. Arrow keys are also supported for navigation, as are a few other keyboard shortcuts.

If you have any trouble with either fortISSimO, teNOR, or even this manual, please open an issue or contact me.

What is fortISSimO?

This Game Boy music driver can be used as a drop-in replacement of SuperDisk’s GB sound driver. It has, however, a few differences.

License

To follow the license of hUGETracker and hUGEDriver, fortISSimO is dedicated to the public domain.

To the extent possible under law, all copyright and related or neighboring rights to fortISSimO have been waived. This work is published from France.

Obtaining fortISSimO

There are two ways of obtaining fortISSimO. Using a development version may be somewhat less reliable than a release, but gives you access to some features earlier, and any testing on those is highly appreciated.

Grabbing a release

First, pick one of the releases. Then, either grab one of the pre-built bundles, or either of the “source code” downloads if you want to compile teNOR yourself or no pre-built is available for your setup.

Pre-built bundle

Since the bundle includes a pre-built binary of teNOR, you must grab the one corresponding to your computer’s architecture:

  • Windows: x86_64-pc-windows-msvc1
  • Linux: x86_64-unknown-linux-gnu1
  • macOS: aarch64-apple-darwin if on “Apple Silicon”, x86_64-apple-darwin otherwise1, 2.

You can then use all of the provided files any way you like. To update fortISSimO, simply overwrite all of the files; delete any files not in the new bundle.

1

If you don’t have a 64-bit Intel processor, you will have to go the “Source code” route instead.

Source code

fortISSimO itself can be used just the same, but you will need to compile teNOR yourself. teNOR is written in Rust, so you must install it. Then, you can build teNOR by running cargo build --release inside of the teNOR/ directory; the resulting binary will be in the target/release/ directory.

Using a development version

First, clone the repository; then, follow the “Source code” instructions. Grabbing a ZIP is also fine.

That said, if you want to integrate fortISSimO in a project that already uses Git, consider using a submodule to make upgrading fortISSimO easier.

2

If you have a PowerPC Mac, you have my respect (and no pre-built binaries :D)

FAQ

Why is this called “fortISSimO”?

It started with a joke by SuperDisk, when we talked about my intention to reimplement hUGEDriver “but more optimised”. He suggested something that contained my nickname (ISSOtm), and we eventually landed on fortISSimO; the weird capitalisation is meant to make the funny more obvious, and is par for the course for the hUGETracker ecosystem 😛

Are there any conditions to using this in my game?

Basically, none! fortISSimO was placed in the public domain, since both hUGETracker and hUGEDriver did the same. Mentioning us in your credits, special thanks, or similar would however be very appreciated.

You are free to incorporate fortISSimO into whatever, and modify it as well! As well, contributing back (typically with a pull request) would be very appreciated.

What do I stand to gain from using fO over hD?

The first upside over choosing another driver outright is that you remain in the hUGETracker ecosystem; both hD and fO have the same high-level interface, so switching between the two is largely painless, whereas switching to another driver would likely mean re-composing your songs on top of more involved integration changes.

All other things equal, hD vs fO is a tradeoff:

New releases

hUGEDriver is the canonical driver for hUGETracker, which implies that it will always be up to date with hUGETracker updates. Comparatively, fortISSimO may be lagging behind, as it’s maintained by a third party (me).

I’m however striving to keep the pace; feel free to open an issue if fortISSimO appears to be significantly out of date.

Design priorities

hUGEDriver is programmed rather straightforwardly, which makes some bugs less likely to appear. fortISSimO is programmed to be fast and small first, and easy to maintain second.1

Optimisations have an impact on three different aspects:

Speed

The music driver typically runs on every frame, so every CPU cycle it uses has a significant impact on how much room the game has to run logic and updates.

Driver size

It’s highly rare for all music data to fit in a single ROM bank, so the music driver is almost placed in ROM bank 0, in which space tends to be quite precious.

Track size

Since music tends to be placed in ROMX, most games actually don’t care too much about the actual music’s size (though smaller doesn’t hurt). However, people making “ROM-only” games have a small, hard cap of 32 KiB on total size; smaller music data helps this use case.2

1

fortISSimO is still written to be maintainable: there are several potential optimisations that were rejected because they would make a real mess of the code. But I’ve been pushing the optimisation/maintainability slider further than most people would, is what I meant to say.

2

Those people have had a tendency to actually compress the music data; it is still worth making the uncompressed data smaller, since that almost always leads to smaller compressed data as well, and also faster decompression (if only because less data has to be read and written).

Human-readable music data

SuperDisk stated that hUGEDriver embeds the input data mostly as-is in the ROM as a design feature—the binary data is more human-readable and easier to track the origin of.

fortISSimO instead believes that hardly anyone looks at the binary data, so it’s not worth optimising for that use case.

It also seems that composers tend to be sloppy and produce tracks with unused or redundant instruments, patterns, etc; this is perfectly legitimate, since they iterate on the file itself, and thus it makes more sense to prioritise convenience and wiggle room over data optimisation.

For all these reasons, fortISSimO opts to pre-process the music data when building the ROM, whenever this enables size reduction or faster processing.

Usage within hUGETracker

fortISSimO can be used directly inside of hUGETracker, which lets you preview any differences live! This only requires overwriting a couple of files, too.

DISCLAIMER

Using fortISSimO in hUGETracker is NOT officially supported by hUGETracker! If you get any issues (garbled sound, lockups, crashes) while using fortISSimO, please report them to me! hUGETracker will still open its own bug reporting page, but I can’t change that.

Those should be rare, but I can’t rule out the possibility.

Here is how to “inject” fortISSimO into hUGETracker:

  1. Locate the hUGEDriver directory next to the hUGETracker.exe you want to “mod”; we will be overwriting some files in there.

  2. Copy fortISSimO.asm into that directory as hUGEDriver.asm.

  3. Copy fortISSimO.inc into the include directory as hUGE.inc.

  4. Modify hUGEDriver.asm (which is now just fortISSimO.asm in disguise). Look for the following line, at or near line 11:

    ; def HUGETRACKER equs "1.0"
    
    1. Make sure between the quotes is the version of hUGETracker that you are using.
    2. Delete the ;.

    You should end up with something like this:

    def HUGETRACKER equs "1.0b10"
    

    If you forget to do this, you should get an error when you press the play button in hUGETracker.

  5. You’re done!

Undoing the changes

To restore hUGEDriver, you simply need to restore the hUGEDriver.asm and hUGE.inc files you overwrote. You can re-download hUGETracker, since the files are bundled with it.

Differences with hUGEDriver

fortISSimO aims to be as close to hUGEDriver as possible, but sports a few differences. As of hUGETracker 1.0, anyway—hUGEDriver might choose to implement some of all of these changes in a future release!

If you notice any difference not listed in this page, it’s likely a bug! Please open an issue or contact me.

Vibrato

Vibrato works quite differently under fortISSimO.

  • Vibrato is not supported at all in subpatterns!
  • fortISSimO produces a triangle vibrato (hUGEDriver’s is square).1
  • As a consequence, the vibrato’s parameter is interpreted differently.

    For a 4xy effect, x indicates the vibrato’s rate, and y its slope: for x ticks, the frequency will be increased by y units each tick; then for x ticks, the frequency will be decreased by y units each tick.

  • A vibrato is restarted at the beginning of its row, except if the previous row had a vibrato with exactly the same parameter.
1

For those who prefer square vibratos: sorry, but the vibrato shape is baked into the driver itself—it avoids using a LUT for size’s sake—so you can’t change it without modifying the driver.

Tone portamento

On a row that contains the tone portamento effect and an instrument ID, hUGEDriver reloads the instrument’s parameters; fortISSimO instead ignores the instrument.

Subpatterns

The “set speed” effect is not supported in subpatterns.

Additionally, fortISSimO fixes a bug in hUGEDriver where any jumps to row #31 (J32 in the tracker) would be ignored.

“Absolute” subpatterns

fortISSimO’s subpatterns allow temporarily overriding the current note! This is done with effect 7 (normally “note delay”, unavailable in subpatterns); the parameter indicates which note shall be used on that row (the lower the argument, the lower the pitch).

Note that using this effect overrides everything on the tick that it’s active.

Usage

There is a demo project for fortISSimO, that can serve as a reference, and as a quick way to preview your track.

Using fortISSimO has two parts to it: converting your songs, and playing them back. The former is handled by teNOR, the latter is handled by fortISSimO itself.

Neither uge2source nor hUGETracker’s “Export to RGBDS asm…” function are suitable for fortISSimO! For this reason, it’s quite advisable to put the .uge files directly in your source files, and run teNOR as part of your build process.

Since both teNOR and fortISSimO cooperate tightly together, you must use compatible versions of both! If you don’t, you should get an error telling you so. teNOR and fortISSimO both follow semantic versioning, which here means that versions x.y.z and x'.y'.z' are compatible if and only if x and x' are equal.

Up next:

teNOR

teNOR stands for “tracker-less exporter with Notably Optimised Results”1.

teNOR is a command-line program that converts .uge files saved by hUGETracker into .asm files. It can be considered an alternative to uge2source, tailored to fortISSimO.

Before talking about how to use it, here is teNOR’s built-in short help text:

$ ./teNOR -h
Exports hUGETracker `.uge` files for fortISSimO

Usage: teNOR [OPTIONS] <INPUT_PATH> [OUTPUT_PATH]

Arguments:
  <INPUT_PATH>   Path to the `.uge` file to be exported
  [OUTPUT_PATH]  Path to the `.asm` file to write to

Options:
  -i, --include-path <INCLUDE_PATH>
          Path to include file to emit [default: fortISSimO.inc]
  -t, --section-type <SECTION_TYPE>
          Type of the section that the data will be exported to; if omitted, no
          SECTION directive will be emitted
  -n, --section-name <SECTION_NAME>
          Name of the section that the data will be exported to [default: "Song
          Data"]
  -d, --song-descriptor <SONG_DESCRIPTOR>
          Name of the label that will point to the song's header (hUGETracker
          calls this the "song descriptor")
  -q, --quiet
          Do not emit stats at the end
      --color <COLOR>
          Use colours when writing to standard error (errors, stats, etc.) [default:
          auto] [possible values: always, auto, never]
  -h, --help
          Print help (see more with '--help')
  -V, --version
          Print version
1

This is totally not a backronym. What? …You don’t believe me?

Usage

teNOR is a command-line program; to use it, you should at least know how to open a terminal, change directories in it, and execute programs. (An alternative might be to use .bat files on Windows / .sh files anywhere else.)

I believe the core usage should be simple enough, so let’s talk about some of the options.

Song descriptor

The “song descriptor” is the label that will have to be passed to hUGE_SelectSong later. Since it is a label, it must be a valid RGBASM symbol name (regex: [A-Za-z_][A-Za-z0-9_#]*), and since it will be exported, it must be unique across the entire program.

Stats

teNOR tries to optimise the exported data to take less space. When it’s done running, it prints statistics about how much space the optimisations saved; this was originally done to check if they were worth the trouble, and then kept because honestly, why not?

If you don’t care about the stats, pass the -q/--quiet option to silence them.

Note that the reported savings are not the difference with the size of an equivalent hUGEDriver export, due to other, more fundamental format differences. Unoptimised fortISSimO exports should be smaller than hUGEDriver exports; how much varies from version to version.

Output file

teNOR aims to produce output files that are easy to understand and nicely formatted. If you want to read the generated file, go ahead!

Integration

Integrating fortISSimO into your project depends on what toolchain you are using; please go to the appropriate page for detailed instructions.

The following, however, is independent of the toolchain.

Debugfile support

fortISSimO supports debugfiles, which enable emulators to perform some run-time checks for free. This can help catch bugs in fortISSimO, songs, or custom routines.

Define a PRINT_DEBUGFILE symbol (e.g. by passing -DPRINT_DEBUGFILE to rgbasm) to have the debugfile printed to standard output.

So, for example:

$ rgbasm src/fortISSimO.asm -I src/include -DPRINT_DEBUGFILE >obj/fortISSimO.dbg

Tuning fortISSimO

fortISSimO supports a bit of configuration without having to modify fortISSimO.asm, which would make upgrading more difficult.

The following symbols can/must be defined when assembling fortISSimO.asm:

NameKindDefaultFunctionality
FORTISSIMO_ROMString constantROM0Attributes for fortISSimO’s ROM section.
Example: ROMX, BANK[42].
If empty, no SECTION directive will be emitted, which can be useful if doing INCLUDE "fortISSimO.asm".
FORTISSIMO_RAMString constantWRAM0Attributes for fortISSimO’s RAM section.
Example: WRAMX, ALIGN[4].
FORTISSIMO_CH3_KEEPAnyNot definedIf any symbol by this name is defined, then fortISSimO will not remove CH3 from NR51 temporarily while writing to wave RAM. This may make the process sound slightly “clicky”, but allows hUGE_TickSound to be safely interrupted by code that writes to NR51.

Integrating fortISSimO into a RGBDS project

Using fortISSimO, like many libraries, has three parts to it.

Global init

fortISSimO has a few variables that must be initialised before some routines are called. Forgetting to do so should result in uninitialised RAM being read (which your emulator is probably configured to warn you of). The ideal time to initialise those is right after booting (example).

Selecting a track

Here comes hUGE_SelectSong! This function simply needs to be called with the song’s label in de (example).

This function’s relationship with the APU is as follows:

  • This function does not touch NR52, so you must turn the APU on yourself (typically as part of the global init above, see this example).

  • This function does not touch NR51 or NR50 either; if your songs make use of panning, they should include 8xx and/or 5xx effects on their first row to reset those registers.

    Keep in mind that 8xx and 5xx are global, and thus affect sound effects as well!

  • This function mutes every channel that is “owned” by the driver; if you do not want this (for example, to join two tracks seamlessly), set hUGE_MutedChannels to e.g. $0F before calling hUGE_SelectSong.

Additionally, hUGE_TickSound must not run in the middle of this function! This can happen if it is called from an interrupt handler, notably. The recommended fix is to “guard” calls to hUGE_TickSound, like this:

	xor a
	ldh [hMusicReady], a
	ld de, BossFightMusic
	call hUGE_SelectSong
	ld a, 1
	ldh [hMusicReady], a
	; In the interrupt handler:

	ldh a, [hIsMusicReady]
	and a
	call nz, hUGE_TickSound

Another possibility is to disable interrupt handlers (usually with di and ei) while hUGE_SelectSong is running; this can have side effects that affect your game, and is therefore not recommended.

Playback

hUGE_TickSound is the function whose use requires the most attention. Calling this function steps playback forward by 1 tick… which is the most fundamental unit of time in hUGETracker!

A given track expects this function to be called on a specific schedule, otherwise it will sound wrong. Imagine playing a MP3 file at 1.5× speed, for example—that’s not quite it, but close.

The schedule is simple:

  • If “Enable timer-based tempo” was not selected in hUGETracker, then hUGE_TickSound must be called once per frame. This is most often done from an interrupt handler (preferably STAT to save VBlank time, but VBlank is fine too), but can also be done in the main loop.
  • If “Enable timer-based tempo” was selected in hUGETracker, then hUGE_TickSound must be called at a fixed rate. This rate can be obtained by setting TAC to 4 (4096 Hz) and TMA to the value in the “Tempo (timer divider)” field, or any equivalent method.

Timer-based tempo can have annoying side effects to the rest of the game’s programming, so VBlank-based tempo is recommended.

GBDK

fortISSimO is written with GBDK compatibility in mind. Unfortunately, no current version of rgb2sdas is able to handle the conversions required by fO, although it is known to be feasible.

Two solutions are possible:

  • The GBDK team switches to using RGBLINK as a back-end, to enable mixing RGBASM and SDAS object files in a single project.
  • Someone upgrades rgb2sdas or writes their own conversion script that can handle the necessary conversions.

If you are interested in helping with either solution, please contact me.

Sound effects

fortISSimO does not include a built-in sound effect engine. However, it has functionality to cooperate with any sound effect engine you want (here is one): if a channel is “muted”, then fortISSimO will never access any of that channel’s registers; this leaves it available for any other code, such as a sound effect engine!

A channel is considered “muted” if its corresponding bit is set in hUGE_MutedChannels; bit 0 controls CH1, bit 1 controls CH2, bit 2 controls CH3, and bit 3 controls CH41. (The constants hUGE_CHx_MASK are available (with x between 1 and 4) for your convenience.)

While a channel is “muted”, all of its effects are processed, but any writes to hardware registers are discarded. This means that “global” effects, such as 5xx, 8xx, Fxx, etc. are still applied properly.

When a channel is un-“muted”, fortISSimO waits until a new “full” note (with instrument) is played on it to resume; this strategy avoids playing any corrupted sounds by accident, but can cause a channel to remain muted for a long time depending on the song’s structure.

1

The upper four bits of hUGE_MutedChannels are currently unused by fortISSimO; they may be repurposed in a future version, so for future-proofing/forward-compatibility, it is advisable not to touch them if possible.

Wave RAM

The wave channel needs one extra precaution: if wave RAM is written to while CH3 is “muted”, fortISSimO must be informed by setting hUGE_LoadedWaveID to the constant hUGE_NO_WAVE. This will force it to reload wave RAM the next time a note is played on CH3.

Routines

Routines are an advanced feature of fortISSimO, which allow you to execute custom code at arbitrary points in a track’s playback.

fortISSimO diverges from hUGEDriver here in major ways: hUGEDriver supports 16 routines, fortISSimO supports only a single one; teNOR completely ignores the routines defined in the .uge file; and the interfaces provided to routines are very different.

Setup

teNOR places each track’s routine pointer at the very end of the generated .asm file. So, to define a routine, it’s sufficient to INCLUDE the file and write the routine’s code right after:

INCLUDE "exports/boss_music.asm"
	; `600` disables the boss' invunlnerability, `601` enables it.
	ld a, b
	ld [wBossInvuln], a
	ret

Interface

Routines are meant to be written in assembly code; no C wrapper is provided. The routine is passed the entire effect’s argument in the b register; no part of the argument is special, since there is only a single routine per song.

It is possible to use the argument to dispatch between several sub-routines, and even to mimic hUGEDriver’s behaviour; for example:

INCLUDE "exports/final_boss_music.asm"
	ld a, b
	and $F0
	jr z, .changeInvuln

	; `610` charges the boss' attack.
	; (This can run a lot more than you expect—
	;  please see the caveat in the next section.)
	ld hl, wBossChargeCounter
	inc [hl]
	ret

.changeInvuln
	; `600` disables the boss' invulnerability, `601` enables it.
	ld a, b
	ld [wBossInvuln], a
	ret

Additionally, hl points at the routine itself, de points at the channel’s note byte (wCHx.note), and c contains the channel’s mask (hUGE_CHx_MASK). The flags are not significant.

When is the routine called?

Each active 6xx effect causes the routine to be called once every tick, not just the first one! Effects can come from the “main grid”, where they are active for as many ticks as the tempo specifies, and/or from subpatterns, where they are only active for a single tick.

fortISSimO, unlike hUGEDriver, does not expose a tick counter by default. You will have to poke at the driver’s internals to obtain one.

Driver internals

This does not aim to document every single inner working of fortISSimO—there are too many of those, and too few people would be interested—it only contains descriptions of what each variable does, since those are at least likely to be useful to people writing routines.

Though I’m not opposed to the scope being expanded, if someone is interested.

Last updated as of commit 7fb8329. This is susceptible to having changed since then; see the changes since then, particularly the file src/fortISSimO.asm.

Be careful of accessing these variables yourself if fortISSimO runs in an interrupt handler! It may be possible for fortISSimO to execute between your code reading different bytes, and thus end up with an inconsistent state!

Note that the variable names follow this naming guide, except for variables exported by default (those with the hUGE_ prefix).

Song “cache”

All of the following variables are merely copied from the song header when hUGE_SelectSong is run, no processing applied! (Their names should be self-explanatory as to what they contain.) They can be modified at any time between two executions of hUGE_TickSong, and will take effect the next time they are read.

VariableAccessed when?
wTicksPerRowOn tick 0 of a new row.
wLastPatternIdxWhen “naturally” switching to the next pattern.
wDutyInstrsOn tick 0 of a new row, if such an instrument must be loaded.
wWaveInstrsOn tick 0 of a new row, if such an instrument must be loaded.
wNoiseInstrsOn tick 0 of a new row, if such an instrument must be loaded.
wRoutineEvery time a “call routine” effect is executed.
wWavesWhen an instrument with a new wave (see hUGE_LoadedWaveID) is loaded, and every time a “change timbre” effect is executed on CH3.

Global variables

  • hUGE_LoadedWaveID: ID of the wave the driver currently thinks is loaded in RAM, or hUGE_NO_WAVE for “none”.
  • wArpState: Which offset arpeggios should apply on this tick (1 = none, 2 = lower nibble, 3 = upper nibble); decremented before every tick.
  • wRowTimer: Decremented before every tick, and if it reaches 0, a new row is switched to.
  • wOrderIdx: Offset in bytes within the order “columns”; since every entry (a pointer) is 2 bytes, this is always a multiple of 2.
  • wPatternIdx: Which row in the current patterns is active, with bits 7 and 6 set. Incremented at the beginning of a tick where a new row is played.
  • wForceRow: When switching to a new row, if this is set, this will be written to wPatternIdx instead of it being incremented.

Channels

The channel variables are grouped under four structures, named wCH1, wCH2, wCH3, and wCH4; each member variable is a local label.

Common

The following is common to all channels:

  • .order: Pointer to this channel’s order “column”. Kind of part of the song “cache”, but per-channel. Read every time a new row is switched to.
  • .fxParams, .instrAndFx, .note: These cache the active row. (This avoids having to re-calculate the pointer to it and re-read it every time.)
  • .subPattern: Pointer to the active instrument’s subpattern, or 0 if disabled.
  • .subPatternRow: Index of the active row in the subpattern (0–31).
  • .lengthBit: This is OR’d into all bytes written to NRx4.

Not CH4

The following is present in wCH1, wCH2, and wCH3, but not wCH4:

  • .period: The current base “period”, in the format that will get written to NRx3/NRx4. May not reflect what was last written to those registers, e.g. vibrato writes to those but not to this.
  • To save some space, the following variables overlap, since they can’t be used concurrently:
    • Used while a “tone porta” effect is active on this channel:
      • .portaTarget: The “period” that is being slid towards. This is redundant with .note, but serves as a cache.
    • Used while a “vibrato” effect is active on this channel:
      • .vibratoOffset: How much must be added to .period when writing to NRx3/NRx4.
      • .vibratoState: The upper nibble counts down before each tick, and the direction is flipped when it underflows. Bit 0 specifies the direction: clear when the period is increasing, and set when it’s decreasing.
      • .vibratoPrevArg: If the previous row contained a vibrato, then this contains its argument; if not, then this has its lower nibble set to 0.

CH4

The following is present on wCH4 and only it.

  • .lfsrWidth: The “LFSR width” bit, in NR43 format.
  • .polynom: The current “polynom”, in NR43 format (all bits but bit 3).

Song format

This describes the format that songs are stored in fortISSimO at the binary level. The format of files exported by teNOR is kind of irrelevant, and the format of hUGETracker’s .uge files is documented elsewhere.

Last updated as of commit 7fb8329. This is susceptible to having changed since then; see the changes since then, particularly files teNOR/export.rs and include/fortISSimO.inc.

ℹ️ For forward compatibility’s sake, it is unwise to assume that components will always be in a certain order unless otherwise specified.

For example, currently, teNOR emits duty instruments immediately after the “row pool”, and right before the wave instruments; this should be considered an unstable implementation detail. However, all fields of the song header will remain in the specified order (until the next major release of fO, anyway).

Unless specified:

  • there is no padding between any of the structures’ fields,
  • all multi-byte values are stored in little-endian format (low byte first).
  1. BYTE — How many ticks each row lasts for.
  2. BYTE — The maximum value wOrderIdx can take; inclusive. Also specifies the size of the pattern pointer arrays, below.
  3. POINTER — To the array of duty instruments.
  4. POINTER — To the array of wave instruments.
  5. POINTER — To the array of noise instruments.
  6. POINTER — To the song’s routine.
  7. POINTER — To the array of waves.
  8. For each channel, from 1 to 4: its column of the order matrix:
    1. As many as specified above:
      1. POINTER — To a pattern.

Patterns

A pattern is simply a collection of rows; teNOR attempts to find overlap between the patterns to minimise how much space they take; thus, all patterns are coalesced into sort of a “pool of rows”.

There is no definitive end to the row pool—simply, only as many rows are emitted as are necessary.

Rows

Rows are constitued differently depending on whether they belong to a “main” pattern, or a subpattern.

Pattern rows

  1. BYTE — The effect’s parameter.
  2. BYTE — Split as follows:
    • UPPER NIBBLE — The instrument ID, or 0 for “none”.
    • LOWER NIBBLE — The effect ID.
  3. BYTE — The note’s ID, or 90 for “no note”.

Subpattern rows

  1. BYTE — The effect’s parameter.
  2. BYTE — Split as follows:
    • UPPER NIBBLE — Bits 0–3 of the next row’s ID.
    • LOWER NIBBLE — The effect ID.
  3. BYTE — Split as follows:
    • BIT 7 — Bit 4 of the next row’s ID.
    • BITS 0–6 — The offset from the base note, plus 36; or 90 for “no offset”.

In order to allow looping back on the same row as an effect, subpatterns rows all have a built-in jump target. Since there are 32 possible rows to jump to, 5 bits are needed—the unused 7th bit of the note byte is used to store that 5th bit.

Effects

Effect IDs are unchanged from hUGETracker. The effect parameter is, however, sometimes different.

EffectID (hex)Stored parameter
Arpeggio0Unchanged.
Porta up1Unchanged.
Porta down2Unchanged.
Tone porta3Unchanged.
Vibrato4Unchanged.
Set master vol5Unchanged.
Call routine6Unchanged.
Note delay7Unchanged.
Set panning8Unchanged.
Change timbre9Unchanged.
Vol slideAUnchanged.
Pos jumpBThe pattern ID is stored in wOrderIdx format.
Set volCNibbles are swapped from hUGETracker.
Pattern breakDThe row ID is stored in wForceRow’s format.
Note cutEUnchanged.
Set tempoFUnchanged.

Instruments

Instruments are grouped in “banks” by their type. Each bank is an array of up to 15 instruments, with no padding in-between.

Duty

  1. BYTE — Frequency sweep, in NR10 format.
  2. BYTE — Duty & length, in NR11/NR12 format.
  3. BYTE — Volume & envelope, in NR12/NR22 format.
  4. POINTER — Pointer to the subpattern, or 0 if not enabled.
  5. BYTE — Control bits:
    • BIT 7 — Always set.
    • BIT 6 — Whether the “length” is enabled.

Wave

  1. BYTE — Length, in NR31 format.
  2. BYTE — Volume, in NR32 format.
  3. POINTER — Pointer to the subpattern, or 0 if not enabled.
  4. BYTE — Control bits:
    • BIT 7 — Always set.
    • BIT 6 — Whether the “length” is enabled.
  5. BYTE — ID of the wave to load.

Noise

  1. BYTE — Volume, in NR42 format.
  2. POINTER — Pointer to the subpattern, or 0 if not enabled.
  3. BYTE — Control bits:
    • BIT 7 — 0 if the LFSR should be in “long” (15-bit) mode, 1 if the LFSR should be in “short” (7-bit) mode.
    • BIT 6 — Whether the “length” is enabled.
    • BITS 0–5 — Length, in NR41 format.

Waves

Each wave is an array of 16 bytes, stored directly in wave RAM format.

There are up to 16 waves; wave IDs start at 0.

Routine

See the dedicated chapter.