Music for your game.
Your game stays in control.
Give your browser game a soundtrack from LoftwahFM’s public stations. Try the small demo, then hand your coding agent a working starter.
No account or key. No sound until you press Play. This is a streaming proof of concept, not a grant to bundle or redistribute the audio.
Hear it before you build.
Move the marker and trigger a game sound. Play, pause or mute the FM music separately. The local controls keep working when the music cannot load.
The Woodhouse copy runs on a separate origin. It is a demonstration of the integration, not an independently adopted game. Source: LoftwahFM’s public demo, pinned to SDK 1.2.0; reviewed 18 October 2026.
- Cartridge ClubChiptune swing and instrumental hip hop for another round.
- Pressure SystemHeavy drums and arcade rhythm.
- Low TideUnhurried instrumentals for exploration and open water.
- Soft FocusGentle grooves for a quieter session.
- Night SignalElectronic pressure and low-slung grooves.
- Saltwater StaticOffshore beats and strange transmissions.
These links open the listener without starting sound. Read the station catalogue or browse the public audio catalogue. Change the station ID in both places in the starter to use another station.
Start with one HTML file.
Save this as an HTML file and serve it from your game’s local web server. It loads the pinned browser SDK directly. The downloadable demo adds local movement, sound effects and music retry.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex,nofollow">
<title>My game music</title>
<button id="play" disabled>Play music</button>
<button id="pause" disabled>Pause music</button>
<button id="mute" disabled>Mute music</button>
<p id="status" role="status">Loading music…</p>
<script type="module">
const play = document.querySelector('#play');
const pause = document.querySelector('#pause');
const mute = document.querySelector('#mute');
const status = document.querySelector('#status');
try {
const { createRadioClient, createMediaElementTransport } = await import(
'https://fm.loftwah.com/radio/v1/client/1.2.0/index.js'
);
const audio = new Audio();
audio.crossOrigin = 'anonymous';
const radio = createRadioClient({
transport: createMediaElementTransport(audio)
});
radio.preferences({ enabled: false, muted: false, speech: false, explicit: false });
radio.subscribe(snapshot => {
status.textContent = snapshot.item
? snapshot.phase + ' — ' + snapshot.item.title
: snapshot.phase === 'unavailable'
? 'Music unavailable. Your game can keep going.'
: 'Loading music…';
play.disabled = !snapshot.item || snapshot.phase === 'playing';
pause.disabled = snapshot.phase !== 'playing';
mute.disabled = !snapshot.item;
mute.textContent = snapshot.preferences.muted ? 'Unmute music' : 'Mute music';
});
// Fetch before Play. Call play() directly from the player's gesture.
play.onclick = () => { void radio.play(); };
pause.onclick = () => radio.pause();
mute.onclick = () => radio.preferences({ muted: !radio.snapshot().preferences.muted });
document.addEventListener('visibilitychange', () => {
if (document.hidden) radio.suspend();
});
window.addEventListener('pagehide', () => radio.suspend());
await radio.selectStation(
'https://fm.loftwah.com/api/radio/v1/stations/night-signal/current',
'night-signal'
);
} catch {
status.textContent = 'Music unavailable. Your game can keep going.';
}
</script>
</html>
For a bundled game, install the versioned URL package with your usual package manager and keep the lockfile:
npm install https://fm.loftwah.com/radio/v1/client/1.2.0/loftwahfm-radio-client-1.2.0.tgzSHA-256 integrity manifest · SDK type reference. This is a URL-installed package; it is not published to the npm registry.
Keep your own audio controls.
- One music owner. Extend your existing audio manager. Keep effects, dialogue, local music and saves independent. Don’t run two music beds together.
- Off means off. The starter begins off, with speech and explicit content disabled. In an existing game, load its saved off/mute preferences before any audio unlock. Pause switches music off; hidden/pagehide suspends it, and returning stays quiet.
- Respect the browser. Select a station before Play; invoke Play directly from a gesture. For reliable iOS gain and ducking, route the media element through the game’s existing Web Audio music bus. Element volume alone is not a qualified iOS ducking path.
- Clean up with the game. Unsubscribe and call
radio.dispose()when its audio owner is destroyed. React owns one client at app level; Phaser owns it in the AudioManager, not each scene. For back/forward navigation, suspend rather than destroying a retained owner. - Let unavailable music be quiet. Keep the game running. Offer explicit retry or the game’s existing local/off option. Test SDK, manifest and media failures separately.
Public streams support anonymous CORS. Fetch without credentials; set crossOrigin="anonymous" before a media source. If your game has a Content Security Policy, admithttps://fm.loftwah.com for connections and media, plus scripts for the direct-import example and images only if showing artwork. Bundling the SDK needs no added script origin.
Physical iPhone interruption and audible-output checks still belong to your game’s release testing. This starter does not promise offline music, synchronised broadcasting, seamless loops or an audio licence.
Give this to your coding agent.
It names the useful defaults and the checks your game still owns.
Add LoftwahFM streaming music to this browser game using the existing audio owner.
Start with https://fm.loftwah.com/game-music/ and the downloadable demo there.
Pin @loftwahfm/radio-client 1.2.0 from the published URL package and verify integrity.json.
Keep gameplay, sound effects, saved settings and local/offline music independent of FM.
Default to music off, speech:false and explicit:false. Honour existing off/mute before unlocking audio.
Prepare the station before Play; call play() synchronously from a user gesture.
Use one music transport. Preserve the host's music/master gain; use its Web Audio graph for iOS ducking.
Suspend on hidden/pagehide. Returning to the page must stay quiet until another gesture.
Show the current track, truthful state, a LoftwahFM listener link and a quiet unavailable/retry choice.
For React, own one client in the app audio lifecycle and dispose/unsubscribe on teardown.
For Phaser, use its existing AudioManager and context, not a client per scene.
Test SDK, manifest and media failure separately; movement/SFX must survive all three.
Verify real cross-origin playback, mute/off, pause, hidden return, cleanup and the game's device matrix.
This is public-catalogue streaming; don't bundle audio or claim a music licence or independent adoption.For a website or showcase with its own visible player, use an embeddable widget. An actual game should own the SDK transport so its mute, effects and lifecycle stay together.