Foreword
Thank you for choosing fortISSimO! First off, please select one of our pricing plans:
Tier | hUGETracker compat | Small | Fast | Coffee |
---|---|---|---|---|
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-msvc
1 - Linux:
x86_64-unknown-linux-gnu
1 - 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.
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.
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
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.
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:
-
Locate the
hUGEDriver
directory next to thehUGETracker.exe
you want to “mod”; we will be overwriting some files in there. -
Copy
fortISSimO.asm
into that directory ashUGEDriver.asm
. -
Copy
fortISSimO.inc
into theinclude
directory ashUGE.inc
. -
Modify
hUGEDriver.asm
(which is now justfortISSimO.asm
in disguise). Look for the following line, at or near line 11:; def HUGETRACKER equs "1.0"
- Make sure between the quotes is the version of hUGETracker that you are using.
- 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.
-
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, andy
its slope: forx
ticks, the frequency will be increased byy
units each tick; then forx
ticks, the frequency will be decreased byy
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.
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:
- Exporting your songs
- Playing your songs back:
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
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
:
Name | Kind | Default | Functionality |
---|---|---|---|
FORTISSIMO_ROM | String constant | ROM0 | Attributes 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_RAM | String constant | WRAM0 | Attributes for fortISSimO’s RAM section. Example: WRAMX, ALIGN[4] . |
FORTISSIMO_CH3_KEEP | Any | Not defined | If 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).
hUGE_MutedChannels
must have been written to (usually to 0, but see the chapter about sound effects) beforehUGE_TickSound
is ever called.
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
orNR50
either; if your songs make use of panning, they should include8xx
and/or5xx
effects on their first row to reset those registers.Keep in mind that
8xx
and5xx
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 callinghUGE_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 settingTAC
to 4 (4096 Hz) andTMA
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.
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 filesrc/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.
Variable | Accessed when? |
---|---|
wTicksPerRow | On tick 0 of a new row. |
wLastPatternIdx | When “naturally” switching to the next pattern. |
wDutyInstrs | On tick 0 of a new row, if such an instrument must be loaded. |
wWaveInstrs | On tick 0 of a new row, if such an instrument must be loaded. |
wNoiseInstrs | On tick 0 of a new row, if such an instrument must be loaded. |
wRoutine | Every time a “call routine” effect is executed. |
wWaves | When 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, orhUGE_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 towPatternIdx
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.
- Used while a “tone porta” effect is active on this channel:
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 filesteNOR/export.rs
andinclude/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).
Header
- BYTE — How many ticks each row lasts for.
- BYTE — The maximum value
wOrderIdx
can take; inclusive. Also specifies the size of the pattern pointer arrays, below. - POINTER — To the array of duty instruments.
- POINTER — To the array of wave instruments.
- POINTER — To the array of noise instruments.
- POINTER — To the song’s routine.
- POINTER — To the array of waves.
- For each channel, from
1
to4
: its column of the order matrix:- As many as specified above:
- POINTER — To a pattern.
- As many as specified above:
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
- BYTE — The effect’s parameter.
- BYTE — Split as follows:
- UPPER NIBBLE — The instrument ID, or 0 for “none”.
- LOWER NIBBLE — The effect ID.
- BYTE — The note’s ID, or 90 for “no note”.
Subpattern rows
- BYTE — The effect’s parameter.
- BYTE — Split as follows:
- UPPER NIBBLE — Bits 0–3 of the next row’s ID.
- LOWER NIBBLE — The effect ID.
- 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.
Effect | ID (hex) | Stored parameter |
---|---|---|
Arpeggio | 0 | Unchanged. |
Porta up | 1 | Unchanged. |
Porta down | 2 | Unchanged. |
Tone porta | 3 | Unchanged. |
Vibrato | 4 | Unchanged. |
Set master vol | 5 | Unchanged. |
Call routine | 6 | Unchanged. |
Note delay | 7 | Unchanged. |
Set panning | 8 | Unchanged. |
Change timbre | 9 | Unchanged. |
Vol slide | A | Unchanged. |
Pos jump | B | The pattern ID is stored in wOrderIdx format. |
Set vol | C | Nibbles are swapped from hUGETracker. |
Pattern break | D | The row ID is stored in wForceRow ’s format. |
Note cut | E | Unchanged. |
Set tempo | F | Unchanged. |
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
- BYTE — Frequency sweep, in NR10 format.
- BYTE — Duty & length, in NR11/NR12 format.
- BYTE — Volume & envelope, in NR12/NR22 format.
- POINTER — Pointer to the subpattern, or 0 if not enabled.
- BYTE — Control bits:
- BIT 7 — Always set.
- BIT 6 — Whether the “length” is enabled.
Wave
- BYTE — Length, in NR31 format.
- BYTE — Volume, in NR32 format.
- POINTER — Pointer to the subpattern, or 0 if not enabled.
- BYTE — Control bits:
- BIT 7 — Always set.
- BIT 6 — Whether the “length” is enabled.
- BYTE — ID of the wave to load.
Noise
- BYTE — Volume, in NR42 format.
- POINTER — Pointer to the subpattern, or 0 if not enabled.
- 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.