DEV Community

Cover image for 📻 I built an infinite 90s boombox with Gemini + Lyria (and it has an AI DJ!)
Paige Bailey for Google AI

Posted on

📻 I built an infinite 90s boombox with Gemini + Lyria (and it has an AI DJ!)

I recently built an experiment that I honestly can’t stop listening to. It’s a virtual, web-based Boombox. You tune the dial, the station changes, and the music crossfades in real-time.

But here is the kicker: The DJ is AI.

Every time you settle on a station, a dynamically generated voice (courtesy of Gemini text-to-speech) chimes in to introduce the track and the genre, totally context-aware. It feels like a ghost in the machine, and it was built using the Google Gen AI SDK, Lit, and the Lyria Real-Time model.

Here is how I built it.

The Tech Stack

  • Vibe-coding Platform: Google AI Studio
  • Framework: Lit (Web Components) + Vite
  • Music Generation: Google's lyria-realtime-exp model
  • DJ Voice & Script: Gemini 2.5 Flash & Gemini TTS (Fenrir voice)
  • Visuals: CSS for the radio, Gemini 2.5 Flash Image for the background.

1. The Infinite Music Stream 🎵

The core of the app is the LiveMusicHelper. It connects to the lyria-realtime-exp model. Unlike generating a static MP3 file, this establishes a session where we can steer the music in real-time by sending "Weighted Prompts."

When you turn the tuning knob on the UI, we aren't downloading a new song; we are telling the AI to shift its attention.

// from utils/LiveMusicHelper.ts

public readonly setWeightedPrompts = throttle(async (prompts: Map<string, Prompt>) => {
  // Convert our UI map to an array for the API
  const weightedPrompts = this.activePrompts.map((p) => {
    return { text: p.text, weight: p.weight };
  });

  try {
    // This is where the magic happens. 
    // We tell the model: "be 100% Bossa Nova" or "mix 50% Dubstep and 50% Jazz"
    await this.session.setWeightedPrompts({
      weightedPrompts,
    });
  } catch (e: any) {
    console.error(e);
  }
}, 200);
Enter fullscreen mode Exit fullscreen mode

The visual knob component maps a rotation angle to an index in a prompt array. If you are on index 0, "Bossa Nova" gets a weight of 1.0.

2. The AI DJ (The "Secret Sauce") 🎙️

This is my favorite part. A radio isn't a radio without a DJ telling you what you're listening to. I created a RadioAnnouncer class to handle this.

It works in a two-step chain:

  1. Generate the Script: We ask Gemini to write a one-sentence intro.
  2. Generate the Audio: We pass that text to the TTS model.

Step 1: The Personality

We prompt Gemini 2.5 Flash to adopt a persona. Note the specific constraints: short, punchy, no quotes.

// from utils/RadioAnnouncer.ts

const scriptResponse = await this.ai.models.generateContent({
  model: 'gemini-2.5-flash',
  contents: `You are a charismatic radio DJ. Write a single, short, punchy sentence to introduce the current song.
  The station frequency is ${freq} FM.
  The music genre is ${station}.
  Do not use quotes. Just the spoken text.
  Example: "You're locked in to 104.5, keeping it smooth with Bossa Nova."`,
});
Enter fullscreen mode Exit fullscreen mode

Step 2: The Voice

We use the Fenrir voice from the prebuilt configurations.

const ttsResponse = await this.ai.models.generateContent({
  model: 'gemini-2.5-flash-preview-tts',
  contents: {
    parts: [{ text: script }]
  },
  config: {
    responseModalities: [Modality.AUDIO],
    speechConfig: {
      voiceConfig: {
        prebuiltVoiceConfig: { voiceName: 'Fenrir' }
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

The Logic: Debouncing

Because the user might scroll past 5 stations quickly to get to "Dubstep," we don't want the DJ to try and announce every single one. I used a debouncer so the generation only triggers once the user stops turning the knob for 800ms.

3. The Aesthetics 🎨

The UI is built using Lit. The boombox itself is a mix of CSS styling and SVG for the speakers and knobs.

The Speaker Pulse:
I used the Web Audio API to create an AudioAnalyser. We grab the current frequency data and map it to a CSS transform: scale() on the speaker cones.

/* from components/PromptDjMidi.ts */
.speaker-cone {
  /* ... textures and gradients ... */
  transition: transform 0.05s cubic-bezier(0.1, 0.7, 1.0, 0.1);
}
Enter fullscreen mode Exit fullscreen mode
// In the render loop
const pulseScale = 1 + (this.audioLevel * 0.15); 
const speakerStyle = styleMap({
    transform: `scale(${pulseScale})`
});
Enter fullscreen mode Exit fullscreen mode

The Background:
To really sell the vibe, I generate a background image on load.

// The prompt used for the background
text: 'A 90s-style girl\'s bedroom, dreamy, nostalgic, vaporwave aesthetic, anime posters on the wall, lava lamp, beaded curtains, photorealistic.'
Enter fullscreen mode Exit fullscreen mode

4. Managing the "Spin" 😵‍💫

One of the hardest parts of this build was the math for the tuning knob. We need to convert mouse/touch movement into rotation, and then snap that rotation to specific "stations."

I implemented a circular capture logic:

  1. Calculate the angle between the center of the knob and the mouse cursor.
  2. Calculate delta (change) from the start of the click.
  3. Handle the 0/360 degree wrap-around logic so you can spin it endlessly.
// from components/PromptDjMidi.ts
private handlePointerMove(e: PointerEvent) {
  // ... math to get angle ...

  // Handle crossing the 0/360 boundary smoothly
  if (delta > 180) delta -= 360;
  if (delta < -180) delta += 360;

  this.rotation = (this.startRotation + delta + 360) % 360;

  // Map rotation to station index
  const index = Math.floor(((this.rotation + segmentSize/2) % 360) / segmentSize);
  this.setStation(index);
}
Enter fullscreen mode Exit fullscreen mode

TL;DR

This project was a blast to build because it combines the tactile feel of old-school interfaces with bleeding-edge generative AI.

The "Ghost DJ" effect really adds a layer of immersion that pure generative music apps usually lack. It gives the AI a voice—literally—and makes the infinite radio feel alive.

You can check out the full code here in AI Studio: https://aistudio.google.com/apps/drive/1L23eufECJSn0KPta3eAVo-PGdM4iXm-1

Let me know if you try building your own stations! I'm currently vibing to 94.0 "Shoegaze." 📻

Top comments (5)

Collapse
 
jjbb profile image
Jason Burkes

This is such a perfect collision of nostalgia and sci‑fi—like if a 90s boombox got possessed by a very friendly ghost who secretly majored in DSP. Love how you treated stations as weighted prompts instead of tracks; the “steerable” infinite stream is genius. And the debounced AI DJ intros? That's the exact amount of chaos control a haunted radio needs.

Collapse
 
tochukwu_dev profile image
Tochukwu Nwosa

Looks great. This is pure AI or you did some customization?

Collapse
 
andretypes profile image
Andre Pereira

Look awesome! Great project.

Collapse
 
timgabrikowski profile image
Tim Gabrikowski

That's a very nice project. Great Idea! Love it!

Collapse
 
jorgegraza profile image
Jorge Luis Graza Melgarejo

This is the best radio station!