Bitphase export format

This document is the reference for how SampleToNES writes Bitphase files: the .btp document and the .json instrument preset. It also lists the Bitphase capacity limits the exporter respects. Read it when you write or check a .btp document or a .json preset. The sibling FamiTracker export document covers the other tracker. The tracker playback check plays exported documents through Bitphase's own engine and lists every tick they sound differently from the app.

The target is Bitphase's NES (2A03) chip: five channels (two squares, triangle, noise, DPCM). The DPCM channel is always silent. Every constant named here has a counterpart under sampletones_core/formats/bitphase/specification/, grouped by unit (chip, channels, instruments, patterns).

Bitphase plays a note with three columns acting together. An instrument supplies the per-tick register values. A table supplies the per-tick pitch movement. The note column supplies the pitch they move around. This shapes the whole mapping: a reconstruction's volume and duty envelopes become the instrument, its arpeggio envelope becomes the table, and its reference pitch becomes the note.

▍A. File formats

A.1 .btp — the document

A .btp is the document's JSON under gzip, with no header and no version field. The exporter writes it without separator padding and with a fixed gzip timestamp, so exporting an unchanged document twice yields identical bytes.

Project    { name, author, songs[], loopPointId, patternOrder[], tables[],
             patternOrderColors{}, instruments[] }
Song       { patterns[], tuningTable[], initialSpeed, tempo, defaultPatternLength,
             chipType, chipVariant, chipFrequency, interruptFrequency, a4TuningHz,
             virtualChannelMap{} }
Pattern    { id, length, channels[], patternRows[] }
Channel    { rows[], label, effectColumnCount }
Row        { note: { name, octave }, effects[], instrument, table, volume }
Effect     { effect, delay, parameter, tableIndex }
Table      { id, rows[], loop, name, additive }
Instrument { chipType, name, macros{ field: { values[], loop } }, id }

The project owns the instruments and tables, so every song addresses the same lists. patternOrder names the pattern each order position plays. loopPointId is the order position playback returns to.

Field names are camelCase, as in Bitphase's own files.

A channel has as many effect columns as its widest row, up to four, and Bitphase gives a channel the same count in every pattern it appears in. An effect reads its argument from a table whenever tableIndex is zero or above, and from parameter otherwise, so an effect driven by its parameter writes tableIndex as -1. An empty value counts as naming the first table, and the effect is then dropped.

A.2 .json — the instrument preset

Bitphase's instruments panel saves and loads a single instrument through a file picker. The file holds { chipType, name, macros }, indented the way Bitphase writes its own, so a preset written here reads like one saved from the tracker. The panel writes the macros into the instrument slot the reader has selected, and that slot gives the instrument its id and its chip.

A preset has macros only, so its pitch movement goes in the toneAdd each tick takes (section C.3) instead of a table.

▍B. The NES instrument

An instrument is a set of macros, one per field, and each macro gives that field a value per engine tick while a note sounds. A field with no macro takes the default below, so an instrument writes a macro only for the fields a reconstruction decides. The fields match Bitphase's own:

Field Range Default Runtime meaning What the exporter writes
volumeOrRate 0–15 15 the literal channel volume while envelope stays off the volume envelope, or one full level where the slice leaves its volume to the channel
pulseWidth 0–3 2 square duty cycle; on the noise channel, the lowest bit selects the short LFSR the duty-cycle envelope (squares), the short or long mode (noise), or one 0 where the slice leaves its duty to the channel; the triangle writes no macro
toneAdd −4096–4095 0 period offset added to the period the note resolves to (squares and triangle), or steps added to the note (noise) the bend the slice sounds (section C.4), and the contour with it in a preset
envelope bool false reads volumeOrRate as a hardware decay rate no macro, so each value is the volume itself
soundLength 0–511 0 length counter in ticks; 0 holds the note no macro, so the volume envelope alone shapes the note
toneAccumulation bool false sums toneAdd across ticks no macro, so each value is a whole offset
retrigger bool false restarts the waveform phase this tick no macro, so the waveform runs continuously
sweep / sweepRate / sweepShift bool / 0–7 / −7–7 false / 0 / 0 the square channel's hardware sweep no macro, so the sweep stays off

Every field has its own counter. A macro has the values one field takes and the index they repeat from, and Bitphase advances each macro on its own. A field whose value never changes therefore costs one value, however long the other fields run.

