Skip to content

Commit 65c1bea

Browse files
authored
Merge pull request #51 from JakimPL/size
Sample/Instrument Size
2 parents b80e37d + bbeb67f commit 65c1bea

79 files changed

Lines changed: 4023 additions & 402 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/development/guidelines.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ These rules govern the Python in this repository. They complement
3131
1. An `__init__` exposes only names from within its own tree hierarchy.
3232
1. Give each module a single area of responsibility.
3333
1. If a module contains many class and function definitions, split into a subpackage divided by a single concern.
34-
1. If a private function serves only a class in the module it lives, move it to the class as a static/class method or isolate helper functions into a separate utility module.
34+
1. If a private function (or public that does not have any external consumers) serves only a class in the module it lives, move it to the class as a static/class method or isolate helper functions into a separate utility module.
3535
1. Prefer subpackages over a flat directory structure.
3636
1. Isolate platform-, desktop-, or external-tool-specific behaviour behind a `Protocol` with one implementation per target, selected by a runtime factory that probes availability and environment. Callers depend only on the `Protocol` and stay platform-agnostic.
3737
1. Wrap a third-party library or OS tool whose behaviour differs across platforms behind our own typed interface, and encode each quirk inside the matching implementation. A comment naming the third-party behaviour is warranted there.
@@ -86,6 +86,7 @@ These rules govern the Python in this repository. They complement
8686
1. A test file mirrors the ownership of the code it exercises.
8787
1. When functionality moves between packages, move its direct unit tests in the same change.
8888
1. Parametrize tests that share a body, using a test-case dataclass.
89+
1. Test case classes and cases themselves should be defined inside the testing class, unless these objects are shared between test classes. A suite inherits from `BaseTestSuite` and names its case class `TestCase`, which inherits from `BaseRegularTestCase`, or from `BaseAutolabelTestCase` where the case derives its own label. The parametrized argument carries the case as `test_case`.
8990
1. For a multi-step scenario, use a test-scenario suite class — a series of functions with assertions.
9091
1. Prefer fixtures over factories, and define shared fixtures in an appropriate place.
9192
1. Do not assert default values of configurations, layouts, settings, and similar. Defaults are not contracts, and pinning them overconstrains the tests. Test behavior instead: validation bounds, serialization round-trips, and invariants. The exception is when values must match by contract rather than equal a chosen constant — e.g. project metadata at creation or after a save/load round-trip should be asserted to match, never hardcoded to a version string.

docs/development/playback.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,22 @@ sequencer distinguishes a history restore from a document transition. And the mu
162162
listening session, so opening, creating, or closing a document starts a fresh one with every channel
163163
audible.
164164

165+
## What the channel holds
166+
167+
A sample states every dimension of every frame, and its reconstruction names which of those
168+
dimensions the instrument itself wrote. The rest are the channel's: each channel carries a value per
169+
dimension — volume, arpeggio, timbre — and an instrument leaving one empty sounds it at the value the
170+
channel holds. That is what clearing an envelope in the instruments panel means once the sample is
171+
played in a song, and it is the same rule a FamiTracker instrument follows with a sequence left out.
172+
173+
The value moves as the song plays. Every frame an instrument writes hands its value to the channel,
174+
so the channel keeps the last one written and an instrument that leaves the dimension empty picks it
175+
up. A silent frame states its level alone, leaving pitch and timbre where the channel holds them.
176+
177+
A pass through the song begins on the values a channel holds from the start — full volume, no
178+
arpeggio offset, the first timbre — so starting the song and looping back to its first row both
179+
sound the same. Seeking within a running song keeps the values, since the channel has reached them.
180+
165181
## Rendering the song to a file
166182

167183
A render writes the whole song to an audio file through the kernel that plays it. `RowSynthesizer`
@@ -231,6 +247,8 @@ terminating would reclaim.
231247
| Where the playhead stands, and both grids' marks for it | `SequencerTabCoordinator` (`coordinators/tabs/sequencer.py`) |
232248
| Marking and revealing the sounding row in the tracker | `GUISequencerTrackerPanel` (`ui/panels/sequencer/tracker.py`) |
233249
| Row mixing, and the mask it pulls while rendering | `RowSynthesizer` (`logic/sequencer/playback/synthesizer/`) |
250+
| Filling in the dimensions a channel governs, frame by frame | `SampleVoice` (`logic/sequencer/playback/synthesizer/voice.py`) |
251+
| The values a channel holds between frames | `ChannelState` (`logic/sequencer/playback/synthesizer/state.py`) |
234252
| The channel generators and the rates they are built at | `ChannelBank` (`logic/sequencer/playback/synthesizer/bank.py`) |
235253
| How long each row of a pattern lasts | `Groove` (`sampletones_core/timing/`), indexed by row while rendering |
236254
| How many samples one of that row's ticks spans | `TickClock` (`sampletones_core/timing/`), followed by `EngineRates` |

