TuneCamp Lab β
The Lab is TuneCamp's experimental zone β a place where developers can ship standalone audio tools that run embedded inside TuneCamp without touching the core codebase.
Each Lab app:
- Runs in a sandboxed iFrame (no risk to the host app)
- Can request browser permissions (microphone, etc.) scoped to itself
- Is described by a single object in the app registry
- Can optionally communicate with TuneCamp via the Lab SDK (PostMessage bridge)
Quick start: adding a Lab app β
All apps live in one file:
webapp/src/data/labApps.tsAdd an entry to the LAB_APPS array. That's it β the Lab page and runner pick it up automatically.
Manifest fields β
interface LabApp {
id: string; // URL slug β /lab/<id>
name: string;
description: string;
src: string; // URL of the app (hosted externally or relative path)
category: 'recording' | 'synthesis' | 'composition' | 'effects' | 'other';
author: string;
sourceUrl: string; // GitHub / repo link shown in the toolbar
permissions: string[]; // Shown as badges on the card (informational)
sandbox: string[]; // iFrame sandbox attribute tokens
allow: string[]; // iFrame allow attribute (feature policy)
}Example β 4-Track Recorder β
// webapp/src/data/labApps.ts
{
id: '4track',
name: '4-Track Recorder',
description:
'Browser-based 4-track audio recorder with overdub support, ' +
'latency compensation, and sample-accurate multi-track playback. ' +
'Runs entirely in your browser β no server needed.',
src: 'https://www.4track.cc',
category: 'recording',
author: 'andreboekhorst',
sourceUrl: 'https://github.com/andreboekhorst/4-track-recorder',
permissions: ['microphone'],
sandbox: ['allow-scripts', 'allow-same-origin', 'allow-downloads', 'allow-forms'],
allow: ['microphone'],
}Common sandbox values β
| Token | When to use |
|---|---|
allow-scripts | App runs JavaScript (always needed) |
allow-same-origin | App uses localStorage, IndexedDB, cookies |
allow-downloads | App lets users save files |
allow-forms | App has <form> submissions |
allow-popups | App opens new windows/tabs |
allow-modals | App uses alert / confirm |
Common allow (feature policy) values β
| Value | When to use |
|---|---|
microphone | App records from the user's microphone |
camera | App accesses the camera |
midi | App uses Web MIDI API |
autoplay | App autoplays audio |
Hosting options β
A β External URL (recommended for experiments) β
Point src at a live demo URL. Fastest way to ship.
src: 'https://www.4track.cc'Caveat: some sites set
X-Frame-Options: DENYorContent-Security-Policy: frame-ancestors 'none', which blocks embedding. If the app refuses to load in the iFrame, use option B.
B β Fork + self-host β
- Fork the repo
- Build it:
npm run build - Host the
dist/folder on any static hosting (Vercel, Netlify, GitHub Pages, your own server) - Point
srcat your hosted URL
C β Bundle inside TuneCamp β
Place the built dist/ folder under webapp/public/lab/<id>/ and set:
src: '/lab/4track/index.html'The app will be served by TuneCamp's own static file server. Good for offline / self-hosted instances.
Lab SDK (PostMessage bridge) β
Apps can optionally communicate with TuneCamp using window.postMessage. This lets a Lab app read the user's library, react to playback events, or save audio back to TuneCamp.
All four actions are handled by TuneCamp out of the box β no changes to the host app are needed when adding a new Lab app.
Protocol β
Every request follows the same pattern:
// 1. Send the request to TuneCamp
window.parent.postMessage(
{ type: 'tunecamp:request', action: '<action>', payload: { /* optional */ } },
'*'
);
// 2. Listen for the response
window.addEventListener('message', (event) => {
if (event.data?.type !== 'tunecamp:response') return;
if (event.data?.action !== '<action>') return; // match by action name
console.log(event.data.payload);
});The response envelope is always { type: 'tunecamp:response', action, payload }.
Available actions β
getUser β
Returns the currently logged-in user, or null if not authenticated.
window.parent.postMessage(
{ type: 'tunecamp:request', action: 'getUser' },
'*'
);
// Response payload
// { id: string, username: string, role: 'admin' | 'user' | 'super_user' | null }
// or nullgetLibrary β
Returns a slice of the user's track library.
window.parent.postMessage(
{ type: 'tunecamp:request', action: 'getLibrary', payload: { limit: 50 } },
'*'
);
// Response payload
// {
// tracks: [
// {
// id: string,
// title: string,
// artist: string,
// album: string,
// duration: number, // seconds
// streamUrl: string, // authenticated stream URL
// coverUrl: string, // cover art URL
// },
// ...
// ]
// }limit defaults to 50 if omitted.
getNowPlaying β
Returns the track currently playing in TuneCamp, or null if nothing is playing.
window.parent.postMessage(
{ type: 'tunecamp:request', action: 'getNowPlaying' },
'*'
);
// Response payload (track playing)
// {
// track: {
// id: string,
// title: string,
// artist: string,
// album: string,
// duration: number,
// streamUrl: string,
// coverUrl: string,
// },
// isPlaying: boolean,
// currentTime: number, // seconds elapsed
// duration: number, // total duration in seconds
// }
// or nullexportAudio β
Saves an audio Blob directly into the user's TuneCamp library.
const blob = await myApp.exportMix(); // your app produces a Blob
window.parent.postMessage(
{
type: 'tunecamp:request',
action: 'exportAudio',
payload: {
blob,
filename: 'my-recording.wav',
mimeType: 'audio/wav',
},
},
'*'
);
// Response payload
// { success: true }
// or { success: false, error: string }Complete helper (copy-paste) β
Drop this into any Lab app to get a typed, promise-based SDK with no dependencies:
function tunecampSDK() {
function request(action, payload) {
return new Promise((resolve) => {
function onMessage(event) {
if (event.data?.type !== 'tunecamp:response') return;
if (event.data?.action !== action) return;
window.removeEventListener('message', onMessage);
resolve(event.data.payload);
}
window.addEventListener('message', onMessage);
try {
window.parent.postMessage({ type: 'tunecamp:request', action, payload }, '*');
} catch {
// Not inside a TuneCamp iFrame
window.removeEventListener('message', onMessage);
resolve(null);
}
});
}
return {
getUser: () => request('getUser'),
getNowPlaying: () => request('getNowPlaying'),
getLibrary: (limit) => request('getLibrary', { limit: limit ?? 50 }),
exportAudio: (blob, filename, mimeType) =>
request('exportAudio', { blob, filename, mimeType }),
};
}
// Usage
const tc = tunecampSDK();
const user = await tc.getUser(); // { id, username, role } | null
const library = await tc.getLibrary(20); // { tracks: [...] }
const now = await tc.getNowPlaying(); // { track, isPlaying, ... } | nullWorked example: integrating 4-Track Recorder β
What it does β
4-Track Recorder is a SvelteKit app that uses the Web Audio API to record up to 4 audio tracks in the browser, with overdub and latency compensation. It saves projects in a custom .4trk binary format.
Tech stack β
- Frontend: SvelteKit + TypeScript
- Build: Vite
- Audio: Web Audio API (no backend)
- Storage: local download (
.4trkfiles)
Why iFrame, not a React component β
The app is built with Svelte, not React. Wrapping it as a React component would require re-writing it. The iFrame approach lets it run as-is, in its own JS context, without any framework conflicts.
Embedding it in TuneCamp β
The manifest entry above is all that's needed for the basic integration. The result:
/labshows a card with the app name, category badge, and permission hints/lab/4trackopens the app full-screen inside TuneCamp's shell with a back button and a link to the source repo- The
allow="microphone"attribute forwards the browser's microphone permission prompt into the iFrame
Optional: deeper integration via Lab SDK (future) β
Once the PostMessage bridge is implemented, a fork of 4-Track Recorder could:
// After finishing a recording, offer to save it to the TuneCamp library
const blob = await exportMix(); // get the final mix as a Blob
window.parent.postMessage({
type: 'tunecamp:request',
action: 'exportAudio',
payload: {
blob,
filename: 'my-recording.wav',
mimeType: 'audio/wav',
},
}, '*');This would save the mix directly into the user's TuneCamp library without leaving the app.
Submitting a Lab app β
- Fork
tunecamp/tunecamp - Add your entry to
webapp/src/data/labApps.ts - Open a PR with the title
feat(lab): add <Your App Name> - In the PR description include:
- What the app does
- Its source repo or live URL
- Which browser permissions it needs and why
- A screenshot or short video
Differences from Plugins β
| Lab Apps | Backend Plugins | |
|---|---|---|
| What they extend | Frontend UI (audio tools, instruments) | Backend providers (metadata, streaming, storageβ¦) |
| Tech stack | Any (iFrame-based) | Node.js / ESM |
| Where they live | webapp/src/data/labApps.ts | plugins/<name>.js |
| Loaded by | React frontend at runtime | Server plugin loader at startup |
| Examples | 4-Track Recorder, Patchcab, ComposeYogi | Custom metadata source, Soulseek, S3 storage |
See PLUGINS.md for backend plugin documentation.