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.

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.

Download this starter

<!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.tgz

SHA-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.

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.