docs/formats/bitphase.md

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ carries every register value the channel takes for that tick. From
6969
| Field | Range | Runtime meaning | What the exporter writes |
7070
| --- | --- | --- | --- |
7171
| `pulseWidth` | 0–3 | square duty cycle; on the noise channel, any nonzero value selects the short LFSR | the duty-cycle envelope item (squares), the short/long mode (noise), a flat value (triangle) |
72-
| `volumeOrRate` | 0–15 | the literal channel volume while `envelope` stays off | the volume envelope item |
72+
| `volumeOrRate` | 0–15 | the literal channel volume while `envelope` stays off | the volume envelope item, or a full level where the slice leaves its volume to the channel |
7373
| `envelope` | bool | reads `volumeOrRate` as a hardware decay rate | `false`, so each item is the volume itself |
7474
| `soundLength` | 0–511 | length counter in ticks; `0` holds the note | `0`, so the volume envelope alone shapes the note |
7575
| `toneAdd` | −4096–4095 | period offset added to the tuning-table period (squares and triangle) | `0` in a document, the pitch contour in a preset |
@@ -79,10 +79,18 @@ carries every register value the channel takes for that tick. From
7979

8080
**Looping.** Playback returns to the instrument's `loop` row once it runs off the end,
8181
which is the only mode there is. A looping slice therefore sets `loop = 0` so its
82-
envelopes repeat from the start while the note is held; a one-shot sets
83-
`loop = len - 1`, and since the volume envelope ends on a note-off item, the
84-
instrument rests in silence once it has played through. A sample's `loop` flag drives
85-
this, the same flag the FamiTracker exporter reads.
82+
envelopes repeat from the start while the note is held; a one-shot sets `loop = len - 1`
83+
and rests on the level that row carries — silence where the volume envelope ends on a
84+
note-off item, the channel's own level where the slice holds its volume. A sample's
85+
`loop` flag drives this, the same flag the FamiTracker exporter reads.
86+
87+
**A held volume.** A slice whose volume envelope carries no item leaves its level to the
88+
channel, so the exporter writes a full `volumeOrRate` for every frame the slice
89+
describes. Playback combines a row's level with the pattern's volume column through a
90+
PT3 volume table, where a full-level row comes out at the column's own level, so those
91+
rows sound at whatever level the channel carries — the same reading FamiTracker gives a
92+
disabled volume sequence. A slice describing no frame at all is what writes a single
93+
silent row, the smallest instrument Bitphase plays.
8694

8795
**Equal lengths.** Instrument rows and table rows advance on independent per-tick
8896
counters, so they share a length and a loop point and stay in step for as long as the

docs/formats/famitracker.md

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -146,19 +146,32 @@ sequence, so its envelopes repeat from the start while the note is held; a one-s
146146
instrument leaves every loop point at `-1`. A sample's `loop` flag drives this when
147147
the sample is exported into a module.
148148

