Skip to content
Merged
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
12 changes: 6 additions & 6 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: Tests

# Documentation-only changes can't change these results, so they don't run them.
# Past that, the scope job reads the diff and each job runs only for the paths it
# checks: ostinato for apps/ostinato and lib/ (its page loads studio's lib/), and
# checks: ostinato for programs/ostinato and lib/ (its page loads studio's lib/), and
# engines for lib/, tests/ and this workflow (which holds its pins). A push to
# main is scoped to what that push changed. A manual run, or a push with no
# earlier commit to compare with, runs both.
Expand Down Expand Up @@ -78,7 +78,7 @@ jobs:
files=$(git diff --name-only "$BASE...$GITHUB_SHA")
echo "$files"
has() { grep -qE "$1" <<< "$files" && echo true || echo false; }
echo "ostinato=$(has '^(apps/ostinato/|lib/|\.github/workflows/tests\.yml$)')" >> "$GITHUB_OUTPUT"
echo "ostinato=$(has '^(programs/ostinato/|lib/|\.github/workflows/tests\.yml$)')" >> "$GITHUB_OUTPUT"
echo "engines=$(has '^(lib/|tests/|\.github/workflows/tests\.yml$)')" >> "$GITHUB_OUTPUT"
echo "synthstation=$(has '^(apps/synthstation/|lib/|\.github/workflows/tests\.yml$)')" >> "$GITHUB_OUTPUT"
echo "appkit=$(has '^(lib/appkit/|tests/appkit/|\.github/workflows/tests\.yml$)')" >> "$GITHUB_OUTPUT"
Expand All @@ -99,11 +99,11 @@ jobs:
python-version: "3.x"

- name: Vendor PyScript
working-directory: apps/ostinato
working-directory: programs/ostinato
run: ./scripts/vendor_pyscript.sh

- name: Ostinato's tests
working-directory: apps/ostinato
working-directory: programs/ostinato
run: python -m unittest discover -s tests -v

# Synth Station's tests on CPython, against controlkit from lib/,
Expand Down Expand Up @@ -240,7 +240,7 @@ jobs:
with:
python-version: "3.13"

# Studio's controlkit tests import lvgl on CPython.
# The controlkit tests import lvgl on CPython.
- name: Install LVGL for CPython
run: |
sudo apt-get update && sudo apt-get install -y libsdl2-2.0-0
Expand Down Expand Up @@ -287,7 +287,7 @@ jobs:
make -C micropython/ports/unix submodules
make -C micropython/ports/unix -j4 USER_C_MODULES=$GITHUB_WORKSPACE/modules

- name: Studio's tests
- name: The apps repo's tests
working-directory: studio
env:
PYDEVICES_MICROPYTHON: ${{ github.workspace }}/micropython/ports/unix/build-standard/micropython
Expand Down
4 changes: 2 additions & 2 deletions .style-exclude
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Paths the style check skips: code that isn't ours, and generated files.
# One glob per line.

# Written by apps/ostinato/scripts/make_icon_font.py.
apps/ostinato/app/ui/icons.py
# Written by programs/ostinato/scripts/make_icon_font.py.
programs/ostinato/app/ui/icons.py
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
- The contract runs on MicroPython and CircuitPython too: no `os.path`, no f-strings with `=`, and `memoryview.cast` isn't there (use `array.array("h", bytes(buf))`). CircuitPython's audio buffers count samples, not bytes.
- `tools/gate.py` drives Windows from WSL: REAPER, a loopback MIDI port and a CPython venv on Windows. A Windows process outlives a timeout on the WSL side; the gate ends them by command line.
- REAPER sends CC0 = 0 to every part when it stops. What a part played is in `Part.played`, not in its bank at the end.
- Applications live in `apps/` (Ostinato, Vamp). Libraries live in `lib/` (soundmodule, songmodel). Code two of them share goes in `lib/` (`songmodel`); Ostinato's `tools/gen_boot.py` lists what its page loads from there, so run it after adding a shared file.
- Home apps (an `app.json`, run by appkit) live in `apps/`; things that run on their own (Ostinato, Vamp) live in `programs/`. Libraries live in `lib/` (soundmodule, songmodel). Code two of them share goes in `lib/` (`songmodel`); Ostinato's `tools/gen_boot.py` lists what its page loads from there, so run it after adding a shared file.
- Vamp's tests need hmilink: `$HMILINK_PATH`, or a `pydevices` checkout beside this one. CI sets `VAMP_REQUIRE=1` so a missing piece fails instead of skipping.
- sayso's tests: `PYTHONPATH=lib python -m unittest discover -s tests/sayso` from the root. The offline tests need no credentials; `test_live.py` skips without a `sayso.local.json`. Never commit `sayso.local.json`, `tokens.json` or `lib/sayso/examples/write_examples.json`.
- To test sayso without a network, replace `sayso.transport.requests` with a mock that has `get`/`post` (and `put`/`delete` for writes) before you build `SpotifyClient`.
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ cloned beside this repository (so the real-instrument checks run):