Looping. Playback returns to the macro's loop index once the values run out. That is the only mode. A macro whose loop is 0 repeats whole, and one whose loop is its last index stays on the last value. A dimension the reconstruction gives a repeat point repeats from that point. A dimension that plays through writes its last index, and the value it ends on is what the note rests on: silence where the volume envelope ends on a note-off item, the channel's own level where the slice leaves the volume alone.

An instrument's slices. Bitphase bakes a channel's registers tick by tick. An instrument therefore reaches a document as one slice per channel it sounds on. Each slice reads the dimensions that channel offers and moves around the pitch the instrument states. The envelopes are one set for every channel, so the slices differ only in what each channel reads of them.

A held volume. A slice whose volume envelope has no item leaves its level to the channel. The exporter writes one full volumeOrRate. Playback combines that level with the pattern's volume column, and a full level comes out at the column's own level (section E). The slice therefore sounds at whatever level the channel has, which is how FamiTracker reads a disabled volume sequence. A slice that describes no frame at all writes a single silent value, the smallest instrument Bitphase plays.

Every note starts where a song does. Bitphase reads every field from the instrument a note plays, from its first tick, and every note cell the exporter writes names its slice's own table. A slice that leaves a dimension to the channel writes the value SampleToNES starts a note on: a full level, a flat table, no tone offset, and a 0 pulse width, which is the long mode on noise. The pulse width needs its macro, because a field with none takes Bitphase's default of 2. The note therefore sounds the same whatever played before it, as it does in the app and in FamiTracker.

A table runs beside the macros. The table advances one step per tick on a counter of its own, so the contour keeps the length and the repeat point the arpeggio envelope was written at, whatever the macros beside it do.

▍C. Pitch

C.1 The tuning table

A song has a 96-entry tuningTable, one channel period per note index. It is a port of Bitphase's generate12TETTuningTable:

frequency = a4TuningHz * 2 ^ ((index - 45) / 12)
period    = round(chipFrequency / 16 / frequency)   clamped to 1..2047

Rounding matches JavaScript's Math.round (half away from zero on positives), so a table built here equals the one Bitphase derives from the same settings. The exporter writes NTSC (1 789 773 Hz). PAL (1 662 607 Hz) and Dendy (1 773 448 Hz) are named in specification/chip.py.

The song plays at the work's tuning. a4TuningHz is the frequency the reconstruction's tuning gives pitch 69, the pitch at index 45. A tuning whose reference names another pitch is read out at pitch 69 all the same. The table is built from that frequency. Bitphase builds the table again from a4TuningHz and chipFrequency when it loads a song (resolveTuningTable in src/lib/chips/nes/schema.ts), so the exporter writes the two fields in step. An instrument or a reconstruction takes its own tuning. A project takes the tuning its samples were reconstructed at, and a project with no sample plays at A4 = 440 Hz. One table sounds one tuning, so a project whose samples were reconstructed at different tunings is refused, as the NSF export refuses it.

A note index is the absolute pitch less 24. Indices 0–95 cover pitches 24–119, the span a FamiTracker note cell covers too. A pattern cell stores the index as a semitone and an octave, which playback resolves back with name - 2 + (octave - 1) * 12. At concert pitch the nine indices below pitch 33 ask for a period longer than the register holds. SampleToNES plays pitches 24–119 and sounds each of those nine at the longest timer the register holds, 0x7FF, as FamiTracker does. Bitphase's tuning table holds a period at 2047 at most, which is timer 2046, so those notes sound one step higher there. Its engine plays periods up to 2048, so a bend reaches 0x7FF (section C.4).

The triangle channel's period comes from the same table, so a written note sounds an octave below. SampleToNES and FamiTracker share that convention.

C.2 Tables carry the contour

A table has one semitone offset per tick, and playback adds rows[position] to the channel's note every tick. It repeats from loop once the steps run out, by the rule a macro repeats by. That matches a reconstruction's arpeggio envelope in absolute mode, so the contour crosses over verbatim on the pitched channels, with the repeat point the envelope was written at. A table whose additive flag is set adds each step to the one before it; a contour measures every step from the note, so the flag stays clear.

A pattern's table column names a table by id + 1. 0 leaves the attached table alone and -1 detaches it.

