Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
158 commits
Select commit Hold shift + click to select a range
486f9aa
Held: the session's remembered paths to the disk at startup
JakimPL Sep 14, 2026
f57752b
Taught: the sample players to play from a chosen position
JakimPL Sep 14, 2026
a6c4be6
Added: playing from a point clicked on a waveform
JakimPL Sep 14, 2026
6e6be5a
Settled: waveform clicks against double-clicks
JakimPL Sep 14, 2026
b5a07b9
Held: the application to one context menu popup
JakimPL Sep 14, 2026
d17b8ae
Held: the converter's moves to the list and its setup to the run
JakimPL Sep 14, 2026
1078d49
Held: every panel's keys to the card standing open
JakimPL Sep 14, 2026
b17bdd2
Held: the stems list's settle pass to the frames it is drawn in
JakimPL Sep 14, 2026
4905d8b
Held: hover rechecks to one waiting frame callback
JakimPL Sep 14, 2026
dbaafbf
Refreshed: the library catalog after a generation and on a new librar…
JakimPL Sep 14, 2026
79e2158
Held: the beta-divergence's precision at the spectrum floor
JakimPL Sep 14, 2026
c92fe79
Averaged: library features in the power domain
JakimPL Sep 14, 2026
15a1eae
Scored: noise the renderer cannot line up at its expected loss
JakimPL Sep 14, 2026
82b23b9
Recorded: the engine defects the bug fixes turned up
JakimPL Sep 14, 2026
8b78283
Owned: a worker pool by the thread that runs it, ended before the out…
JakimPL Sep 14, 2026
4c4c642
Held: a library generation to the catalog it started in, and a rebuil…
JakimPL Sep 14, 2026
51be1bb
Held: a conversion run to the request it was started from, and every …
JakimPL Sep 14, 2026
a6ce739
Simplified: context menus, key scopes, the stems list's settle clock …
JakimPL Sep 14, 2026
04fe93f
Resolved: remembered paths where they are used, an owned seek, and a …
JakimPL Sep 14, 2026
bf5eea8
Simplified: the divergence precision, the library mean, the upgrade s…
JakimPL Sep 14, 2026
17eea4f
Rendered: every recorded frame from its instruction, and retired the …
JakimPL Sep 15, 2026
b6e9b6a
Scaled: every approximation through one drive rule, named a fragment'…
JakimPL Sep 15, 2026
e8cb6fc
Judged: renders by loudness-weighted referees, and made calibration a…
JakimPL Sep 15, 2026
b8c6de4
Opened: the calibration group to every system and wrote the tool's ex…
JakimPL Sep 15, 2026
46e634f
Leveled: the input by its typical frame's RMS against one channel at …
JakimPL Sep 15, 2026
4afcb1c
Scored: every candidate by the frame's cost with it sounding, so a ch…
JakimPL Sep 15, 2026
db58c6d
Sped up: the test suite by vectorizing histogram densities, isolating…
JakimPL Sep 15, 2026
a98328e
Settled: every matching tuning in one shipped generation.yaml
JakimPL Sep 16, 2026
5223abc
Counted: a batch conversion in the reconstructions it writes, and a r…
JakimPL Sep 16, 2026
dfedaf4
Merge branch 'features' into calibration
JakimPL Sep 16, 2026
8ee0e22
Removed: the loudness decibels spectral distance
JakimPL Sep 16, 2026
a233444
Wired up: the channels a calibration run reconstructs with
JakimPL Sep 16, 2026
26ce253
Shipped: the page a calibration run is listened to on
JakimPL Sep 16, 2026
15844d4
Moved: the channel count onto each recording's settings
JakimPL Sep 16, 2026
49c0d5c
Moved: the drive onto each recording's channels
JakimPL Sep 16, 2026
4ce77df
Rewired: the converter onto per-recording drives and counts
JakimPL Sep 16, 2026
cfc8558
Rebuilt: the source settings card around each channel's drive and count
JakimPL Sep 16, 2026
c7830cf
Documented: the drive and channel count
JakimPL Sep 17, 2026
f3a3c90
Documented: the rules an edit carries stem ownership by
JakimPL Sep 17, 2026
fe0ff5b
Rebuilt: the reconstruction around stored instructions and rendered a…
JakimPL Sep 17, 2026
18db46b
Fixed: an instrument edit keeps the stems it was built from
JakimPL Sep 17, 2026
b403fa6
Moved: the recorded source onto the stems entry it names
JakimPL Sep 17, 2026
5407ec7
Added: editing one recording's part of a channel at a time
JakimPL Sep 17, 2026
0782f0c
Documented: what the rebuilt reconstruction changes for a reader
JakimPL Sep 17, 2026
aa41008
Fixed: an edit shortens a channel only as far as its scope reaches
JakimPL Sep 17, 2026
fefbc75
Shortened: the changelog entry for the smaller reconstruction files
JakimPL Sep 17, 2026
159a5a5
Added: reading the part of a channel a reader is listening to
JakimPL Sep 17, 2026
1de796f
Added: the instruments panel showing the recordings a reader is liste…
JakimPL Sep 17, 2026
4cbbfdf
Unified: how long something lasts and how far into it a moment is
JakimPL Sep 17, 2026
3bf4e7e
Added: the waveform naming its positions in minutes and seconds
JakimPL Sep 17, 2026
937fa82
Added: the ribbon naming the recording behind each stretch
JakimPL Sep 17, 2026
1cf1d2b
Fixed: the ribbon following the channels a reader has on
JakimPL Sep 17, 2026
005564e
Fixed: the waveform taking the height it is given
JakimPL Sep 17, 2026
5d680ba
Fixed: the time axis handing back its marks when no recording names them
JakimPL Sep 17, 2026
72a9c47
Fixed: the lanes standing down while a voice is drawn
JakimPL Sep 17, 2026
061c91e
Stated: what a channel that only rests reads as
JakimPL Sep 17, 2026
e070bfa
Authored: frames follow their box
JakimPL Sep 17, 2026
5d106b3
Changed: the recordings taking colors of their own
JakimPL Sep 17, 2026
cb0b06d
Added: the ribbon marking each lane with its channel's letter
JakimPL Sep 17, 2026
c222fff
Changed: a recording opening in the file browser on a double-click
JakimPL Sep 17, 2026
1df4af7
Fixed: the remove button standing as tall as its row
JakimPL Sep 17, 2026
f7921e8
Added: the color a recording is painted in standing beside its name
JakimPL Sep 17, 2026
59ec781
Changed: the lanes standing tall enough for the letter that marks them
JakimPL Sep 17, 2026
654e602
Added: the files the last release wrote, kept to read them back
JakimPL Sep 17, 2026
957e8a7
Added: holding each format's stored file to the shape this build reads
JakimPL Sep 17, 2026
11d3e68
Rewrote: the step carrying a reconstruction from the last release
JakimPL Sep 17, 2026
660c5f0
Changed: a channel switched off keeping the lane it stands on
JakimPL Sep 17, 2026
a5b2dfd
Stated: what a file written by the last release is read with
JakimPL Sep 17, 2026
6fa2157
Added: telling a library this build reads from one it rebuilds
JakimPL Sep 19, 2026
35b2a59
Changed: a headless conversion rebuilding the library it needs
JakimPL Sep 19, 2026
3a97516
Changed: a conversion rebuilding a library another version built
JakimPL Sep 19, 2026
ec88d32
Changed: opening a library another version built asking to rebuild it
JakimPL Sep 19, 2026
fb9aae7
Removed: the step repairing a library the last release wrote
JakimPL Sep 19, 2026
cd45182
Changed: an outdated library row reading
JakimPL Sep 19, 2026
9f8377a
Removed: version differences from the format documents
JakimPL Sep 19, 2026
cbb53a2
Updated: documentation
JakimPL Sep 19, 2026
3264030
Added: the divider a bend moves a note to and the pitch nearest any d…
JakimPL Sep 19, 2026
037db08
Added: the bend reaching an exported NSF
JakimPL Sep 19, 2026
9c16fae
Added: the plane layouts a bend may be written in to the codec study
JakimPL Sep 19, 2026
d694c4b
Fixed: the Copy button's label restored on the render thread
JakimPL Sep 19, 2026
bf45333
Changed: a plane that never moves leaving the song block
JakimPL Sep 19, 2026
4d46321
Changed: a bent tick counted from the note its frame names
JakimPL Sep 19, 2026
3dfbd9e
Changed: pastes to wait for the clipboard's answer
JakimPL Sep 19, 2026
6962cf0
Added: a bend plane read only on the ticks its channel flags
JakimPL Sep 19, 2026
b5098cc
Documented: the bend an NSF sounds
JakimPL Sep 19, 2026
2d2b1d9
Refactored: clipboard management
JakimPL Sep 19, 2026
1adbd28
Minor improvements
JakimPL Sep 19, 2026
4426144
Merge pull request #68 from JakimPL/clipboard
JakimPL Sep 19, 2026
f8aa9b2
Merge branch 'bend' into compatibility
JakimPL Sep 19, 2026
0120d1b
Added: the rules for writing the documentation
JakimPL Sep 20, 2026
9e6e0ba
Removed: the reconstruction manager's unread hash and coefficient
JakimPL Sep 20, 2026
c350790
Stated: the level an instrument preview sounds at
JakimPL Sep 20, 2026
df02360
Moved: the ETA mark to the layer that shows it
JakimPL Sep 20, 2026
762636e
Fixed: a redrawn bar writing to the field beneath it
JakimPL Sep 20, 2026
0984a95
Re-weighed: the stages a conversion reports
JakimPL Sep 20, 2026
7b7e10c
Held: a channel's frames to one reading of what is in play
JakimPL Sep 20, 2026
869ed63
Named: the recording behind each frame under an instrument's bars
JakimPL Sep 20, 2026
f5c483a
Removed: the drive a frame was rendered at
JakimPL Sep 20, 2026
4c95d93
Inverted: the drive a candidate is measured at
JakimPL Sep 20, 2026
1199c50
Stated: the drive a conversion reaches at
JakimPL Sep 20, 2026
823206f
Restated: the shares a conversion's stages are weighed at
JakimPL Sep 20, 2026
147afa4
Stated: the two shapes a category's message module takes
JakimPL Sep 20, 2026
49578b0
Settled: when a channel reads as standing by
JakimPL Sep 20, 2026
007c6e9
Painted: each ownership stretch by the reading it stands for
JakimPL Sep 20, 2026
d27dc68
Carried: the envelopes and their owners in one model
JakimPL Sep 20, 2026
4802d18
Folded: every dialog onto one geometry it opens from
JakimPL Sep 20, 2026
7e47c20
Wrote: the contract a dialog's geometry holds to
JakimPL Sep 20, 2026
0864cef
Refined: the repository-wide development documents
JakimPL Sep 20, 2026
a661d7a
Changed: measurements stated as comparisons rather than bare figures
JakimPL Sep 20, 2026
65589c3
Refined: the application development documents
JakimPL Sep 20, 2026
0a6c527
Compressed: the payload a binary document is written as
JakimPL Sep 20, 2026
609d139
Merge pull request #69 from JakimPL/deflation
JakimPL Sep 20, 2026
36a5f0d
Refined: the release documents
JakimPL Sep 20, 2026
e995840
Added: the packed-plane layout the compression study prices
JakimPL Sep 20, 2026
2968eb6
Added: the repeated-value grammars the study compares
JakimPL Sep 20, 2026
43c731d
Stated: what a named risk and a documented claim owe
JakimPL Sep 20, 2026
39ed407
Held: which stem wins a channel clear of the drive it plays at
JakimPL Sep 21, 2026
b6a5ac3
Refined: the user guide and the glossary
JakimPL Sep 21, 2026
019347a
Guarded: the band beneath the bars against a press meant for them
JakimPL Sep 21, 2026
bf0098a
Held: a dimension's band to the frames that dimension draws
JakimPL Sep 21, 2026
5421b0d
Moved: the application half of the stems document
JakimPL Sep 21, 2026
1b427f6
Refined: the concept documents
JakimPL Sep 21, 2026
98de827
Refined: the format references
JakimPL Sep 21, 2026
dfc24a0
Measured: the frames a dialog takes to settle
JakimPL Sep 21, 2026
ef25396
Folded: the fifth dialog onto the base its siblings use
JakimPL Sep 21, 2026
97e7978
Stated: a song's planes once, in the table every reader derives from
JakimPL Sep 21, 2026
875204f
Refined: README
JakimPL Sep 21, 2026
69fc703
Held: a dialog to the width it states
JakimPL Sep 21, 2026
ea30ec5
Added: a repeat count in the bits a plane's register ignores
JakimPL Sep 21, 2026
910aea2
Added: a test holding the documentation's links and index
JakimPL Sep 21, 2026
ce73850
Restated: what the dialogs, the lanes and the drive actually do
JakimPL Sep 21, 2026
d1aaf71
Folded: the triangle's silence into the pitch it sounds
JakimPL Sep 21, 2026
5a01ded
Cleared: the bug ledger
JakimPL Sep 21, 2026
3aca4d6
Read: a phrase's own count from the token that leaves it unstated
JakimPL Sep 21, 2026
4c1bbfe
Removed: the trailing rest entry the reading's own case answers
JakimPL Sep 21, 2026
5d9bae5
Wrote: what a plane's byte holds and what the driver reads from it
JakimPL Sep 21, 2026
138d0f4
Centered: the error dialog again on the height its traceback reaches
JakimPL Sep 21, 2026
7cd6854
Left: a resting stretch to the ground of whatever draws it
JakimPL Sep 21, 2026
2a05d3d
Merge branch 'revision' into documentation
JakimPL Sep 21, 2026
53a8a96
Closed: the two bugs this branch's fixes answer
JakimPL Sep 21, 2026
7502775
Refined: the glossary, the index and the documentation rules
JakimPL Sep 21, 2026
60e5b15
Refined: the user guide's prose and glossary links
JakimPL Sep 21, 2026
7635efd
Refined: the reference pages and the API page
JakimPL Sep 21, 2026
d8e40ee
Refined: calibration and the small concept pages, and corrected the h…
JakimPL Sep 21, 2026
7041e86
Refined: the concept documents and their glossary terms
JakimPL Sep 21, 2026
d9a7df0
Refined: the development documents at the top level
JakimPL Sep 21, 2026
5f52454
Refined: the application development documents
JakimPL Sep 21, 2026
c486b8c
Refined: the release documents
JakimPL Sep 21, 2026
ae7181b
Refined: the documentation's remaining habit phrases and cross-refere…
JakimPL Sep 21, 2026
2c6d2fb
Merge branch 'revision' into compression
JakimPL Sep 21, 2026
5db72c7
Read: a plane's absence against the value it is seeded to
JakimPL Sep 21, 2026
491513a
Stated: the conditions the duty-cycle split was measured under
JakimPL Sep 21, 2026
1e4055b
Reformat
JakimPL Sep 21, 2026
0742295
Read: every document in the encoding it is stored in
JakimPL Sep 21, 2026
886e037
Merge pull request #70 from JakimPL/compression
JakimPL Sep 21, 2026
9d44132
Merge pull request #71 from JakimPL/revision
JakimPL Sep 21, 2026
76565fa
Merge pull request #67 from JakimPL/compatibility
JakimPL Sep 21, 2026
59a3b6e
Merge pull request #72 from JakimPL/stems
JakimPL Sep 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,6 @@ sampletones
*.exe
*.dll
*.so

!tests/data/compatibility/**/*.ins
!tests/data/compatibility/**/*.stn
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

* Added NSF player and export.
* Added stems conversion: to mix several recordings into one reconstruction.
* Changed drive to reach for louder instructions while a recording converts; reconvert anything converted at a drive other than `1.00`.
* Improved clarity of reconstructions.
* Optimized the size of reconstructions.
* Bumped the reconstruction data-version to `2.2` with backward compatibility for `2.1`.
* Bumped the library data-version to `2.1`.

## v0.3.1 [2026-08-18]

Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help setup install system-deps build release run clean pre-commit test test-docs benchmarks lint format
.PHONY: help setup install system-deps build release run calibration clean pre-commit test test-docs benchmarks lint format

ifeq ($(OS),Windows_NT)
PYTHON := python
Expand All @@ -24,6 +24,7 @@ help:
@echo $(Q) make test - Run the test suite with coverage$(Q)
@echo $(Q) make test-docs - Run the doctests$(Q)
@echo $(Q) make benchmarks - Run the measured-duration suite$(Q)
@echo $(Q) make calibration - Measure reconstruction on the reference sounds; writes the renders and a report$(Q)
@echo $(Q) make clean - Remove build artifacts and cache files$(Q)
@echo $(Q) make lint - Run mypy and pylint (ARGS=--mypy or ARGS=--pylint for one of them)$(Q)
@echo $(Q) make format - Auto-format code (isort, black)$(Q)
Expand All @@ -48,6 +49,9 @@ system-deps:
run:
uv run sampletones