```bash
python -m unittest discover -s tests
cd apps/ostinato && python -m unittest discover -s tests
cd programs/ostinato && python -m unittest discover -s tests
```

MicroPython and CircuitPython runs get `MICROPYPATH` from the test wrapper
Expand Down Expand Up @@ -45,7 +45,7 @@ bank at the end. The gate's docstring lists what it needs on Windows.

## Ostinato's checks

From `apps/ostinato`, run them all in one app session, about four minutes, against a real Chrome:
From `programs/ostinato`, run them all in one app session, about four minutes, against a real Chrome:

```bash
.venv/bin/python tools/check_all.py --cdp http://127.0.0.1:9222
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Apps

The apps for PyDevices: things you run on a board or a desktop.
The apps for PyDevices: things you run on a board or a desktop. A Home app lives in `apps/`; a program, which runs on its own, lives in [`programs/`](programs/README.md).

## Install

Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ keyboard sends it over MIDI, the way a hardware tone module does.

- A published copy of the app on the web, so it installs without running a
local server.
- The rest is in [Ostinato's roadmap](apps/ostinato/ROADMAP.md).
- The rest is in [Ostinato's roadmap](programs/ostinato/ROADMAP.md).

## Synth Station

Expand Down
2 changes: 1 addition & 1 deletion apps/midiengine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
starts; a channel's instrument is made the first time it is played or given a program, and its
samples are read on its notes, a bounded number of them kept. A tap on a piece only starts the
player's clock: its first events go on the app's timer.
The MIDI it answers is the sound module's (studio's ``soundmodule.engine``).
The MIDI it answers is the sound module's (the apps repo's ``soundmodule.engine``).
"""

import lvgl as lv
Expand Down
4 changes: 2 additions & 2 deletions docs/get-started/first-program.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
[`tools/render_mid.py`](../../tools/render_mid.py) plays a Standard MIDI File
through the sound module and writes the result to a WAV. It needs no MIDI
hardware and no sound card, and it is the same engine that plays live MIDI.
You need [studio installed](install.md) and a SoundFont.
You need the [apps repo installed](install.md) and a SoundFont.

From the studio folder:
From the apps folder:

```bash
python tools/render_mid.py tests/midi/musicbox1_spooky_waltz.mid take.wav --bank GeneralUser-GS.sf2 --path lib
Expand Down
6 changes: 3 additions & 3 deletions docs/get-started/install.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Install

studio is used from a clone, beside the libraries it builds on. The sound
The apps repo is used from a clone, beside the libraries it builds on. The sound
module and Vamp need audiodsp, which provides the audio nodes, and
audioinstruments and audioeffects, which provide the instruments and effects:

```bash
git clone https://github.com/PyDevices/studio
git clone https://github.com/PyDevices/apps
git clone https://github.com/PyDevices/audioinstruments
git clone https://github.com/PyDevices/audioeffects
git clone https://github.com/PyDevices/audiodsp
Expand All @@ -15,7 +15,7 @@ pip install --no-deps ./audioinstruments ./audioeffects/lib/audioeffects

## A SoundFont

The sound module plays General MIDI from a SoundFont, which studio does not
The sound module plays General MIDI from a SoundFont, which the apps repo does not
ship. Download a General MIDI `.sf2`, such as
[GeneralUser GS](https://schristiancollins.com/generaluser.php), and either
pass it with `--bank`, set the `SOUNDFONT` environment variable, or put it in
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/play-the-sound-module.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ module does. You set nothing up on it: each part's Bank Select and Program
Change pick the sound, so a DAW's own instrument-selection UI is enough. The
[reference](../reference/sound-module-midi.md) lists the banks.

You need [studio installed](../get-started/install.md), a SoundFont, and a
You need the [apps repo installed](../get-started/install.md), a SoundFont, and a
virtual MIDI port your DAW can send to, such as a loopback port on Windows.

## Run it on a port
Expand Down
12 changes: 6 additions & 6 deletions docs/guides/put-vamp-on-a-board.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Ostinato's window and a browser page edit the song it plays, live, over
[hmilink](https://github.com/PyDevices/pydevices/tree/main/hmilink). A MIDI
keyboard on the board plays the selected track.

You need [studio installed](../get-started/install.md), a General MIDI
You need the [apps repo installed](../get-started/install.md), a General MIDI
SoundFont, [mpftp](https://github.com/PyDevices/mpftp) installed, and a clone
of [pydevices](https://github.com/PyDevices/pydevices) beside studio, where
of [pydevices](https://github.com/PyDevices/pydevices) beside the apps repo, where
`tools/vamp_files.py` finds hmilink (`--hmilink DIR` names it otherwise).

## On a board
Expand All @@ -29,25 +29,25 @@ import vamp; vamp.run()

## On a desktop MicroPython

With studio's `lib/`, hmilink, audioinstruments' and audioeffects' `lib/` and pydevices' `lib/`
With the apps repo's `programs/` and `lib/`, hmilink, audioinstruments' and audioeffects' `lib/` and pydevices' `lib/`
on `MICROPYPATH`:

```bash
micropython -X heapsize=96M -m vamp.main --bank GeneralUser-GS.sf2 --song apps/ostinato/songs/sketches/soundfont_check.json
micropython -X heapsize=96M -m vamp.main --bank GeneralUser-GS.sf2 --song programs/ostinato/songs/sketches/soundfont_check.json
```

## Open a panel

Browse to `http://HOST:9100/` for the browser panel, or join with Ostinato's
window, from `apps/ostinato`:
window, from `programs/ostinato`:

```bash
python -m ostinato --connect HOST:9100
```

Every panel sees the same song, and an edit on one shows on the others while
the box keeps playing. The options (`--port`, `--midi`, `--lookahead-ms`,
`--seconds`) are listed at the top of [`apps/vamp/main.py`](../../apps/vamp/main.py).
`--seconds`) are listed at the top of [`programs/vamp/main.py`](../../programs/vamp/main.py).

## Check that a box is behaving

Expand Down
8 changes: 4 additions & 4 deletions docs/guides/run-ostinato.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

Ostinato is a pattern sequencer and song editor. It runs in a browser, where
you can install it from the address bar as an app that works offline, and as
a native window on the desktop. It lives in [`apps/ostinato`](../../apps/ostinato).
a native window on the desktop. It lives in [`programs/ostinato`](../../programs/ostinato).

The steps are in its [README](../../apps/ostinato/README.md), and every
The steps are in its [README](../../programs/ostinato/README.md), and every
command there runs from that folder:

- **In a browser.** Fetch the PyDevices wheels it runs on, vendor PyScript
Expand All @@ -15,15 +15,15 @@ command there runs from that folder:

Both read the song model in [`lib/songmodel`](../../lib/songmodel), which
Vamp uses too. `tools/serve.py` serves that folder from this repository, so
run it from a studio checkout.
run it from an apps checkout.

## Use a SoundFont in Ostinato

The two SoundFont instruments, **SOUNDFONT** and **SOUNDFONT DRUMS**, play
whatever `.sf2` they find: the `SOUNDFONT` environment variable, else the
first one in `%USERPROFILE%\soundfonts` (`~/soundfonts` elsewhere). Without
one the track stays silent and the status line says so.
[`apps/ostinato/songs/sketches/soundfont_check.json`](../../apps/ostinato/songs/sketches/soundfont_check.json)
[`programs/ostinato/songs/sketches/soundfont_check.json`](../../programs/ostinato/songs/sketches/soundfont_check.json)
plays all four tracks on them.

