Skip to content
Merged

Logo #52

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ jobs:
- name: Run every pre-commit hook
run: uv run pre-commit run --all-files --show-diff-on-failure --color always

- name: Check the committed icons match the mark
run: uv run pre-commit run icons --all-files --hook-stage pre-push --color always

tests:
name: Tests (${{ matrix.os }}, py${{ matrix.python }})
runs-on: ${{ matrix.os }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
venv_python=.venv-build/bin/python
fi
"$venv_python" -m pip install --upgrade pip
"$venv_python" -m pip install ".[build]"
"$venv_python" -m pip install ".[build]" --group assets

- name: Build the bundle (Linux)
if: runner.os == 'Linux'
Expand Down
11 changes: 3 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,20 @@ __pycache__/
.ipynb_checkpoints/
.mypy_cache/
.pytest_cache/
.ruff_cache/
.venv/
.venv-build/
.vscode/

bin/
build/
dist/
wheels/
!scripts/**/build/
/bin/
/build/

sampletones
!src/sampletones
!tests/sampletones

**/*.idea
**/*.vscode/**
**/*.ipynb_checkpoints/**
**/*__pycache__/**

*.pyc
*.pyo
*.coverage
Expand Down
10 changes: 10 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,16 @@ repos:
require_serial: true
exclude: ^(tests/)

- id: icons
name: icons
entry: uv run python scripts/assets/icons.py
language: system
files: ^src/sampletones_assets/(icons|mark)/
pass_filenames: false
verbose: true
stages:
- pre-push

- id: pytest
name: pytest
entry: make test
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
* Improved Sequencer module playback.
* Added song export to WAV/MP3.
* Added tracker selection operations.
* Added a _SampleToNES_ logo.

## v0.3.0 [2026-07-31]

Expand Down
4 changes: 3 additions & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ SOFTWARE.

---

The MIT license above covers the SampleToNES source code only.
The MIT license above covers the SampleToNES source code and the application
icons under `src/sampletones_assets/icons/`, which are drawn from the mark
declared in `src/sampletones_assets/mark/`.

Font files bundled under `src/sampletones_assets/fonts/` are the work of third
parties and remain under their own licenses (SIL Open Font License 1.1 and the
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.PHONY: help setup install build release system-deps run clean pre-commit test \
ftm-samples check-import-boundary check-tag-names check-unused-tags \
ftm-samples icons check-import-boundary check-tag-names check-unused-tags \
check-language-keys check-palette-colors calibration lint pylint mypy format

ifeq ($(OS),Windows_NT)
Expand Down Expand Up @@ -70,6 +70,7 @@ help:
@echo $(Q) make release - Compile standalone executable with the release deployment config$(Q)
@echo $(Q) make test - Run unit tests with coverage$(Q)
@echo $(Q) make ftm-samples - Emit example .ftm files to build/ftm via the integration suite$(Q)
@echo $(Q) make icons - Generate the icon suite into src/sampletones_assets/icons$(Q)
@echo $(Q) make calibration - Score the reconstruction corpus; the report lands in Documents/SampleToNES/calibration$(Q)
@echo $(Q) make clean - Remove build artifacts and cache files$(Q)
@echo $(Q) make lint - Run linting (pylint, mypy)$(Q)
Expand All @@ -78,6 +79,7 @@ help:

setup:
$(SETUP_ENV) uv sync --group dev $(if $(GPU_EXTRA),--extra $(GPU_EXTRA),)
$(MAKE) icons
$(SETUP_ENV) uv tool install --force $(if $(GPU_EXTRA),".[$(GPU_EXTRA)]",.)

install:
Expand Down Expand Up @@ -109,6 +111,9 @@ ftm-samples: export SAMPLETONES_FTM_OUTPUT_DIR := build/ftm
ftm-samples:
uv run python -m pytest tests/integration/famitracker

icons:
uv run --group assets python scripts/assets/icons.py

check-import-boundary:
uv run scripts/checks/import_boundary.py --all

Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@
[![Python](https://img.shields.io/pypi/pyversions/sampletones.svg)](https://pypi.org/project/sampletones/)
[![License](https://img.shields.io/pypi/l/sampletones.svg)](https://github.com/JakimPL/SampleToNES/blob/main/LICENSE)

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

## Overview

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

<img src="docs/images/sampletones.png" alt="SampleToNES" width="640">
<img src="https://raw.githubusercontent.com/JakimPL/SampleToNES/main/docs/images/sampletones.png" alt="SampleToNES" width="640">

The core idea is to approximate an audio sample using only the chip's basic oscillators — two pulse channels, a triangle, and noise — **without any DPCM samples**.

Expand Down
15 changes: 15 additions & 0 deletions THIRD-PARTY-NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,18 @@ The published bundles are **CPU-only**: CuPy, the CUDA runtime and the NVIDIA li
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.

## Build-time tooling

The application icons are drawn by `sampletones_assets.mark` and rasterized with
[Pillow](https://pypi.org/project/Pillow/), which is under the
[MIT-CMU license](https://github.com/python-pillow/Pillow/blob/main/LICENSE). Pillow belongs
to the `assets` dependency group alone, so `pip`/`uv` installs it on the machine that
generates the icons: it stays out of the wheel's dependency set, and PyInstaller is told to
leave it out of the bundles. Both distributions carry the finished icon files, so Pillow's
attribution clause — a condition on redistributing Pillow itself — rests with the build
environment.

The icons (`sampletones.svg`, `sampletones.png` and the multi-resolution `sampletones.ico`)
are original _SampleToNES_ artwork and fall under the MIT License together with the rest of
the source.
21 changes: 21 additions & 0 deletions docs/development/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ Dialogs open through the XDG desktop portal (`org.freedesktop.portal.FileChooser

`jeepney` is declared for Linux alone, so the modules that speak to the portal are imported where it is installed: the application probes for it before reaching them, and the root `conftest.py` keeps them out of collection elsewhere, leaving the Linux runs of the suite to cover them.

## Application icon

The icon suite in `src/sampletones_assets/icons` is generated from the mark declared beside it in
`src/sampletones_assets/mark`: `mark.yaml` carries the geometry, colours and rasterization
settings, validated as a `Mark`, and `template.svg` is the vector the rendered geometry fills. The
package writes the whole suite — the vector `sampletones.svg` and the rasters the application
ships, `sampletones.png` and the multi-resolution `sampletones.ico` — and `scripts/assets/icons.py`
points it at the directory the icons are shipped from. Rasterization uses Pillow, declared in the
`assets` dependency group.

The whole suite is committed, so a plain checkout carries the icons the application opens its window
with, and every wheel, bundle and test run finds them where they lie. `make icons` writes them again
from the mark, and the `icons` pre-push hook writes them for a push that touches either directory,
holding the committed files to what the mark describes. CI runs that same hook.

Pillow is a build-time tool, and the bundle scripts pass `--exclude-module PIL` to hold it to that:
`pygments`, which arrives with `rich`, offers an image formatter that imports Pillow where it is
installed, and PyInstaller follows that import into the bundle. The application reads its icons as
files, so the exclusion spares every bundle Pillow's extension modules and the imaging libraries
that come with them. `scripts/ci/checks/bundle.py` holds the release bundles to it.

## Linux (standalone executable)

Building a standalone executable on Linux needs the PortAudio, Tk and OpenGL/X11 system packages. Install them with `make system-deps` (or run `scripts/linux/build/dependencies.sh`), which holds the full list.
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@ gpu-cuda11 = [
]

[dependency-groups]
assets = ["pillow>=11,<13"]
dev = [
{ include-group = "assets" },
"black==26.5.1",
"isort==8.0.1",
"mypy==2.1.0",
Expand Down Expand Up @@ -129,6 +131,7 @@ addopts = "--import-mode=importlib"
[tool.coverage.run]
source = [
"sampletones_application",
"sampletones_assets",
"sampletones_core",
"sampletones_shared",
"sampletones_synthesis",
Expand All @@ -143,6 +146,7 @@ python_version = "3.12"
files = [
"src/sampletones",
"src/sampletones_application",
"src/sampletones_assets",
"src/sampletones_core",
"src/sampletones_shared",
"src/sampletones_synthesis",
Expand Down
46 changes: 46 additions & 0 deletions scripts/assets/icons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3

"""
Writes the application icon suite from the packaged mark definition.

The mark, its template and the code drawing them live in `sampletones_assets/mark`; this
script points them at the directory the icons are shipped from.

Usage:
python scripts/assets/icons.py # write the suite into src/sampletones_assets/icons
"""

import argparse
import sys
from pathlib import Path
from typing import Final, Sequence

from sampletones_assets.mark.specification import Mark
from sampletones_assets.mark.suite import write_icon_suite

REPOSITORY_ROOT: Final[Path] = Path(__file__).resolve().parents[2]
ICONS_DIRECTORY: Final[Path] = REPOSITORY_ROOT / "src" / "sampletones_assets" / "icons"


def main(argv: Sequence[str]) -> int:
"""Writes the icon suite and reports each file it produced."""

parser = argparse.ArgumentParser(
description="Write the application icon suite from the mark definition.",
)
parser.add_argument(
"--directory",
type=Path,
default=ICONS_DIRECTORY,
help="directory receiving the icon files",
)
arguments = parser.parse_args(list(argv))

for path in write_icon_suite(arguments.directory, Mark.load()):
print(f"Wrote {path}")

return 0


if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
2 changes: 1 addition & 1 deletion scripts/calibration.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
GeneratorName,
SpectrumMethod,
)
from sampletones_core.paths import USER_PATH_DOCUMENTS
from sampletones_shared.logger import logger
from sampletones_shared.paths.user import USER_PATH_DOCUMENTS

DEFAULT_OUTPUT_ROOT: Final[Path] = USER_PATH_DOCUMENTS / "calibration"
DEFAULT_METHODS: Final[str] = f"{SpectrumMethod.FFT.value},{SpectrumMethod.CQT.value}"
Expand Down
2 changes: 1 addition & 1 deletion scripts/checks/language_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
from sampletones_shared.meta.source.modules import discover_modules, module_name
from sampletones_shared.meta.source.packages import package_directory
from sampletones_shared.meta.source.values import EnumMembers, EnumTable
from sampletones_shared.paths import SOURCE_ROOT
from sampletones_shared.paths.source import SOURCE_ROOT

EnumPredicate = Callable[[object], bool]

Expand Down
2 changes: 1 addition & 1 deletion scripts/checks/palette_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from sampletones_shared.meta.source.modules import SourceModule, discover_modules
from sampletones_shared.meta.source.nodes import terminal_name
from sampletones_shared.meta.source.packages import package_directory
from sampletones_shared.paths import CONFIG_DIRECTORY
from sampletones_shared.paths.resources import CONFIG_DIRECTORY

APPLICATION_PACKAGE: Final[Path] = package_directory("sampletones_application")

Expand Down
2 changes: 1 addition & 1 deletion scripts/checks/unused_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from sampletones_shared.meta.source.modules import SourceModule, discover_modules
from sampletones_shared.meta.source.packages import package_directory
from sampletones_shared.meta.source.references import count_identifier_loads
from sampletones_shared.paths import REPOSITORY_ROOT, SOURCE_ROOT
from sampletones_shared.paths.source import REPOSITORY_ROOT, SOURCE_ROOT

TAGS_PACKAGE: Final[Path] = package_directory("sampletones_application", "tags")
REFERENCE_ROOTS: Final[Tuple[Path, ...]] = (
Expand Down
21 changes: 20 additions & 1 deletion scripts/ci/checks/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
"THIRD-PARTY-LICENSES.txt",
)

INTERNAL_DIRECTORY: Final[str] = "_internal"
BUILD_TOOLS: Final[Sequence[str]] = ("PIL",)


def launcher_path(bundle: Path, *, system: str) -> Path:
"""The executable a built bundle offers on the platform it was built for."""
Expand All @@ -28,8 +31,19 @@ def missing_notices(bundle: Path) -> List[str]:
return [name for name in REQUIRED_NOTICES if not (bundle / name).is_file()]


def carried_build_tools(bundle: Path) -> List[str]:
"""The build-time packages found in a bundle, which the notices place on the build machine.

A bundle carries the application and its runtime dependencies. Tooling that only draws the
assets belongs to the machine that builds it, so finding it here means the notices describe
a different set of components than the bundle ships.
"""
directories = (bundle, bundle / INTERNAL_DIRECTORY)
return [name for name in BUILD_TOOLS if any((directory / name).is_dir() for directory in directories)]


def main(argv: Sequence[str]) -> int:
"""Confirm a built bundle ships its notices and that its launcher starts."""
"""Confirm a built bundle ships its notices, holds to them, and that its launcher starts."""
parser = argparse.ArgumentParser(
description="Verify a built bundle before it is archived.",
)
Expand All @@ -46,6 +60,11 @@ def main(argv: Sequence[str]) -> int:
print(f"::error::Bundle {bundle} is missing {', '.join(absent)}")
return 1

carried = carried_build_tools(bundle)
if carried:
print(f"::error::Bundle {bundle} carries build-time tooling its notices leave out: {', '.join(carried)}")
return 1

launcher = launcher_path(bundle, system=platform.system())
if not launcher.is_file():
print(f"::error::Bundle {bundle} offers no launcher at {launcher}")
Expand Down
2 changes: 2 additions & 0 deletions scripts/linux/build/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ else
fi

bash "$SCRIPT_DIR/preflight.sh" "$@"
bash "$SCRIPT_DIR/icons.sh"

if [[ -e "${PROJECT_DIR}/bin/sampletones" ]]; then
echo "Removing the previous artifact: ./bin/sampletones"
Expand All @@ -44,6 +45,7 @@ echo "Building executable..."
--add-data "src/sampletones_assets/fonts:assets/fonts" \
--add-data "src/sampletones_config:config" \
--copy-metadata sampletones \
--exclude-module PIL \
"${RELEASE_HOOK_ARGS[@]}" \
"src/sampletones/__main__.py"

Expand Down
12 changes: 12 additions & 0 deletions scripts/linux/build/icons.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash

set -e

SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
. "$SCRIPT_DIR/../lib/root.sh"

PROJECT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
VENV_PY="$PROJECT_DIR/.venv-build/bin/python"

echo "Generating the icon suite..."
"$VENV_PY" scripts/assets/icons.py
2 changes: 1 addition & 1 deletion scripts/linux/build/sampletones.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ echo "Installing dependencies..."

EXTRAS_STR=$(IFS=,; echo "${EXTRAS[*]}")
echo "Installing with extras: $EXTRAS_STR"
"$VENV_PY" -m pip install ".[$EXTRAS_STR]"
"$VENV_PY" -m pip install ".[$EXTRAS_STR]" --group assets

echo "sampletones Python package installed successfully."
2 changes: 2 additions & 0 deletions scripts/windows/build/build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ if "%RELEASE%"=="1" (
)

call "%SCRIPT_DIR%preflight.bat" %* || exit /b 1
call "%SCRIPT_DIR%icons.bat" || exit /b 1

if exist "bin\sampletones.exe" (
echo Removing the previous artifact: bin\sampletones.exe
Expand All @@ -51,6 +52,7 @@ echo Building executable...
--add-data "src\sampletones_assets\fonts;assets\fonts" ^
--add-data "src\sampletones_config;config" ^
--copy-metadata sampletones ^
--exclude-module PIL ^
%RELEASE_HOOK% ^
"src\sampletones\__main__.py" || exit /b

Expand Down
Loading
Loading