calibration:
uv run sampletones calibration

clean:
$(PYTHON) scripts/clean.py

Expand Down
57 changes: 24 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<div align="center">
<img src="https://raw.githubusercontent.com/JakimPL/SampleToNES/main/src/sampletones_assets/icons/sampletones.svg" alt="SampleToNES" width="64">
<p><i>SampleToNES</i> v0.3.2</p>
<p><i>SampleToNES</i></p>
</div>

## Overview
Expand All @@ -21,26 +21,31 @@ The core idea is to approximate an audio sample using only the chip's basic osci

A built-in sequencer lets you arrange the reconstructed samples into patterns and play them back inside the application, so you can experiment with the results before exporting the instruments into FamiTracker.

It supports:
With it you can:

* loading common audio formats: WAV, MP3, FLAC, OGG, AIFF, and AU
* a wide range of NES frequencies, from 15 Hz to 300 Hz, including the two most common standards:
* NTSC (60 Hz)
* PAL (50 Hz)
* various sample rates, from 8000 Hz to 192,000 Hz
* restricting the reconstruction to a chosen subset of oscillators:
* `pulse1`
* `pulse2`
* `triangle`
* `noise`
* exporting reconstructed audio as FamiTracker `.fti` instruments, Bitphase `.json` instrument presets, `.nsf` programs the NES itself plays, or `.wav`
* convert your own recordings — WAV, MP3, FLAC, OGG, AIFF or AU — into NES instruments
* choose which of the four channels each recording may use, and how loud it plays on them
* arrange the results into a song and play it back in the app
* export instruments and songs for:
* [_FamiTracker_](http://famitracker.com/)
* [_Bitphase_](https://bitphase.app/)
* `.nsf` program
* audio `.wav` file

## Installation

You can install _SampleToNES_ in three ways:

- **Download a release** for Windows or Linux from the [releases page](https://github.com/JakimPL/SampleToNES/releases), extract it, and start `sampletones`.
- **Install from PyPI** on Windows, macOS or Linux. You need Python 3.12 or newer:
- **Install from PyPI** on Windows, macOS or Linux. You need Python 3.12 or newer. On Linux and
macOS, install the audio and file dialog libraries first:

```sh
sudo apt-get install libportaudio2 libasound2 python3-tk # Debian and Ubuntu
brew install portaudio # macOS
```

Then install the app and start it:

```sh
uv tool install sampletones # or: pipx install sampletones
Expand All @@ -53,26 +58,12 @@ An NVIDIA graphics card can speed up conversion. The [installation guide](https:

## Usage

### Where your files are stored

Your configuration, instruction libraries (`.ins`), and reconstructions (`.stn`) live under your documents folder, in `SampleToNES/`:

- Windows: `C:\Users\<user>\Documents\SampleToNES`
- Linux: `/home/<user>/Documents/SampleToNES`
- macOS: `/Users/<user>/Documents/SampleToNES`

### Command line

Every operation is a named command, and `sampletones` alone starts the interface:

```sh
sampletones run --config <config-path> # start with a custom config
sampletones open <project-path> # start with a project, reconstruction or library loaded
sampletones convert <audio-path> --config <config-path> -o <out> # reconstruct a recording without the GUI
sampletones library --config <config-path> # generate an instruction library
```
`sampletones` starts the app. Your work is saved in a `SampleToNES` folder inside your documents
folder.

Run `sampletones --help` for the commands and `sampletones <command> --help` for a command's options. The [command-line guide](https://github.com/JakimPL/SampleToNES/blob/main/docs/guide/command-line.md) explains them.
Every operation is also a named command — `sampletones convert <audio-path>` reconstructs a
recording without the interface, for example. Run `sampletones --help` for the list, and see the
[command-line guide](https://github.com/JakimPL/SampleToNES/blob/main/docs/guide/command-line.md).

## Documentation

Expand Down
50 changes: 10 additions & 40 deletions docs/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,10 @@ This page is for using _SampleToNES_ as a library in your own Python code. Use i
Names come from two packages:

- The names in the table below come from `sampletones`: `from sampletones import ...`.
- The examples also use a few helpers from `sampletones_core`: `write_wave`, `generate_library`, `DEFAULT_CHANNELS`, and the FamiTracker instrument writers. Import them with the full path each example shows.
- The examples also use a few helpers from `sampletones_core` that are not in the table: `write_wave`, `ensure_library`, `DEFAULT_CHANNELS`, and the FamiTracker instrument writers. Import them with the full path each example shows.

## Public surface

```python
from sampletones import (
Config,
Window,
InstructionLibrary,
Reconstruction,
Reconstructor,
ChannelName,
Generator,
PulseGenerator,
TriangleGenerator,
NoiseGenerator,
Instruction,
PulseInstruction,
TriangleInstruction,
NoiseInstruction,
)
```

| Name | Purpose |
| --- | --- |
| `Config` | generation configuration; build it with `Config.load(path)` or `Config.default()` |
Expand All @@ -51,42 +32,36 @@ The package version is available as `sampletones.__version__`.
from sampletones import Config, PulseGenerator, PulseInstruction
from sampletones_core.audio.io import write_wave

# Load configuration
config = Config.load("config.json")

# Prepare generator and instruction
generator = PulseGenerator(config)
instruction = PulseInstruction(on=True, pitch=55, volume=7, duty_cycle=2)

# Generate waveform
audio = generator(instruction)

# Save audio file
sample_rate = config.sample_rate
write_wave("pulse.wav", sample_rate, audio)
write_wave("pulse.wav", config.sample_rate, audio)
```

The output is a single `G2` square wave one frame long.

Each generator keeps an oscillator phase and clock. By default a call renders a standalone waveform and leaves that state where it was; pass `save=True` to advance it into the next call, so a sequence of instructions renders as one continuous signal:
Each generator keeps an oscillator phase and clock. A call renders a standalone waveform and leaves that state where it was. Pass `save=True` to advance the state into the next call, so a sequence of instructions renders as one continuous signal:

```python
audio = generator(instruction, save=True) # advances the generator state
```

### Generate an instruction library

A reconstruction searches an [instruction library](../formats/instruction-libraries.md) built for its configuration, so the library must exist first. Generate it once for a given config:
A reconstruction searches an [instruction library](../formats/instruction-libraries.md) built for its configuration by this version of _SampleToNES_, so prepare the library first:

```python
from sampletones import Config
from sampletones_core.headless.library import generate_library
from sampletones_core.headless.library import ensure_library

config = Config.load("config.json")
generate_library(config) # renders every instruction and writes the .ins library
ensure_library(config) # builds the .ins library when it is missing or another version built it
```

The same step is reached from the application's _Instructions_ tab, or on the command line with `sampletones library --config config.json`.
The application's _Instructions_ tab and `sampletones library --config config.json` do the same.

### Reconstruct a sample

Expand All @@ -97,19 +72,14 @@ from sampletones import Config, Reconstructor
from sampletones_core.audio.io import write_wave
from sampletones_core.constants.enums import DEFAULT_CHANNELS

# Load configuration
config = Config.load("config.json")

# Prepare the reconstructor for the channels the run may use
# The channels the run may use
reconstructor = Reconstructor(config, frozenset(DEFAULT_CHANNELS))

# Reconstruct an audio file and save the reconstruction
reconstruction = reconstructor("sample.wav")
reconstruction.save("reconstruction.stn")

# Save the reconstruction waveform
sample_rate = config.sample_rate
write_wave("reconstruction.wav", sample_rate, reconstruction.approximation)
write_wave("reconstruction.wav", config.sample_rate, reconstruction.approximation)
```

### Load a reconstruction
Expand Down Expand Up @@ -139,4 +109,4 @@ for channel, features in reconstruction.export().items():
write_fti(f"{channel.value}.fti", instrument)
```

This writes one `.fti` per channel, named after the channel. A complete FamiTracker `.ftm` module is assembled from a project in the application, not from a single reconstruction — see [FamiTracker formats](../formats/famitracker.md).
This writes one `.fti` per channel, named after the channel. A `.ftm` module comes from a project in the application. See [FamiTracker formats](../formats/famitracker.md).
66 changes: 0 additions & 66 deletions docs/concepts/calibration.md

This file was deleted.

Loading
Loading