## Give it a second panel
Expand Down
4 changes: 2 additions & 2 deletions docs/how-it-works/how-its-checked.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# How it is checked

Every check in studio has been shown to fail on a fault planted on purpose.
Every check in the apps repo has been shown to fail on a fault planted on purpose.
`tests/engine_contract.py` keeps a `PLANTS` table, one broken-on-purpose
variant per check, and the suite runs the contract once per plant and expects
it to fail. A check that has only ever passed proves nothing, so a new check
Expand Down Expand Up @@ -46,6 +46,6 @@ turn. They need a Windows machine as well as WSL.
Ostinato's timing is measured in Chrome, because headless Chromium's audio
clock is not reliable under load. Its checks, which cover MIDI-to-sound
latency, the editing workflows, file export and offline start, are listed in
the [Ostinato README](../../apps/ostinato/README.md#checks).
the [Ostinato README](../../programs/ostinato/README.md#checks).

To run them yourself, see [CONTRIBUTING.md](../../CONTRIBUTING.md).
8 changes: 4 additions & 4 deletions docs/how-it-works/overview.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# What is in studio
# What is in the apps repo

studio holds three applications that share a song model and an audio stack.
The apps repo holds two programs and a sound module that share a song model and an audio stack.

- **soundmodule** ([`lib/soundmodule`](../../lib/soundmodule)) is a MIDI
tone module. Its engine has no face of its own: feed it MIDI bytes and pull
its `output`, from a headless loop (`soundmodule.run`), a UI or a test.
- **Ostinato** ([`apps/ostinato`](../../apps/ostinato)) is the sequencer and
- **Ostinato** ([`programs/ostinato`](../../programs/ostinato)) is the sequencer and
song editor. LVGL draws its interface in a browser worker or a desktop
window, and audioinstruments makes the sound.
- **Vamp** ([`apps/vamp`](../../apps/vamp)) plays Ostinato's songs on a
- **Vamp** ([`programs/vamp`](../../programs/vamp)) plays Ostinato's songs on a
MicroPython with no screen of its own. Its panels are the Ostinato window
and a browser page, over the network.

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ through the sound module.
**Reference.** [The apps, and what each does](reference/apps.md). [What an appkit app must provide](reference/app-requirements.md). [The sound module's MIDI](reference/sound-module-midi.md): banks,
controllers, System Exclusive, and the `soundmodule.run` options. Vamp's
options are listed at the top of
[`apps/vamp/main.py`](../apps/vamp/main.py).
[`programs/vamp/main.py`](../programs/vamp/main.py).

**How it works.** [How the apps are built](how-it-works/apps.md), [what is in the apps](how-it-works/overview.md) and
[how it is checked](how-it-works/how-its-checked.md).
Expand Down
3 changes: 1 addition & 2 deletions docs/reference/apps.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Apps

What each app here does. Apps that run on a board are also listed in the
What each app in `apps/` does. These are the apps Home lists and appkit runs; things that run on their own, like Ostinato and Vamp, are in [programs](../../programs/README.md). Apps that run on a board are also listed in the
[gallery](https://gallery.omnipython.org).

| App | What it does |
Expand All @@ -17,4 +17,3 @@ What each app here does. Apps that run on a board are also listed in the
| [Sound card](../../apps/card) | The board is a USB speaker for a computer, with a spectrum of what plays. |
| [System](../../apps/system) | Whether the board is well, at a glance: a turning arc and frame rate, a touch test, memory, boot, network, audio and what the board is; `stats()` gives the same from the REPL. |
| [Wake word](../../apps/wake) | Listens on a USB microphone for "Hey Jarvis" and flashes on each detection. |
| [Ostinato](../../apps/ostinato) | A pattern sequencer and song editor in a browser or on the desktop. |
4 changes: 2 additions & 2 deletions lib/songmodel/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#
# SPDX-License-Identifier: MIT

"""The song both of studio's groovebox applications edit, and what carries it
"""The song both of this repo's groovebox applications edit, and what carries it
to a front panel.

- `songmodel.model`: the project, plain JSON data changed only by ops that
Expand All @@ -12,7 +12,7 @@
be a front panel (needs ``hmilink``, from pydevices).
- ``page.html``: the browser front panel a device serves on its hmilink port.

Ostinato (``apps/ostinato``) and Vamp (``apps/vamp``) both use it.
Ostinato (``programs/ostinato``) and Vamp (``programs/vamp``) both use it.
"""


Expand Down
14 changes: 14 additions & 0 deletions programs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Programs

A program runs on its own: you start it, and it doesn't need Home. A Home app
is different. It lives in [`apps/`](../apps), has an `app.json`, and appkit
runs it from the home screen.

| Program | What it does |
|---|---|
| [Ostinato](ostinato) | A pattern sequencer and song editor, in a browser or a desktop window. |
| [Vamp](vamp) | Plays Ostinato's songs on one MicroPython, with the screens elsewhere. |

Every program folder has a `README.md` that opens with what the program does,
then a `Runs on:` line naming the boards or computers it works on, then how to
put it there.
File renamed without changes.
10 changes: 6 additions & 4 deletions apps/ostinato/README.md → programs/ostinato/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ synth from a MIDI keyboard, put drums on a step grid, record loops and fix
them in a piano roll, chain patterns into a song, put effects on each track,
and export a WAV or a MIDI file. It installs as an app and works offline.

Runs on: a desktop browser (Chrome or Edge) or a Windows desktop with Python 3.14 or later. Put it there with the steps under [Run it](#run-it) or [Run it on the desktop](#run-it-on-the-desktop).

It's built from PyDevices: LVGL draws the interface, audioinstruments' 33
instruments and 44 effects make the sound, and `midiports` and
`adafruit_midi` read the keyboard, all running in Pyodide in Web Workers.

## Run it

Every command here runs from this folder, `apps/ostinato` in studio.
Every command here runs from this folder, `programs/ostinato` in the apps repo.

```bash
.venv/bin/python scripts/fetch_wheels.py # the PyDevices wheels it runs on
Expand Down Expand Up @@ -65,7 +67,7 @@ venv above unless `PY315` names another interpreter.
track's mute and volume, and a step grid. An edit on either shows on the other.
`--device` is that process alone, with no window, and `--connect HOST:PORT`
is the window alone, joining one that's already running. That one can be
Vamp, studio's headless groovebox: the window is one of its front panels.
Vamp, the apps repo's headless groovebox: the window is one of its front panels.

The two SoundFont instruments, **SOUNDFONT** (General MIDI's 128 programs)
and **SOUNDFONT DRUMS** (its kits), play whatever `.sf2` they find: the
Expand Down Expand Up @@ -107,13 +109,13 @@ for the rest; the script says how many bars that changed.
its track and hands it to the master worker (`app/engine/master.py`),
which sums them, runs the master effects, and is the one stream the
AudioWorklet (`workers/audio_worklet.js`) plays.
- The project is `songmodel.model`, in studio's `lib/songmodel/`: plain data,
- The project is `songmodel.model`, in the apps repo's `lib/songmodel/`: plain data,
changed only by ops that both sides apply. Vamp plays the same songs.
- `workers/` is pyscript-template's worker kit.

`songmodel.model` imports on CPython, Pyodide and MicroPython. The engine and
UI around it haven't been tried on a board yet; on a board, the songs play on
Vamp instead (`apps/vamp/`).
Vamp instead (`programs/vamp/`).

## Checks

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
python -m ostinato --role device [--port 9100] [--song FILE]

It runs the engines and the mixer (desktop/runners.py) and holds the song
(songmodel.link, in studio's lib/). Panels connect on one port: the desktop UI
(songmodel.link, in the apps repo's lib/). Panels connect on one port: the desktop UI
(``python -m ostinato --connect 127.0.0.1:9100``, or ``--split``, which
starts this process for you) and any browser, which gets the song page
(songmodel's page.html, in studio's lib/) at http://<this machine>:9100/.
(songmodel's page.html, in the apps repo's lib/) at http://<this machine>:9100/.

Every edit is a JSON Patch op a panel sends. The device numbers it, applies
it to the song and to the engines, and sends it to every panel. The engines'
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes
File renamed without changes
File renamed without changes.
File renamed without changes.
Loading
Loading