Provable-Games/onchain-midi-player
Provable-Games/onchain-midi-player · 1 stars · 1 plugin
Marketplace
Install
/plugin marketplace add Provable-Games/onchain-midi-playerRun inside Claude Code, then install plugins from it. Check the repo's README before you run it.
Plugins 1
After adding the marketplace, install one with /plugin install <name>@onchain-midi-player.
- 1onchain-midi-playerIntegrate the onchain MIDI player into an NFT's token_uri, write MIDI and TinySynthSettings for it, and inspect deployed tokens.
/plugin install onchain-midi-player@onchain-midi-player
Files
onchain-midi-player
A Cairo class library for Starknet that gives an NFT's token_uri an onchain MIDI player. The player is an HTML page in the token's animation_url. The page holds the synth engine, the player, the token's MIDI, its sound settings and its SVG art, and it plays offline in the browser with no network requests. Your NFT contract supplies the MIDI, settings and art, and calls the class with library_call to get the pieces of its token_uri. The class is declared on Starknet, and your contract calls it by class hash, never through a deployed instance. It has no storage, no constructor and no collection-specific logic. The engine is currently TinySynth, a General MIDI synthesizer (oscillators and FM, no samples).
How it works
Your contract holds the class hash and calls the class through ITinySynthLibraryDispatcher. Every function is a deterministic view, and the class touches none of your storage.
token_uri is nested base64 data URIs. The JSON is data:application/json;base64,…, and its animation_url is data:text/html;base64,…, the player page. The engine and player are the same for every token, so the class stores them already encoded and never encodes them at call time. Your contract encodes only per-token data, in pieces that are each a multiple of 3 bytes long, and joins them. That works because b64(X ++ Y) == b64(X) ++ b64(Y) when len(X) % 3 == 0.
"data:application/json;base64,"
++ b64('{' members ',' <pad> '"image":"data:image/svg+xml;base64,')
++ b64(S) S = b64(svg) '"' <pad>, encoded once, used twice
++ b64(',' <pad>)
++ animation_url_segment() ← class: the fixed page (engine and player), pre-encoded
++ midi_segment(midi, settings) ← class: this token's settings and MIDI
++ b64(S) the art the page shows; closes both data URIs
++ b64('}')
<pad> is spaces between JSON tokens. They keep each piece a multiple of 3 bytes.
In the browser, the page shows the art at once. ▶ starts the MIDI, which loops at End-of-Track, and the art restarts in step with every pass. If the engine, the settings or the MIDI fails, ▶ stays disabled, the page shows the error, and the art stays. The page works in a sandboxed iframe and under a strict CSP.
Details: token_uri layout and the player page.
Quick start for integrators
1. Depend on the crate. Use the Scarb and Starknet Foundry versions in .tool-versions.
[dependencies]
onchain_midi_player = { git = "https://github.com/Provable-Games/onchain-midi-player", tag = "v<version>" }
# A class without a release tag: rev = "<commit>", its `built_from` commit
[[target.starknet-contract]]
sierra = true
# Builds the class from the dependency so your tests can declare it.
build-external-contracts = ["onchain_midi_player::contract::TinySynth"]
2. Hold a class hash. Store it in your NFT, or in a small renderer contract the NFT calls, and let the owner change it. Take it from the deployments/<network>.json of the network your NFT lives on (class.class_hash). The same build has the same class hash on every network. A class hash fixes the engine and the player, so tokens keep their sound until you switch. When you store or change it, check that engine() returns 'tinysynth': another engine's class shares the midi_segment selector and could render the wrong thing without an error.
version()is the class's SemVer version. Every declared class has its own version, and a declared class never changes. From1.0.0, the major number changes only when the call orTinySynthSettingslayout does, so a later minor or patch class keeps your calls and settings working (it may change the engine, the page or the sound).- A release has the tag
v<version>(release_tagin the deployment file), and its class is built from that tag. A class without one is a test class: do not store its hash for production tokens. - The ABIs are in
abi/: the class's, andISoundProvider's for providers.
use onchain_midi_player::interface::{
ITinySynthDispatcherTrait, ITinySynthLibraryDispatcher,
};
let synth = ITinySynthLibraryDispatcher { class_hash: tinysynth_class_hash };
The interface, in src/interface.cairo:
| Function | Returns |
|---|---|
animation_url_segment() |
The "animation_url":"data:text/html;base64,<page> JSON member, pre-encoded at both base64 layers. |
midi_segment(midi, settings) |
This token's settings and MIDI, encoded at both layers. Reverts with 'TS: …' on invalid settings. |
base64(data) |
Standard RFC 4648 base64, for your own JSON pieces. |
script_sha256() |
SHA-256 of the engine script. |
engine() |
The engine's name: 'tinysynth'. |
version() |
The class's SemVer version, such as '1.2.3'. |
license() |
The license notices for the class and the code it embeds. |
3. Assemble token_uri in the layout above. Building token_uri in Cairo has the full function. The rules:
- Every piece you pass to
base64, except the final'}', must be a multiple of 3 bytes. Pad with spaces between JSON tokens. - The SVG must never contain
</script, in any letter case. See Art (SVG) requirements. - Optional: add spaces, 3 at a time, so your two largest appends start on a 31-byte
ByteArrayword. That makes them about 4x cheaper. midiandsettingscome from constants in your contract, or from a composer's sound provider.
4. Test it. In snforge, declare the class (never deploy it) and library-call it from your NFT. Then:
- Decode your
token_uriand compare it with a reference, as the example's golden tests do. - Check
engine(),version()andscript_sha256()against your class's record inscripts/page_versions.json, which holds the current version; for an earlier one, usegit log -p scripts/page_versions.json. - Confirm the class hash is declared on the network you will use (
starknet_getClass). - Test that your rendered SVGs never contain
</script. - Run
check-midion every score in CI (see below). - Budget your largest token's gas (see How much fits).
examples/beast_consumer is a complete, tested NFT that does all of this. AI agents can use the integrator-guide skill. examples/stress_nft is a test NFT of twenty tokens whose token_uri costs from 30M to over 10B gas, for finding where RPC providers, wallets and explorers stop serving a large token.
Quick start for composers
You need Node 22 or later and a clone of this repository. No npm install is needed. Use a clone whose PAGE matches the class you target: grep 'pub const VERSION' src/page_data.cairo must print the class's version().
Preview the page a token would get, byte for byte, offline:
npm run preview -- song.mid # default settings, placeholder art npm run preview -- song.mid --settings sound.json --svg art.svg # your settings and art npm run preview -- song.mid --serve # also serve it on http://127.0.0.1:8000/
It checks the MIDI, the settings and the art first, and writes nothing if any check fails. Details: Previewing a score.
Check MIDI files against the page's own MIDI check. It exits 0 when every score passes, 1 when any fails, and 2 on a usage error, so it can gate CI:
npm run check-midi -- song.mid other.mid
The MIDI contract, in short. The class embeds the MIDI without parsing it, so a bad file does not revert: the page shows an error instead. The full rules are in the MIDI contract.
- Standard MIDI File, format 0 or 1, with ticks-per-quarter-note timing.
- End-of-Track is the last event of every track, and nothing follows the last track.
- The song always loops. A pass ends at the latest End-of-Track and lasts at least 50 ms.
- Only the tempo resets between passes. Set tempo, programs and controllers at tick 0 so every pass starts the same way.
- Channel 10 is drums: notes 35–81, and note-offs are ignored. The other channels play General MIDI programs 0–127.
- The art restarts at every pass. Make the pass a whole multiple of the art's animation periods, or the art jumps at the loop point.
- Every byte costs gas: about 14M L2 gas per KB.
Sound settings. Each midi_segment call takes a TinySynthSettings value, declared in src/types.cairo:
| Field | Meaning | Limits |
|---|---|---|
quality |
The engine's built-in sounds: 0 chip-tune, 1 FM | 0 or 1 |
reverb |
Reverb level in percent, 0 is off | any u8 |
master_vol |
Master volume in percent; above 100 can clip | any u8 |
voices |
Simultaneous melodic notes. A drum hit takes no voice but applies the limit, so leave headroom (with 1, every hit cuts the melody). A note beyond it cuts a released note first, then the earliest held one (rules) | at least 1 |
timbres |
Custom sounds. Each replaces a General MIDI program (0–127) or a drum note (35–81), so the MIDI selects it as usual | up to 175, each slot at most once; 1 to 8 operators each |
waves |
Custom waveforms (sample tables or harmonics) that timbres can use. Operators can also carry a fixed low-, high- or band-pass filter | up to 256 |
The defaults are quality 1, reverb 30, master_vol 40, voices 64 and no custom sounds.
Fractional values are fixed point, in units of 1/10,000. Invalid settings revert with a 'TS: …' message and the offending indices. The reference is Sound settings.
Serving sound from a contract. A composer's contract can serve each token's MIDI and settings in one call, through ISoundProvider in src/interface.cairo. The NFT calls it and passes the result to midi_segment.
#[starknet::interface]
pub trait ISoundProvider<T> {
fn get_sound(self: @T, token_id: u256) -> TinySynthSound; // { midi: ByteArray, settings: TinySynthSettings }
}
get_sound takes the token ID as minted, returns a raw MIDI file and valid settings, and is deterministic and view-only. Each engine has its own typed settings struct, with no enum, and the layouts of TinySynthSettings and TinySynthSound are fixed at release. A later engine's providers will implement get_<engine>_sound; get_sound belongs to the TinySynth engine. The rules, and a safe way to call a provider, are in Sound provider interface.
AI agents can use the midi-guide and sound-design skills.
How much fits (gas and limits)
A marketplace or indexer reads token_uri with a view call (starknet_call). The RPC node that serves the call caps its gas, so your largest token must fit under the caps of the nodes your users rely on. Estimate a token in L2 gas (Sierra gas, as snforge reports it by default):
Part of token_uri |
L2 gas |
|---|---|
The fixed page: animation_url_segment() and appending it, word-aligned |
about 10M |
| Art: per KB of SVG (it is encoded twice) | about 9M |
| MIDI: per KB | about 14M |
SETTINGS: per KB |
about 14.5M |
| Your contract's own work: rendering the SVG, the JSON members | yours to measure |
SETTINGS is the text form of your TinySynthSettings. The defaults are 16 bytes, and three typical custom sounds about 0.3 KB. Each operator adds about 50 bytes, and each wave sample or harmonic 2 to 6 bytes. SETTINGS has no size cap: gas and the node's cap are the only limits.
Example. A full-size token from examples/beast_consumer costs 288.6M L2 gas: a 22,733-byte animated SVG, a 3,716-byte MIDI file and 334 bytes of SETTINGS. A token with a 1 KB SVG and a 112-byte MIDI file costs about 33M.
Node caps on a view call's gas:
| Node | Cap |
|---|---|
| Pathfinder | 10B |
| Madara | 10B |
| Katana (development) | 1B by default |
| Juno | 100M by default (--rpc-call-max-gas) |
| Hosted providers | Undocumented. Each configures its own cap, so a full-size token can revert Out of gas on one and succeed on another |
- Check a full-size token through the providers your marketplaces and indexers use, on the network you deploy to. A provider's cap on one network says nothing about another.
- At 10B, a token has room for about 670 KB of
SETTINGS. - Nodes built on jsonrpsee cap responses at 10 MiB, a
token_uriof about 4.85 MB. - If another contract calls
token_uriinside a transaction, the transaction cap of 1.11B L2 gas applies. - Keep every class in the call chain at Sierra 1.7 or later. An older class switches its frame to Cairo-steps accounting, capped at 10M steps (Juno: 4M).
Measurements, per-function costs and the sources of the caps: Gas and limits.
Verifying the engine
Every token's animation_url carries the engine, so anyone can check it offline:
- Save the collection's
token_uri(from its contract, an explorer or a marketplace) totoken_uri.txt. - Run
node scripts/verify_engine.mjs token_uri.txt --expect <script_sha256>. It needs Node 22 or later and nothing else. It prints the SHA-256 of the engine, of its gzip payload and of the fixed page. - Compare them with the record for the class's
version()inscripts/page_versions.json(script_sha256,gzip_sha256,page_sha256). For an earlier version, find its record withgit log -p scripts/page_versions.json. - When
engine_refin that record is a fork release tag, the release'sSHA256SUMSlists the same SHA-256 forwebaudio-tinysynth.min.js. - Optionally, rebuild the engine from the fork ref in that record (
engine_commit), and the page and class from this repository's tagv<version>: the class hash must equalclass.class_hash.
To check a whole token_uri (OpenSea metadata fields and attributes, the SVG image, the page, the MIDI and the sizes), run npm ci once and then npm run validate-token-uri -- token_uri.txt (it needs the pinned SVG parser, unlike the steps above): Validating a token_uri.
Each deployment file also lists an inspection instance: a deployment of the class with no constructor and no state, so an explorer or an RPC call can read version(), engine(), script_sha256() and license(). Your contract never calls it.
Shell and Python versions of step 2, and the rebuild steps: Verifying the engine.
Agent skills
Four skills help AI agents in other projects use the player. They live in plugins/onchain-midi-player/skills/ and link to these docs.
| Skill | For |
|---|---|
integrator-guide |
Adding the player to a contract's token_uri, testing it and budgeting its gas |
midi-guide |
Writing MIDI for the player, previewing it and keeping it in step with the art |
sound-design |
Designing TinySynthSettings: custom timbres, waves and filters |
token-uri-inspector |
Decoding, verifying and viewing a deployed or local token_uri |
Install in Claude Code. This repository is a plugin marketplace (.claude-plugin/marketplace.json). In your project:
claude plugin marketplace add Provable-Games/onchain-midi-player # or Provable-Games/onchain-midi-player#<tag> to pin a ref claude plugin install onchain-midi-player@onchain-midi-player --scope project
The skills then run as /onchain-midi-player:midi-guide and so on. claude plugin update onchain-midi-player@onchain-midi-player brings the latest. To offer them to everyone who opens your project, commit this to its .claude/settings.json:
{
"extraKnownMarketplaces": {
"onchain-midi-player": { "source": { "source": "github", "repo": "Provable-Games/onchain-midi-player" } }
},
"enabledPlugins": { "onchain-midi-player@onchain-midi-player": true }
}
Install in Codex. Ask the built-in $skill-installer to install all four skills. Send this prompt in Codex:
$skill-installer Install these skills from Provable-Games/onchain-midi-player:
- plugins/onchain-midi-player/skills/integrator-guide
- plugins/onchain-midi-player/skills/midi-guide
- plugins/onchain-midi-player/skills/sound-design
- plugins/onchain-midi-player/skills/token-uri-inspector
For a pinned version, add Use ref <tag-or-commit> for all four skills to the prompt. The installer adds them to your user skills directory, available across projects. Open /skills to check they appear; restart Codex if needed. Invoke them as $integrator-guide, $midi-guide, $sound-design or $token-uri-inspector.
For a project-local installation, run this from your project's root, using the path to your clone of this repository:
mkdir -p .agents/skills cp -R /path/to/onchain-midi-player/plugins/onchain-midi-player/skills/. .agents/skills/
Copy all four folders, including their scripts and references, to preserve links between skills. Commit .agents/skills/ to share them with your team. To update a project-local installation, repeat the copy from an updated clone. For an installer-managed update, remove the installed skill folders before asking $skill-installer to install them again from the desired ref.
Other agents. Each SKILL.md follows the open Agent Skills format. Copy the whole skills/ folder into your agent's skills directory to keep the links between skills.
The tools the skills use need Node 22 or later and a clone whose VERSION matches your class (see Quick start for composers). Every commit with the same VERSION has the same PAGE, so use main while its VERSION matches, otherwise the last commit before it changed (git log --oneline -- src/page_data.cairo). A released class's tag, v<version>, works too.
Documentation
token_urilayout and the player page- MIDI contract
- Sound settings
- Sound provider interface
- Gas and limits
- Verifying the engine
deployments/: what is declared and deployed on each network, one JSON file per networkabi/: the ABIs of the class and ofISoundProviderscripts/page_versions.json: the currentversion()'s engine commit, page revision and hashes (earlier versions are in its git history)- Development: toolchain, build, tests and CI, for contributors
License
Licensed under the Apache License, Version 2.0. See LICENSE and NOTICE. The embedded TinySynth engine is also Apache-2.0: copyright Tatsuya Shinyagaito (g200kg), modified by Provable Games in https://github.com/Provable-Games/webaudio-tinysynth. The page's gunzip shim is derived from fflate, MIT License, Copyright (c) 2026 Arjun Barrett (text). The class's base64 encoder is the game_components_encoding package of game-components, MIT License, Copyright (c) 2026 Provable Games (text). license() includes all four notices.
The Beast and MIDI test fixtures are Apache-2.0 as well. The Beast SVG in tests/fixtures/beasts/ is Beasts artwork that Provable Games licenses under Apache-2.0 for this repository, and the MIDI scores in tests/fixtures/midi/ are synthetic, generated by scripts/gen_midi_fixtures.mjs. Neither is part of the class.
{
"name": "onchain-midi-player",
"description": "Agent skills for integrating the onchain MIDI player for NFTs and driving it with MIDI.",
"owner": {
"name": "Provable Games",
"url": "https://github.com/Provable-Games"
},
"plugins": [
{
"name": "onchain-midi-player",
"source": "./plugins/onchain-midi-player",
"description": "Integrate the onchain MIDI player into an NFT's token_uri, write MIDI and TinySynthSettings for it, and inspect deployed tokens.",
"category": "development"
}
]
}Facts
- Kind
- Marketplace
- Repo
- Provable-Games/onchain-midi-player
- Group
- Uncategorized
- Stars
- 1
- Marketplace name
- onchain-midi-player
- Owner
- Provable Games
- License
- Apache-2.0
- Language
- JavaScript
- Created
- 2026-10-02
- Last push
- 2026-10-08
- Forks
- 1
- Plugins
- 1
- 1obra/superpowersobra/superpowersAn agentic skills framework & software development methodology that works.297.9k
- 2affaan-m/everything-claude-codeaffaan-m/ECCThe agent harness performance optimization system. Skills, instincts, memory, security, and research-first development for Claude Code, Codex, Opencode, Cursor and beyond.277.5k
- 3anthropics/skillsanthropics/skillsPublic repository for Agent Skills180.6k
- 4f/prompts.chatf/prompts.chatf.k.a. Awesome ChatGPT Prompts. Share, discover, and collect prompts from the community. Free and open source — self-host for your organization with complete privacy.172.7k
- 5anthropics/claude-codeanthropics/claude-codeClaude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.150.3k
- 6nextlevelbuilder/ui-ux-pro-max-skillnextlevelbuilder/ui-ux-pro-max-skillAn AI skill that provides design intelligence for building professional UI/UX across multiple platforms.135k