Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
cc2098a
Updated: README statement
JakimPL Oct 9, 2026
69e5a64
Built: GPU support into the local bundle by default, with an opt-out
JakimPL Oct 9, 2026
e7b2692
Added: a GPU self-check that a GPU bundle is held to
JakimPL Oct 9, 2026
5389989
Added: GPU support to the local standalone build
JakimPL Oct 9, 2026
d9e98f2
Updated: CHANGELOG
JakimPL Oct 10, 2026
ecc5690
Fixed: stem mute state carried onto a newly opened reconstruction
JakimPL Oct 10, 2026
5ff0096
Extended: the bend tooltip
JakimPL Oct 10, 2026
9f043c1
Redrawn: the tracker caret under its character and with the row's scroll
JakimPL Oct 10, 2026
0e83937
Added: a meter for what an edit's landing costs, headless and on screen
JakimPL Oct 10, 2026
c984868
Retuned: the tracker's palettes and fades for readable text under eve…
JakimPL Oct 10, 2026
1605527
Extended: the on-screen meter with a held burst and a single landing
JakimPL Oct 10, 2026
0b2f5a0
Redrawn: the guide pictures the tracker and the disabled shade changed
JakimPL Oct 10, 2026
e84d4c8
Rendered: the edited channel and the mix on the regeneration worker
JakimPL Oct 10, 2026
0b8cfb0
Fixed: the single-landing reading keeps the frames after the landing
JakimPL Oct 10, 2026
02c95c4
Changed: the long-document readings wait on the waveform's fade
JakimPL Oct 10, 2026
d136f4c
Updated: the typing scenario for the entry's typing color
JakimPL Oct 10, 2026
74aa309
Merge pull request #91 from JakimPL/redesign
JakimPL Oct 10, 2026
29e735a
Read: whether a channel stands by from its volume envelope
JakimPL Oct 10, 2026
3795860
Read: the waveform once per gesture, and the kept renders where nothi…
JakimPL Oct 10, 2026
216e2a6
Rendered: what a reader hears on the regeneration worker
JakimPL Oct 10, 2026
a313934
Painted: ownership one series per recording, repainted where it changed
JakimPL Oct 11, 2026
7f1d65a
Exported: the envelopes on the regeneration worker, installed at the …
JakimPL Oct 11, 2026
3544145
Kept: each waveform's reduction to the display's points by the array'…
JakimPL Oct 11, 2026
42eef96
Measured: a landing against the document's cold render, and a tick ag…
JakimPL Oct 11, 2026
f2c062b
Memoized: each stream's, each owner record's and each pattern's diges…
JakimPL Oct 11, 2026
167383b
Named: what the waveform's fade waits for
JakimPL Oct 11, 2026
3238d66
Owed: a WAV export to the worker where the cache keeps no sound for t…
JakimPL Oct 11, 2026
eded83c
Merge pull request #92 from JakimPL/showing
JakimPL Oct 11, 2026
50df3ae
Carried: the reader's choice onto the rebuilt record on the worker
JakimPL Oct 11, 2026
53b329c
Rendered: the sequencer's voice preview on a regeneration worker
JakimPL Oct 11, 2026
cf95ef2
Merge pull request #93 from JakimPL/hearing
JakimPL Oct 11, 2026
a0c6a60
Removed: the render cache's own rendering, every sound now a worker's…
JakimPL Oct 11, 2026
fcfb855
Answered: silence of the document's own length, a landing kept whole,…
JakimPL Oct 11, 2026
9c65616
Owed: one showing merged across gestures, and the envelopes to the wo…
JakimPL Oct 11, 2026
9dd50a6
Forgotten: a wanted preview when playback starts, and the trees' auto…
JakimPL Oct 11, 2026
ede8bd2
Mixed: the original of the recordings heard on the worker, kept besid…
JakimPL Oct 11, 2026
e1bc351
Witnessed: the fade and the edit on screen, the landing's cost in the…
JakimPL Oct 11, 2026
168141b
Simplified: one memo in the history, required inputs, and the docs to…
JakimPL Oct 11, 2026
59b035d
Answered: the fresh review's findings, a change waiting behind the ti…
JakimPL Oct 11, 2026
8aa0752
Updated: bugs and todos
JakimPL Oct 12, 2026
f87a0ab
Merge pull request #94 from JakimPL/closing
JakimPL Oct 12, 2026
79c0387
Fixed: the waveform update contract, its waveform taken by position
JakimPL Oct 12, 2026
245aa2d
Answered: the instruments panel's words while a document's envelopes …
JakimPL Oct 12, 2026
a310006
Held: every press of a burst to the double-click time, a late one a s…
JakimPL Oct 12, 2026
badad6c
Kept: the instruments panel's last envelopes
JakimPL Oct 12, 2026
e5dc4e0
Added: a web layout read from the application's themes, with the cali…
JakimPL Oct 12, 2026
f5e5c57
Added: the documentation maker in assets, writing the user docs as pa…
JakimPL Oct 12, 2026
2e893dc
Added: a wheel build hook writing the documentation into every instal…
JakimPL Oct 12, 2026
f57ae7c
Added: Help > Documentation, opening the shipped pages in the reader'…
JakimPL Oct 12, 2026
1af5c0c
Added: a screen scenario opening the documentation from the Help menu
JakimPL Oct 12, 2026
0e22071
Held: every wheel and bundle to its documentation, and every program …
JakimPL Oct 12, 2026
f58d12a
Spaced: the pages' cards and prose wider, in multiples of the applica…
JakimPL Oct 12, 2026
130048e
Kept: one documentation copy at a fixed address, replaced whole when …
JakimPL Oct 12, 2026
6c2dcac
Fixed: the pitch refinement crashing on a stretch of digital silence
JakimPL Oct 12, 2026
fdd60ec
Fixed: the file manager stand-in's cases running its shell scripts on…
JakimPL Oct 12, 2026
85b8aca
Noted: skipping a recording that fails in a run of one per recording
JakimPL Oct 12, 2026
a4cde65
Fixed: the screen stalling after a library generation
JakimPL Oct 12, 2026
c620e2a
Skipped: a recording that fails in a batch, which goes on and names i…
JakimPL Oct 12, 2026
604fcbb
Named: the recordings a batch skipped in the dialog at its end
JakimPL Oct 12, 2026
863615d
Kept: the converter's skipping cases writing under their own directory
JakimPL Oct 12, 2026
551e07a
Settled: held worker reports round after round until the workers have…
JakimPL Oct 13, 2026
ea9fd6e
Chose: NumPy when CuPy finds no graphics card to compute on
JakimPL Oct 13, 2026
c11657b
Held: the self-check's CPU case to the reason the backend fell back
JakimPL Oct 13, 2026
57839c1
Resolved: a GPU bundle's CUDA libraries in its wheels ahead of a syst…
JakimPL Oct 13, 2026
7903386
Merge pull request #101 from JakimPL/bundle
JakimPL Oct 13, 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
  •  
  •  
  •  
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ jobs:
- name: Check the committed icons match the mark
run: uv run pre-commit run icons --all-files --hook-stage pre-push --color always

- name: Check a built wheel carries its documentation
run: |
uv build --wheel --out-dir dist
uv run --isolated --no-project --with dist/sampletones-*.whl sampletones self-check --documentation

tests:
name: Tests (${{ matrix.os }}, py${{ matrix.python }})
runs-on: ${{ matrix.os }}
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ jobs:
sampletones --version
sampletones --help

- name: Check the wheel carries every resource it needs at startup
- name: Check the wheel carries every resource it needs at startup and its documentation
shell: bash
run: sampletones self-check
run: sampletones self-check --documentation

- name: Check a developer command refuses to run outside a checkout
shell: bash
Expand Down Expand Up @@ -124,7 +124,7 @@ jobs:

- name: Build the bundle
shell: bash
run: python scripts/bundle.py --release
run: python scripts/bundle.py --release --gpu 0

- name: Check the bundle runs and carries its notices
shell: bash
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ wheels/
/bin/
/build/
/bundles/
/src/sampletones_assets/documentation/

sampletones
!src/sampletones
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Optimized reconstruction size.
* Optimized algorithm memory usage.
* Improved FamiTracker/Bitphase compatibility.
* Supported CUDA for local installation if available.
* Bumped the reconstruction data- version to `2.2`, with backward compatibility for `2.1`.
* Bumped the project data version to `1.1`, with backward compatibility.
* Bumped the library data version to `2.1`.
Expand Down
14 changes: 9 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help setup install system-deps build release run calibration tracker-playback icons demo pictures clean pre-commit test test-docs benchmarks screens lint format
.PHONY: help setup install system-deps build release run calibration tracker-playback icons docs demo pictures clean pre-commit test test-docs benchmarks screens lint format