149-
**Equal lengths.** FamiTracker advances each sequence on its own per-tick counter, so
150-
every populated sequence of an instrument carries the same item count and the
151-
dimensions stay in step. The volume envelope arrives one item longer than the others,
152-
carrying a trailing zero that releases the note. A looping instrument therefore keeps
153-
the shortest length, dropping that trailing item so the loop sustains; a one-shot
154-
keeps the longest, each shorter dimension holding its final value through the release
155-
tick. That shared length stays within the 252 items a FamiTracker sequence holds, so a
156-
reconstruction longer than 252 frames — 8.4 s at the default 30 fps — exports its opening
157-
252 frames and logs the shortening. The instruments panel colours a sequence input warning
158-
orange once it passes that length, so the limit is visible before an export.
149+
**Lengths.** FamiTracker advances each sequence on its own per-tick counter. A sequence
150+
that reaches its last item halts and leaves the value it wrote applied, which the driver
151+
holds for as long as the note sounds (`CSeqInstHandler::UpdateInstrument`). A one-shot
152+
instrument therefore carries every dimension at the length it was written: a two-item
153+
volume envelope beside a one-item duty envelope plays exactly as a padded pair would, and
154+
costs the padding less. A looping instrument brings its populated dimensions to the
155+
shortest length instead, so the envelopes repeat in step and the trailing zero that
156+
releases the note is dropped from the cycle.
157+
158+
Every length stays within the 252 items a FamiTracker sequence holds, so a reconstruction
159+
longer than 252 frames — 8.4 s at the default 30 fps — exports its opening 252 frames and
160+
logs the shortening. The instruments panel colours a sequence input warning orange once it
161+
passes that length, so the limit is visible before an export.
162+
163+
An empty dimension is written as a disabled sequence, which is a different instrument from
164+
one carrying a single zero: the disabled slot leaves that dimension to the channel, while a
165+
one-item sequence sets the value once and holds it. A dimension arrives empty when the
166+
reconstruction records it as one the channel governs — the state clearing the envelope in the
167+
instruments panel puts it in (see [Reconstructions](reconstructions.md)).
159168