Noise derives its period from the note index, not from the tuning table: playback writes 15 - (index mod 16) to the period register, whose timers run fastest first. SampleToNES counts its sixteen periods from the slowest, so a period p reaches the register as 15 - p, which is what the NSF player writes too. The note index therefore carries the period itself. Every period repeats once per sixteen indices, and the exporter picks a base index far enough below the top of the table for a whole cycle of offsets to stay in range:

base index   = 48 + (initial_period mod 16)      lands in 48..63
table offset = arpeggio_step mod 16              lands in 0..15

So (base + offset) mod 16 is the period the reconstruction chose, wrapped into the sixteen the channel has, and the register holds 15 less that period.

C.3 Presets fold the contour into the period

An instrument preset has no table, so its pitch movement is the per-tick toneAdd each tick applies to the note's own period. One offset carries the contour and the bend together. The offsets are measured against the pitch the slice was reconstructed at, under the tuning a freshly created Bitphase document plays: NTSC at concert pitch. A preset loads into a document that keeps its own tuning, so a preset stays at concert pitch whatever the reconstruction was tuned at. On the noise channel the offset moves the note, which carries the period itself, so a noise preset's offset is the contour step and the bend added together, in period steps.

C.4 The bend rides the tone offset

A reconstruction says which note a frame plays and how far from that note it sounds, in steps of the channel's own timer. That distance is the bend, written as a fine dimension of one step per unit and a coarse one of sixteen. Bitphase counts a period where SampleToNES counts a timer, and the two differ by one step across the table, so a distance in steps crosses over unchanged. The bend is the toneAdd macro, one value per tick.

What the engine does What the exporter writes
moves the note by the table step, then reads that note's period each value is measured from the note its own contour step reaches, so a transposed trigger keeps its bend
adds toneAdd to that period the two bend dimensions added together, one value per tick
leaves toneAccumulation clear a whole offset per tick, not a step added to a running one
holds the period within 0–2048, silences a channel at zero, and loads the timer with the period less one an offset that keeps the timer within 1–0x7FF, as in-app playback bends it, measured from the period the song's own table gives the note

The squares and the triangle add the offset to the period. The noise channel adds it to the note, which carries the period itself, so a noise slice's bend is written as it stands: one period step per unit. A slice that sounds every tick on its own note writes no toneAdd, so a document pays only for the bends it sounds.

▍D. Tempo as a groove

A Bitphase song has a speed, the ticks each row lasts. A SampleToNES project has a tempo and a speed together. The row rate the pair asks for is fractional at most tempi, so the exporter writes it as a groove: whole tick counts, one per row, averaging out to that rate, placed as song timing places them. In-app playback reads the same timing, so a document plays the rows the sequencer played. For example, at 60 Hz with speed 6 and tempo 210, a 16-row pattern in common time lasts 68 4/7 ticks, so the song's first two frames play:

5 4 5 4 5 4 4 4 5 4 4 4 5 4 4 4      69 ticks
5 4 4 4 5 4 4 4 5 4 4 4 5 4 4 4      68 ticks

A Bitphase song has a tempo too. Above 0, Bitphase spreads the ticks over the rows the way FamiTracker's tempo does, with a running count from row to row. At 0, the speed alone sets each row. The exporter writes 0 and places the ticks itself, by bar and by beat.

The groove reaches the engine as speed effects. A speed effect sets the ticks a row lasts from its row on, so the exporter writes one wherever a row lasts differently from the row played before it:

Part What the exporter writes
tempo 0, so the speed alone sets the ticks of every row
initialSpeed the ticks the song's first row lasts
The effect S with delay = 0, the row's ticks as its parameter, and tableIndex = -1
Its place the DPCM channel, on every row whose length differs from the row before it

The order comes round to its first pattern after the last, so the song's last row counts as the row before its first. A speed effect applies from whichever channel has it, so the groove rides the DPCM channel, which this exporter leaves silent. Every sounding channel keeps its own effect column free. Bitphase finds the speed a row it starts playing from lasts by reading back to the last speed effect, so a song started anywhere plays every row at its length.

A tempo the speed column can state needs no groove. Where every row lasts alike, as with tempo 150 at 60 Hz where the rate is the speed itself, initialSpeed has the tempo whole. The document then has one table per slice, and every effect column is empty.