ifeq ($(OS),Windows_NT)
PYTHON := python
Expand All @@ -21,15 +21,16 @@ help:
@echo $(Q) make setup - Set up development environment (uv); GPU auto-detected, GPU=0 forces CPU$(Q)
@echo $(Q) make pre-commit - Install pre-commit hooks$(Q)
@echo $(Q) make system-deps - Install system packages required to build and run (apt on Debian-based Linux, Homebrew on macOS)$(Q)
@echo $(Q) make build - Compile standalone executable (development deployment config: DEBUG, strict history)$(Q)
@echo $(Q) make release - Compile standalone executable with the release deployment config (INFO, self-healing history)$(Q)
@echo $(Q) make build - Compile standalone executable (development deployment config: DEBUG, strict history); GPU auto-detected, GPU=0 forces CPU$(Q)
@echo $(Q) make release - Compile standalone executable with the release deployment config (INFO, self-healing history); CPU only$(Q)
@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 screens - Run the screen scenarios: the application driven on a virtual display (needs Xvfb)$(Q)
@echo $(Q) make calibration - Measure reconstruction on the reference sounds; writes the renders and a report$(Q)
@echo $(Q) make tracker-playback BITPHASE=folder FAMITRACKER=FamiTracker.exe [PROJECT=song.stp] - Play exported projects (the corpus by default) in Bitphase, FamiTracker or both; reports every tick that differs from the app$(Q)
@echo $(Q) make icons - Write the application icon suite from the mark into the package$(Q)
@echo $(Q) make docs - Write the user documentation as HTML pages into the package$(Q)
@echo $(Q) make demo - Make the demo tree (recordings, a library, reconstructions, a project) in DEMO=build/demo$(Q)
@echo $(Q) make pictures - Draw the guide's and the README's pictures of the application from the demo tree (needs Xvfb)$(Q)
@echo $(Q) make clean - Remove build artifacts and cache files$(Q)
Expand All @@ -45,10 +46,10 @@ install:
$(MAKE) build

build:
$(PYTHON) scripts/bundle.py
$(PYTHON) scripts/bundle.py --gpu $(GPU)

release:
$(PYTHON) scripts/bundle.py --release
$(PYTHON) scripts/bundle.py --release --gpu 0

system-deps:
$(PYTHON) scripts/system_dependencies.py
Expand All @@ -67,6 +68,9 @@ tracker-playback:
icons:
uv run python -m assets.icons

docs:
uv run python -m assets.documentation

demo:
uv run python -m assets.demo -o $(DEMO)

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

## Overview

