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 (2)

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
 
andretypes profile image
Andre Pereira

Look awesome! Great project.