▍E. What the exporter builds per scope

A .btp is a whole document, so every scope lands in one file. A preset is one instrument, so a reconstruction lands as a set of presets beside the name the export was given, one per slice.

Scope .btp .json preset
One channel slice a playable document holding that instrument one file
A whole reconstruction a playable document holding every slice one file per slice, beside the chosen name
A project the song, its samples and its arrangement —

Instrument and reconstruction documents are playable. Each slice becomes an instrument plus the table that carries its contour. One pattern triggers every slice at row 0 on the channel it was reconstructed for, so opening the document and pressing play sounds the reconstruction. The pattern is sized to cover the longest instrument. Where an instrument outlasts a single pattern, the order gains resting positions until it has played through.

A project flattens its order. A SampleToNES order frame points each channel at its own pattern, while a Bitphase order position names one pattern spanning every channel. Each frame therefore becomes a pattern of its own with that frame's channels side by side, and patternOrder = [0..n-1]. The arrangement crosses over whole and shares fewer patterns.

Row cells follow from the columns. An instrument command writes the note from initial_pitch + transpose, the instrument number, the table column and the volume column below. A note-off writes note name 1. A blank line leaves every column alone.

The note keeps the song's pitch range. In-app playback holds every tick's transposed pitch within 24–119, the span of the tuning table. Playback moves the written note by the table's step each tick and holds the result at index 0 (pitch 24) and index 95 (pitch 119), so a note within the span is written as it is and a higher one is written at 119. Bitphase writes no note below index 0, so a lower one is raised only as far as bringing the table's highest step to pitch 24:

Row What the exporter writes
a flat slice transposed below 24 pitch 24, the note in-app playback sounds
a contour whose steps stay at its note or below the note whose highest step lands on 24
a contour rising above a note below 24 pitch 24, where the ticks the contour raises sound higher than in-app playback

A transpose row moves the note sounding. In-app playback reads a row that states a transpose and no note as a new pitch for the note already sounding: the voice goes on from the tick it reached, and every tick from that row on sounds at the new transpose. A note-on starts over at its own transpose. Bitphase restarts the instrument on a note or an instrument cell, and a table cell alone attaches a table at its first step while the instrument plays on. So the row writes a table cell and an effect:

Column What the exporter writes
Table a copy of the note's table with every step moved by N, the distance from the note already written to the note a note-on at the new transpose would write
Effect 5 (ornament position), delay = 0, the parameter naming the step the note's table has reached, which the first step leaves out
Note, instrument empty, so the instrument goes on

N is measured from the note the note-on wrote, whatever an earlier row moved the note to, so each row's pitch is the transpose it states. From that tick the channel sounds what a note-on at the new transpose would sound, the low-note rule above included. On noise N is measured the same way, between the two note indices the period mapping of section C.2 writes, so the moved table walks the period around the sixteen the channel has and follows that mapping wherever it places a period. A row returning to the note's own transpose names the note's own table, a row keeping the transpose in force writes nothing, and rows moving one slice by one distance share one table.

The note's table advances a step per tick from the note on, so the step the effect names counts the ticks every row since the note lasts, the groove's uneven rows and the frames between included. The effect reads its parameter as a byte, so it names steps 0–255. A row reaching a later step names a copy that opens on that step: its steps run from there and circle over the steps the note's table circles over, and the row writes no effect.

A transpose row reached while no note sounds moves nothing and writes nothing, and neither does one following a note-on that was written as a note cut.

An instrument placed without a pitch takes the channel's. In-app playback starts such an instrument on the pitch the channel is sounding, whichever voice sounded it, and leaves a silent channel silent. Each frame is a pattern of its own, so the cell writes the note the channel was sounding in that frame beside the instrument, and a row that starts nothing writes an empty cell.

A note starts at the full level. In-app playback starts a note whose row states no volume at the full level. Playback carries the level a channel last took into every note after it. Such a note therefore writes 15 wherever playback reaches it carrying another level, and keeps an empty cell where the channel stands at the full level already. The exporter follows each channel's level through the order the way Bitphase plays it: frame by frame, then from the loop point, the first frame, with the level the order ended on. A note-on written as a note cut takes the same level.