_SampleToNES_ (`sampletones`) is a desktop tool for people writing music for the NES 2A03 sound chip, mainly in [_FamiTracker_](http://famitracker.com/).
_SampleToNES_ (`sampletones`) is a desktop tool for people writing music for the NES 2A03 sound chip, mainly in [_FamiTracker_](http://famitracker.com/) or [_Bitphase_](https://bitphase.app/).

<div align="center">
<img src="https://raw.githubusercontent.com/JakimPL/SampleToNES/main/docs/images/sampletones.webp" alt="SampleToNES" width="760">
Expand Down Expand Up @@ -67,7 +67,7 @@ recording without the interface, for example. Run `sampletones --help` for the l

## Documentation

The [user guide](https://github.com/JakimPL/SampleToNES/tree/main/docs/guide) explains how to use the app, step by step. The rest of [`docs/`](https://github.com/JakimPL/SampleToNES/tree/main/docs) covers the reconstruction algorithms, the file formats, the Python API and the developer notes.
The [user guide](https://github.com/JakimPL/SampleToNES/tree/main/docs/guide) explains how to use the app, step by step. The app opens it in your browser from **Help ▸ Documentation**. The rest of [`docs/`](https://github.com/JakimPL/SampleToNES/tree/main/docs) covers the reconstruction algorithms, the file formats, the Python API and the developer notes.

## License

Expand Down
13 changes: 9 additions & 4 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,15 @@ three years from the date of that release.

### GPU acceleration

The published bundles are **CPU-only**: CuPy, the CUDA runtime and the NVIDIA libraries
are proprietary, and their EULA reserves redistribution to NVIDIA. GPU acceleration comes
from installing _SampleToNES_ from PyPI with the `gpu` extra, which fetches CuPy and the
CUDA components from their publishers straight to your machine.
The published bundles are **CPU-only** and carry no CuPy or CUDA libraries.

CuPy is MIT-licensed. The CUDA libraries (cuBLAS, cuFFT, cuRAND, cuSOLVER, cuSPARSE, NVRTC and the CUDA
runtime) are NVIDIA software under the
[CUDA EULA](https://docs.nvidia.com/cuda/eula/index.html), which lists them as distributable inside an
application with material functionality of its own, without modification and for use by that application
alone. GPU support comes from installing _SampleToNES_ from PyPI with the `gpu` extra, or from building
the standalone app on your own machine with an NVIDIA driver, which bundles the libraries fetched from
NVIDIA's wheels for your own use.

## Build-time tooling

Expand Down
Empty file.
48 changes: 48 additions & 0 deletions assets/documentation/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import sys
from argparse import ArgumentParser
from dataclasses import dataclass
from pathlib import Path
from typing import Final, Optional, Sequence

from assets.documentation.build import SiteRequest, build_site
from assets.documentation.paths import DOCS_DIRECTORY, PROJECT_FILE, SITE_DIRECTORY

PROGRAM: Final[str] = "python -m assets.documentation"
DESCRIPTION: Final[str] = "write the user's documentation as HTML pages in the application's look"
OUTPUT_HELP: Final[str] = "the folder receiving the site; without it, the folder the package ships it from"


@dataclass(frozen=True)
class DocumentationArguments:
"""What a documentation run is given: where the site goes, if anywhere but the package."""

output: Optional[Path]

@classmethod
def parse(cls, argv: Sequence[str]) -> "DocumentationArguments":
"""The arguments ``argv`` states, read by the maker's own parser."""
arguments = cls._parser().parse_args(list(argv))
return cls(output=arguments.output)

@staticmethod
def _parser() -> ArgumentParser:
parser = ArgumentParser(prog=PROGRAM, description=DESCRIPTION)
parser.add_argument("--output", "-o", type=Path, default=None, help=OUTPUT_HELP)
return parser

@property
def directory(self) -> Path:
"""The folder the site is written into: the one given, or the one the package ships it from."""
return self.output if self.output is not None else SITE_DIRECTORY


def main(argv: Sequence[str]) -> int:
"""Writes the documentation site and reports its index."""
given = DocumentationArguments.parse(argv)
site = build_site(SiteRequest(docs=DOCS_DIRECTORY, project=PROJECT_FILE, output=given.directory))
print(f"Wrote {site.index}")
return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
172 changes: 172 additions & 0 deletions assets/documentation/build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import posixpath
import shutil
from dataclasses import dataclass
from pathlib import Path, PurePosixPath
from shutil import copyfile
from string import Template
from typing import Final, Iterable, List, Set

from markdown_it import MarkdownIt
from markdown_it.utils import EnvType

from assets.documentation.links.resolver import EXTERNAL_PREFIXES, BrokenLinkError, LinkResolver
from assets.documentation.markdown.parser import markdown_parser
from assets.documentation.navigation import Navigation
from assets.documentation.pages.model import ParsedPage
from assets.documentation.pages.reader import parse_page
from assets.documentation.pages.sections import image_sources, split_sections
from assets.documentation.paths import STATIC_DIRECTORY
from assets.documentation.project import ProjectFacts
from assets.documentation.published import INDEX_PAGE, page_path, published_pages
from assets.documentation.rendering.cards import section_card
from assets.documentation.rendering.navigation import navigation_html
from assets.documentation.rendering.page import LAYOUT_DIRECTORY, PageChrome, PageContent, render_page
from sampletones_application.utils.palette.catalog import DEFAULT_PALETTE_NAME
from sampletones_shared.documentation.site import DocumentationSite
from sampletones_tools.web.design import ApplicationDesign
from sampletones_tools.web.writer import write_layout

TEMPLATE_FILE: Final[str] = "page.html"
DOCUMENTATION_STYLESHEET: Final[str] = "documentation.css"
TEXT_NEWLINE: Final[str] = "\n"
PARENT: Final[str] = ".."


class OutputOccupiedError(ValueError):
pass


@dataclass(frozen=True)
class SiteRequest:
"""Where the documentation is read from and where its site is written.

Attributes:
docs: The docs folder of a checkout.
project: The checkout's ``pyproject.toml``, for the version and the online address.
output: The folder the site is written into; empty, or holding an earlier site.
"""

docs: Path
project: Path
output: Path


def build_site(request: SiteRequest) -> DocumentationSite:
"""Write the user's documentation as HTML pages in the application's look.

Every published document becomes a page at the same place in the site, wearing the web
layout in the application's default palette, with the navigation the docs index lays out. The
pictures the pages show are copied beside them, and the site is sealed with its stamp last.

Raises:
OutputOccupiedError: If the output holds files other than an earlier site.
BrokenLinkError: If a page links to a missing document, heading or picture.
"""
_prepare(request.output)
facts = ProjectFacts.read(request.project)
parser = markdown_parser()
pages = {relative: parse_page(parser, request.docs, relative) for relative in published_pages(request.docs)}
navigation = Navigation.read(pages[INDEX_PAGE], pages)
resolver = LinkResolver(
request.docs,
{relative: page.anchors for relative, page in pages.items()},
navigation.folder_pages,
facts.documentation_address,
)
design = ApplicationDesign.load(DEFAULT_PALETTE_NAME)
layout = request.output / LAYOUT_DIRECTORY
stylesheets = write_layout(layout, design)
copyfile(STATIC_DIRECTORY / DOCUMENTATION_STYLESHEET, layout / DOCUMENTATION_STYLESHEET)
chrome = PageChrome(
version=facts.version,
address=facts.documentation_address,
stylesheets=(*stylesheets, DOCUMENTATION_STYLESHEET),
)
template = Template((STATIC_DIRECTORY / TEMPLATE_FILE).read_text(encoding="utf-8"))
pictures: Set[PurePosixPath] = set()
for page in pages.values():
_resolve_links(page, resolver)
html = _page_html(parser, template, chrome, navigation, page, design.glyphs.common.tick)
target = request.output / page_path(page.relative)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(html, encoding="utf-8", newline=TEXT_NEWLINE)
pictures.update(_pictures(request.docs, page))

_copy_pictures(request.docs, request.output, pictures)
site = DocumentationSite(request.output)
site.write_stamp()
return site


def _prepare(output: Path) -> None:
site = DocumentationSite(output)
if output.exists() and any(output.iterdir()):
if site.read_stamp() is None:
raise OutputOccupiedError(f"{output} holds files other than a documentation site; give an empty folder")

shutil.rmtree(output)

output.mkdir(parents=True, exist_ok=True)


def _resolve_links(
page: ParsedPage,
resolver: LinkResolver,
) -> None:
for token in page.tokens:
for child in token.children or []:
href = child.attrGet("href") if child.type == "link_open" else None
if href is not None:
child.attrSet("href", resolver.resolve(str(href), page.relative))


def _page_html(
parser: MarkdownIt,
template: Template,
chrome: PageChrome,
navigation: Navigation,
page: ParsedPage,
glyph: str,
) -> str:
env: EnvType = {}

def inline(text: str) -> str:
return str(parser.renderInline(text, env))

sections = split_sections(parser, page.tokens, env)
content = PageContent(
relative=page.relative,
title_html=inline(page.title),
navigation_html=navigation_html(navigation, page.relative, sections, inline),
cards_html="\n".join(section_card(section, glyph) for section in sections),
)
return render_page(template, chrome, content)


def _pictures(
docs: Path,
page: ParsedPage,
) -> List[PurePosixPath]:
pictures: List[PurePosixPath] = []
for source in image_sources(page.tokens):
if source.startswith(EXTERNAL_PREFIXES):
continue

target = posixpath.normpath(posixpath.join(page.relative.parent.as_posix(), source))
if target.startswith(PARENT) or not (docs / target).is_file():
raise BrokenLinkError(f"{page.relative} shows a picture outside the docs or missing: {source}")

pictures.append(PurePosixPath(target))

return pictures


def _copy_pictures(
docs: Path,
output: Path,
pictures: Iterable[PurePosixPath],
) -> None:
for picture in sorted(pictures):
target = output / picture
target.parent.mkdir(parents=True, exist_ok=True)
copyfile(docs / picture, target)
Empty file.
Loading
Loading