160169
**How _SampleToNES_ fills an instrument.** Each generator slice of a sample's
161170
reconstruction becomes one instrument, so a sample yields one to four instruments.
171+
A reconstruction holds a stream for every channel, and one describing no frame is a
172+
channel standing by (see [Reconstructions](reconstructions.md#contents)): it takes no
173+
place in the instrument table, so the instruments an export writes are the channels
174+
that play.
162175
The arpeggio sequence carries the reconstruction's pitch contour as signed offsets,
163176
and triggering the instrument at `initial_pitch` replays that contour. Volume, duty
164177
(or noise mode) and any pitch sequences carry across directly. The DPCM
@@ -202,3 +215,50 @@ for order slots the song leaves unset; a channel that already fills indices up t
202215
127 leaves no room for it, which the exporter reports rather than emitting a corrupt
203216
order. When the domain model grows to enforce these limits, the editor can prevent
204217
reaching a state the exporter would reject.
218+
219+
## D. Driver memory footprint
220+
221+
Compiling a module into an NSF lays each instrument out across two regions of the driver's
222+
data, and an instrument's sequences size both of them. `footprint.py` measures the two, and
223+
`specification/memory.py` names every field the measurement counts. The instruments panel and
224+
the samples context menu display the result, so the cost of a sample is readable before an
225+
export.
226+
227+
The **instrument region** holds the instrument list — one pointer per instrument — followed by
228+
each instrument's body: a sequence-enable bitmask, then one pointer per populated sequence. The
229+
**sequence region** holds one chunk per sequence: a four-field header followed by the items.
230+
231+
| Field | Bytes | Region |
232+
| --- | --- | --- |
233+
| instrument list entry | 2 | instrument |
234+
| sequence-enable bitmask | 1 | instrument |
235+
| sequence pointer, per populated sequence | 2 | instrument |
236+
| item count · loop point · release point · setting | 1 each | sequence |
237+
| item, per tick | 1 | sequence |
238+
239+
An instrument with `n` populated sequences carrying `s₁ … sₙ` items therefore occupies
240+
`3 + 2n` bytes of the instrument region and `Σ (4 + sᵢ)` of the sequence region. A dimension the
241+
channel leaves unused is written as a disabled slot, and the populated sequences alone are
242+
charged: `n` is 3 on the pulse and noise channels (volume, arpeggio, duty) and 2 on triangle.
243+
Each sequence is charged at its own length (section B), so shortening any one dimension shows
244+
in the figure, and an instrument tops out at 777 bytes — three sequences at the 252-item limit.
245+
246+
These two figures are the ones FamiTracker itself prints while creating an NSF —
247+
`Instruments used: N (X bytes)` and `Sequences used: M (Y bytes)` — which is how a measurement
248+
is held against the tracker.
249+
250+
**Version.** The figures are vanilla FamiTracker 0.4.6, the target section A names. The 0CC and
251+
Dn-FamiTracker forks open each instrument body with a channel-type byte, so an instrument costs
252+
one byte more there.
253+
254+
**Pooling narrows a module's total.** The `SEQUENCES` block stores each distinct sequence once
255+
(section A.2), so a module holding two instruments with the same volume envelope pays for that
256+
chunk once. A per-instrument or per-sample figure states that instrument's own cost, and a
257+
module total is therefore at most the sum of them. Within one instrument each kind appears
258+
once, so its own sequences are charged once each.
259+
260+
**Looping levels the sequences.** A looping instrument brings its populated dimensions to the
261+
shortest length, while a one-shot keeps each dimension as written (section B), so the two forms
262+
of one set of envelopes cost differently. A sample carries the flag that decides which applies;
263+
a reconstruction standing on its own is measured as a one-shot, matching the instrument its
264+
**Export instrument** writes.

docs/formats/reconstructions.md

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,31 @@ A `.stn` file holds:
2626
* **approximation** — the rendered NES audio: the sum of every channel's output,
2727
the closest match to the original;
2828
* **per-channel approximations** — the audio each channel contributes on its own,
29-
one waveform per enabled channel (`pulse1`, `pulse2`, `triangle`, `noise`);
29+
one waveform per channel that sounds;
3030
* **per-channel instructions** — the instruction stream each channel plays, one
3131
[instruction](../glossary.md#instruction) per frame. This is the data a
32-
FamiTracker export is built from;
32+
FamiTracker export is built from. A reconstruction holds a stream for every one
33+
of the four channels (`pulse1`, `pulse2`, `triangle`, `noise`), and a stream of
34+
no frames is a channel standing by: it is written by no export and costs
35+
nothing, while staying open to edit, so writing an envelope into it puts the
36+
channel in play and clearing every envelope takes it out again;
3337
* **per-channel reference pitch** — the note each channel's arpeggio offsets are
3438
measured against, chosen once when the reconstruction is built and stored with
3539
the instructions it describes. An export reads the offsets against this pitch,
3640
so editing an arpeggio moves the frames around a base that stays put (see
37-
[FamiTracker export](famitracker.md)).
41+
[FamiTracker export](famitracker.md));
42+
* **per-channel held dimensions** — the envelopes each channel leaves to the
43+
player. An instruction states a value for every dimension of its frame, so this
44+
is what says which of them the instrument itself writes; the rest are the
45+
channel's, and the player keeps the value it already holds for them. A channel
46+
in play writes them all as it is built, and clearing an envelope in the
47+
instruments panel adds that dimension here.
48+
49+
A channel standing by rests at a reference pitch of its own, so the first envelope
50+
written into it sounds on a mid-range note, and it leaves every dimension it offers
51+
to the player, which is the record a channel edited down to empty envelopes reaches
52+
as well. A file naming a stream for the channels it plays alone reads as the whole
53+
four, with the rest coming back standing by.
3854

3955
## Detached reconstructions
4056

docs/guide/interface.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,14 @@ Sequencer** (see the [sequencer guide](sequencer.md)).
5151

5252
For finer control, the **Instruments** panel on the right shows each channel's
5353
instrument — its pitch, volume, arpeggio, and duty sequences — which you can edit
54-
by dragging the bars or typing values. **Export instrument...** writes the channel
55-
on show, for whichever tracker the save dialog's file type names — see
56-
[where your files live](files.md#exported-files).
54+
by dragging the bars or typing values. Clearing a sequence hands that dimension to
55+
the channel, so an instrument with no volume sequence plays at whatever level its
56+
channel carries. Beside each channel is the room its instrument takes on the NES,
57+
with the whole sample's above them, so you can see what an edit costs. The figures
58+
are in bytes, and they count what a FamiTracker export saves, so clearing a
59+
sequence brings them down. **Export instrument...** writes the channel on show, for
60+
whichever tracker the save dialog's file type names — see [where your files
61+
live](files.md#exported-files).
5762

5863
## Instructions
5964

docs/guide/sequencer.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,11 @@ frequency**; **Add anyway** adds it regardless.
2020
Manage the imported samples in the **Samples** list on the right: right-click one
2121
to **Edit**, **Rename**, **Duplicate**, **Remove**, or reorder it, and toggle its
2222
**Loop** flag. The **Edit** menu carries the same actions for the sample you have
23-
picked. Removing a sample that patterns still use asks **Remove sample** first,
24-
because it clears every row that references it.
23+
picked. The right-click menu also names how much room the sample takes on the NES —
24+
its total, then each channel it plays — measured as its **Loop** flag has it. The
25+
figures are in bytes, and they count what a FamiTracker export saves.
26+
Removing a sample that patterns still use asks **Remove sample** first, because it
27+
clears every row that references it.
2528

2629
## Writing a pattern
2730

0 commit comments

Comments
 (0)