The column scales the instrument's level. Playback combines the two levels the way in-app playback does. On the pulse channels it reads a PT3 volume table, which rounds their product over the full level to the nearest step. On noise it rounds the product down, and plays the quietest level wherever that comes out silent while both levels sound. Either way a full instrument level plays at the column's own level.

The triangle sounds above half volume. In-app playback sounds the triangle while a row's volume is 8–15 and silences it at 0–7. Playback enables the triangle while the PT3 product of the pattern level and the instrument level is above zero, and a triangle slice writes a full instrument level, so any pattern level above zero sounds it. A triangle row at 0–7 therefore writes the silencing -1, and a row at 8–15 writes its level.

The volume column names silence. In Bitphase you type 0 to silence a channel and leave the cell blank to carry its level forward. The file stores those two as -1 and 0. The volume field is declared allowZeroValue, so Bitphase parses a typed 0 to -1 and prints a stored -1 back as 0, and a stored 0 shows as a blank cell. Its engine reads -1 as volume zero. A row asking for silence therefore writes -1, a row naming a level writes it verbatim, and a row with an empty volume cell writes 0. Each is the same cell you would see in the tracker.

▍F. Bitphase capacity limits

Quantity Bitphase limit Exporter behavior
Values per instrument macro 1–512 writes the opening values of a longer dimension, keeps a volume's closing silence, and reports what it left out
Rows per table unbounded writes the contour whole
Effect columns per channel 1–4 writes one: the speed effects on the DPCM channel, the ornament position on a transpose row
Instruments the instrument column holds 2 base-36 digits, so 1–1295 raises past 1295
Tables the table column holds 1 base-36 digit, so ids 0–34 raises past 35 tables, counting the moved tables transpose rows name
Ornament position the effect parameter is a byte, so steps 0–255 names a copy of the table opening on a later step
Note range the 96-entry tuning table, pitch 24–119 keeps the song's range, 24–119, raising a lower note only as far as its table's highest step reaching 24 (section E)
A4 tuning 220–880 Hz, the range the song settings offer (src/lib/chips/nes/schema.ts) writes the work's tuning, and raises past that range
Volume column -1 silences (the tracker shows 0), 0 carries the level forward (shown blank), 1–15 set the level writes the row's level, 15 on a note the channel reaches at another level, and -1 where a row asks for silence or the triangle's level is 0–7
Pattern length (rows) 1–256 clamps the preview pattern; a project keeps rows_per_pattern
Order positions unbounded matches
Speed 1–255 the groove's tick counts, bounded to that range
DPCM channel present rests, apart from the speed effects the groove sets

A row that names a voice on a channel the voice has no instrument for plays nothing in the song, so the exporter writes a note cut on it and reports the row by its frame, channel and row. The project export dialog lists those rows.

Tables and instruments are numbered together, and each slice takes one of each. The table column is therefore what a wide document reaches first, and the exporter raises an error instead of writing a document whose later voices cannot be named. The moved tables transpose rows name take the ids above the slices, and a document needing more of them than the column names is refused the same way.

The macro limit is the one a reconstruction meets by itself. A dimension reaches it at 512 frames, which is 8.5 s at 60 Hz. Each field is counted on its own, so a flat duty or a held level costs one value. In a .btp the contour the table carries keeps its whole length. A preset folds the contour into toneAdd, so there the contour stops at 512 values too. A volume dimension keeps its closing silence as its last value, because the note has to end. The FamiTracker export meets its own limit by the same rule.

The export reports what it left out. Every scope reports the instruments a macro shortened: an instrument by the values it kept of the values it had, a reconstruction and a project by how many instruments were shortened. A project counts each slice as one instrument.

▍G. Data without a counterpart

ProjectInfo.comment has no counterpart in a Bitphase document, which has a name and an author only, so the exporter leaves the comment out.

A field a reconstruction does not decide gets no macro. The hardware envelope, the length counter, the phase retrigger, the sweep and the tone accumulator each take the default in section B. Bitphase plays the DPCM samples an instrument assigns to its notes (dpcmSamples, dpcmAssignments). The exporter writes no sample and no note on the DPCM channel, so that channel rests.

interruptFrequency carries the reconstruction's own tick rate. Bitphase's settings panel offers 50 and 60 Hz beside a custom value, and its loader and timeline accept any rate. A rate outside that pair plays correctly.