Superconductor
A touchscreen control surface for music software.
A small service runs beside your music applications, serves one page to a touchscreen's browser, and holds a single WebSocket to it. Each application dials the service over a socket of its own and declares what it can be controlled by; the page draws widgets bound to those declarations. Tapping the glass changes the music; changing the music changes the glass.
Nothing in this package knows about your studio. An application declares its own controls, and the names in them are yours. MIDI channels, drum note maps and device names live in your own files, never in here.
Status
This is an early release, and it is not polished. It has been played on real hardware most days, by one person — its author — and by nobody else. Expect rough edges, and expect the interface to move: very little here is settled enough to be promised, and a version that changes how something works is more likely than one that does not.
So far it drives Subsequence, and nothing else. The package itself knows nothing about any particular music software — an application dials in and declares what it can be controlled by, and that door is open to anything that can hold a WebSocket and speak the protocol. But Subsequence, a generative MIDI sequencer, is the only application anyone has written an adapter for, so today "an application" means that one. Sampling and radio are intended and not started.
If you do not already run Subsequence, there is nothing here you can play yet. The service will start, serve its page, and wait for an application that is not coming.
What runs today: as many pages as an application declares, holding step grids, pitched note grids with sub-step timing, an instrument's own settings, stacks of generators that contribute to a pattern, and a transport with a bar-beat-step counter. Blocks are arranged by dragging and the arrangement is kept by the application. A generator is wired to the pattern it builds; a grid that belongs to no instrument is patched into as many patterns as you like by dragging a cable from its outlet. Everything below documents one of those.
One thing to know before you play anything into it. A pattern you edit on the glass lives in the running composition and nothing writes it down, so restarting that composition throws away every note you tapped. Until that is settled there are two tools for it, and the habit is to run the first before restarting anything:
python tools/capture_state.py # before
python tools/restore_state.py # after
A restore is additive and cannot clear, so a composition that seeds an opening pattern comes back with that pattern plus whatever was captured.
Those two tools live in the repository rather than in the installed package, as
does compositions/drm1_grid.py below — the only worked example, and the only
place the application-facing API is written down. Installing from a package
index gets you the service and the page; the examples are worth the clone.
What it needs
Genuinely required:
- Python 3.11 or newer for the service.
- A browser that delivers Pointer Events and holds a WebSocket. Any current browser does.
- A network path between the browser, the service and the applications. Loopback is fine if they share a machine.
Everything else is a recommendation with a reason, not a requirement. The development rig is a Raspberry Pi 5 driving a 22-inch capacitive panel over Firefox, with the service and the music applications on a separate machine, but the code assumes none of that: it is where the work was done, not what the product is for.
Running it
pip install superconductor
superconductor # serves on port 8090
superconductor --port 9000 # or wherever you like
Then open http://<that machine>:8090/ on the touchscreen.
The service starts with no configuration at all. A YAML file passed with
--config overrides the defaults:
host: 0.0.0.0 # the interface to listen on
port: 8090 # anything you like; this one is clear of the ports the
# neighbouring applications use
page: grid # which page to serve
It listens on every interface by default, because the usual arrangement has
the panel on a different machine from the service. That is a default chosen for
a use, not a recommendation about your network: there is no authentication in
front of it, so anyone who can reach the port can change your music. Set host
to 127.0.0.1 if the browser is on the same machine, and keep it off any
network you do not trust.
The page shows nothing until an application dials in and declares something, which is the expected state on a fresh start rather than a fault.
The corner of the bar shows the version and the build the page was made from. If the service has newer files than the browser loaded — which happens whenever you change something while a panel is left open — that readout becomes a button saying so, and tapping it loads the new page. It never reloads on its own: somebody may be playing.
Sizing the grid to your hands
The size button on the page sets how big a grid cell is. It starts on fit the glass, which measures your own screen and makes the grid as large as will fit on it — so a panel nothing here was written against still uses all of itself. The named sizes override that:
| Compact, 22 px | Half the tested size. Fits twice the music across the same glass, and frees room for whatever else you want beside it. |
| Snug, 32 px | |
| Tested, 44 px | The size the proof of concept was played at, with taps landing where intended. It is the only one with evidence behind it. |
| Large, 60 px | |
| Huge, 96 px |
None of these is recommended over the others, because the right one depends on your hands, your panel and how far away it is. Someone who wants the most music on the glass and someone who needs a larger target are both served by the same control, and the choice is remembered by the browser that made it — so two people with their own panels do not have to agree.
The chooser itself never shrinks. Whatever size you pick, the way back is the same size it always was.
Pitched patterns
A step grid's cells are on or off. A note grid's cells are notes: one row per pitch, and a cell that carries its own length and velocity.
Press an empty cell to place a note, and keep dragging right to make it longer — it grows a step at a time, and each length is sent as it changes, so the bar you see is never longer than the instrument has agreed to. Press a note to take it away. Beneath the grid is a velocity lane, one bar to a step, aligned with the grid above so a column is the same moment in both; drag a bar up or down to set how hard that note is struck.
Rows are drawn in the order they are declared, so a pitched part lists its
highest note first and a rising line rises. A part that declares visible_rows
shows a window onto a pattern taller than itself — two octaves is twenty-five
rows, and a block tall enough for all of them crowds everything else off the
page. Only the pitches scroll: the velocity lane and the playhead stay put,
because a column is a moment in time and scrolling up and down does not change
the time.
A part declares how many voices its instrument has, and no more than that many
notes sound at once: one for a monophonic synth, four for a Moog Matriarch in its
four-voice mode, and nothing at all for a part with no limit worth stating.
Placing a note that would exceed the count takes an earlier one away, newest
first, and each one taken is reported so the glass never goes dark unexplained.
That is enforced by the application rather than left to the instrument: a monophonic synth handed two notes at once chooses between them by its own note-priority setting, which the panel cannot see — so the glass would show two notes while one sounded. It is counted by extent rather than by starting position, because a note beginning part-way through another is exactly the case the instrument would have to arbitrate.
Rows are names, as they are everywhere here. compositions/drm1_grid.py builds
them from note names and hands the same list to Subsequence as a note map, so
what the panel calls C2 and what the synthesiser plays cannot drift apart.
An instrument's own settings
A params control is a block of an instrument's settings, in the three shapes they come in: a switch, a number you drag, and a choice of named options. Between them those cover every control-change message a Moog Minitaur answers to, and probably most other instruments.
Nothing in this package knows that a switch is a MIDI control change. A composition declares what shape each setting is and what it may hold, and is given a function to call when one moves — which is where a message gets sent, if that is what the setting stands for:
superconductor.subsequence_adapter.Params(
composition,
parameters=[
superconductor.subsequence_adapter.Parameter("glide", "switch", label="Glide"),
superconductor.subsequence_adapter.Parameter("rate", "number", default=24),
superconductor.subsequence_adapter.Parameter(
"shape", "choice", options=[("lcr", "LCR"), ("exp", "EXP")]),
],
on_change=send_setting,
)
The settings worth putting on glass are usually the ones an instrument has no knob for at all — reachable otherwise only through editor software. On the Minitaur that is most of them.
Arranging a page
Tap ARRANGE in the bar. While it is latched the grids stop responding and each block's title bar becomes its handle: drag one and it moves a cell at a time, on the same lattice the steps themselves sit on — so two patterns on a page line up step for step rather than nearly.
Any position is allowed, including on top of another block. The last block you moved is the one on top, which is what makes a busy page workable. Nothing is ever pushed aside to make room: a block you did not touch does not move.
Because a block can be covered completely, and a title bar is the only handle it has, the bar lists every block on the page while you are arranging. Tapping a name brings that block back to the top.
Tap DONE to leave. Outside the latch every touch is a control again, which is what stops a stray finger rearranging a page mid-performance.
An arrangement is saved when you lift your finger from a block that moved, and
it is saved to the application that declared the page, not to your browser. For a composition using PageStore that means a file beside the
composition itself, so a piece and the way you look at it travel together:
link = superconductor.subsequence_adapter.AppLink(
composition,
controls=[...],
pages=[...],
page_store=superconductor.subsequence_adapter.PageStore(
pathlib.Path(__file__).with_suffix(".pages.json")),
)
Leave page_store out and arranging still works — it simply is not kept, and
the panel says so rather than letting you find out at the next reload. Because
the arrangement belongs to the application rather than to one browser, a second
panel sees it too.
Connecting an application
superconductor/subsequence_adapter.py is the worked example. A composition
builds a link, gives it the controls it wants to offer, and starts it:
link = superconductor.subsequence_adapter.AppLink(
composition,
controls=[
superconductor.subsequence_adapter.StepGrid(composition, rows=ROWS, steps=16),
superconductor.subsequence_adapter.Transport(composition),
],
)
link.start()
compositions/drm1_grid.py is a complete example driving a Vermona DRM1: it is
the file that holds the MIDI port, the channel and which drum voice sits on
which row, and it is the file you would copy and change for your own rig.
The adapter imports nothing from the application it serves — it is written against whatever object it is handed. That is deliberate, and it is what keeps this package free of any dependency on a particular piece of music software.
Keeping it running
None of this is required. Superconductor is an ordinary process: start it from a
terminal, from your window manager's autostart, from a tmux session, or from
whatever you already use. It needs no supervisor, and it does not need systemd
to exist.
If you do want it supervised and you have systemd, there are two shapes and the difference is real. A system unit starts at boot with nobody logged in, which is what you want on a machine that boots into being a studio. A user unit needs no root and shares your own environment and files, which suits a machine that is also somebody's desktop — but it starts only when you log in unless you enable lingering.
Both of these have placeholders in capitals. They will not start until you have replaced them, which is deliberate: a unit file that half-works with someone else's paths in it is worse than one that refuses.
A system unit, at /etc/systemd/system/superconductor.service:
[Unit]
Description=Superconductor control surface
After=network-online.target
[Service]
Type=simple
User=REPLACE_WITH_YOUR_USERNAME
ExecStart=/REPLACE/WITH/YOUR/VENV/bin/superconductor
Restart=on-failure
RestartSec=2
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now superconductor
A user unit, at ~/.config/systemd/user/superconductor.service:
[Unit]
Description=Superconductor control surface
[Service]
Type=simple
ExecStart=/REPLACE/WITH/YOUR/VENV/bin/superconductor
Restart=on-failure
RestartSec=2
[Install]
WantedBy=default.target
systemctl --user enable --now superconductor
loginctl enable-linger $USER # only if it should run before you log in
Order never matters. The applications dial the service on a backoff and the panel reconnects on its own, so any of the three can be started, stopped or restarted without the others being told.
Development
From a checkout, rather than from the package index:
pip install -e ".[dev]"
pytest
mypy superconductor
The suite includes tests that drive the page in a real Firefox, so it needs the browser's own dependencies once:
playwright install-deps firefox
Firefox rather than all three browsers — it is a third of the packages, and it
is the browser this is built for. No sudo was needed here.
The page is plain ES modules with no build step: Preact and htm are vendored
under superconductor/client/vendor/, with their licences recorded there. Edit
the files and reload the browser.
What is in here
superconductor/ |
the package — the service, the protocol, the adapter and the page it serves |
compositions/ |
the worked example, and the only place the application-facing API is written down |
tools/ |
probes that stand in for a browser, a capture and a restore, and the theme separation check |
tests/ |
the suite, including the ones that drive a real Firefox |
research/ |
the probes and raw measurements the design was made from |
reviews/ |
the code reviews it has been through |
licences/ |
notices for the vendored face and icons |
research/ and reviews/ are provenance: they record what was true on the day
they were written and are deliberately not kept up to date. They still say
Superintendent, which is what this was called until September 2026, and they
cite a tracker you cannot reach. They are here because the documents that cite
them should be checkable, not because they describe the code as it stands.
Licence
Functional Source License 1.1 with an Apache 2.0 future licence
(FSL-1.1-ALv2) — see LICENSE. You may read, run, modify and
redistribute it for any purpose except competing with it, and each version
converts to Apache 2.0 two years after its release.
The libraries it carries keep their own permissive licences, recorded in
superconductor/client/vendor/README.md.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file superconductor-0.1.1.tar.gz.
File metadata
- Download URL: superconductor-0.1.1.tar.gz
- Upload date:
- Size: 435.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9eeee1e82127e25c9ce3b5ccced74b2ad430b58622bd3a95d11e7e7e6907298a
|
|
| MD5 |
3c7fd25141c932a9591bdc79d2e025d2
|
|
| BLAKE2b-256 |
35b9c5e287b7af401bfcbcf2431e66ae1ec2de66bb4e191c32765e05a2755e8d
|
Provenance
The following attestation bundles were made for superconductor-0.1.1.tar.gz:
Publisher:
publish.yml on simonholliday/superconductor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
superconductor-0.1.1.tar.gz -
Subject digest:
9eeee1e82127e25c9ce3b5ccced74b2ad430b58622bd3a95d11e7e7e6907298a - Sigstore transparency entry: 2765796553
- Sigstore integration time:
-
Permalink:
simonholliday/superconductor@bb61a66c0e6be1fcbb39f7ddd7459a9a2f2d012f -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/simonholliday
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bb61a66c0e6be1fcbb39f7ddd7459a9a2f2d012f -
Trigger Event:
push
-
Statement type:
File details
Details for the file superconductor-0.1.1-py3-none-any.whl.
File metadata
- Download URL: superconductor-0.1.1-py3-none-any.whl
- Upload date:
- Size: 236.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9b03781a9ae00fcf515da78f910f3e5514d487656d5caf86544f532ac7c8d05
|
|
| MD5 |
acf392ab5e78c7c148c474178efcd51b
|
|
| BLAKE2b-256 |
1d709e8eaf7cfcc460b12f306a1d34efc5cdb6364236dc07fa0abff6323e2e46
|
Provenance
The following attestation bundles were made for superconductor-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on simonholliday/superconductor
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
superconductor-0.1.1-py3-none-any.whl -
Subject digest:
d9b03781a9ae00fcf515da78f910f3e5514d487656d5caf86544f532ac7c8d05 - Sigstore transparency entry: 2765796567
- Sigstore integration time:
-
Permalink:
simonholliday/superconductor@bb61a66c0e6be1fcbb39f7ddd7459a9a2f2d012f -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/simonholliday
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bb61a66c0e6be1fcbb39f7ddd7459a9a2f2d012f -
Trigger Event:
push
-
Statement type: