Welcome to LofiRadio, an automated, global time-synchronized 24/7 Lofi radio station. The platform is built using .NET 10 Clean Architecture (Blazor Server) on the Frontend/API, and an automated modular music generator in Python 3.11 powered by Google DeepMind Vertex AI Lyria 3 Pro and Gemini 3.1 artificial intelligence.
The entire web hosting and database infrastructure is designed under a $0.00 USD Cost Serverless paradigm (leveraging Google Cloud free tiers), while GenAI API generation costs are kept to an optimized minimum (see the Financial Section for full details).
The system operates in a completely decoupled and asynchronous manner. There are no heavy audio stream processes consuming CPU 24/7 in the cloud; instead, a live synchronized radio broadcast is simulated through real-time UTC timeline calculations.
We intentionally chose a Timeline-Synchronized Pseudo-Streaming Architecture (simulating a live broadcast on the client-side via UTC calculations and HTTP Range requests from GCS) over a traditional continuous streaming server (such as Icecast, SHOUTcast, or HLS segments):
- The Traditional Approach (Real Streaming): Requires a dedicated VM or container running 24/7, continuously decoding, mixing, and transcoding audio tracks in memory. This represents a constant, high CPU consumption and a fixed monthly billing of $15 โ $30+ USD (even with 0 active listeners), introducing server-side bottlenecks under heavy traffic spikes.
- Our Timeline-Synchronized Approach: By completely decoupling the audio delivery and letting Google Cloud Storage (GCS) serve the static
.mp3files directly to listeners' browsers, we achieve infinite, global scalability at $0.00 USD hosting costs (staying well within standard GCS Free Tier bandwidth and operation thresholds). C# only runs lightweight UTC clock calculations in milliseconds to tell the browser the exact playhead offset, completely offloading the heavy audio decoding/decryption and bandwidth tasks to the client and Google's high-speed CDN. - Conclusion: This design choice is a deliberate engineering trade-off. We exchange minor client-side clock drifts (fully compensated in C#) for decade-scale serverless robustness and absolute cost-effectiveness, keeping the entire 24/7 radio web hosting and database infrastructure operating entirely within Google Cloud's free-tier threshold.
graph TD
%% Cloud Infrastructure
subgraph GCP ["Google Cloud Platform (Serverless)"]
Scheduler["โฐ Cloud Scheduler (0 6 * * 1-5 UTC)"]
Worker["๐ Cloud Run Job (Python Worker)"]
GCS["๐ชฃ Cloud Storage Bucket (MP3s private)"]
Firestore["๐ฅ Cloud Firestore NoSQL"]
WebApp["๐ป Cloud Run Service (Blazor Web App)"]
end
%% Client Entry Points
subgraph Clients ["Authorized Listeners"]
UserA["๐จ๐ฑ Listener Chile (Browser)"]
UserB["๐ฏ๐ต Listener Japan (Browser)"]
end
IAP["๐ Identity-Aware Proxy (Google Login Gate)"]
%% Worker Flows (Daily Job)
Scheduler -->|Triggers once daily| Worker
Worker -->|AI Audio Generation| VertexAI["๐ง Vertex AI (Lyria 3 Pro)"]
Worker -->|AI Image Generation| Gemini["๐ง Gemini 3.1 (gemini-3.1-flash-image)"]
VertexAI -->|MP3 Audio| Worker
Gemini -->|PNG Image| Worker
Worker -->|In-Memory WebP Compression| Worker
Worker -->|Uploads fresh MP3s & WebPs with Metadata| GCS
Worker -->|Wipes & Saves Contiguous Playlist Sequence| Firestore
GCS -->|Lifecycle Rule: Autocleans files older than 24h| GCS
%% Web App Flows
UserA -->|Visits Web App| IAP
UserB -->|Visits Web App| IAP
IAP -->|Authenticated & Authorized only| WebApp
WebApp -->|Queries State in Transaction| Firestore
WebApp -->|Generates Secure Signed URL| WebApp
WebApp -->|HTTP 302 Redirect| UserA
WebApp -->|HTTP 302 Redirect| UserB
UserA -->|Direct Streaming - HTTP Range| GCS
UserB -->|Direct Streaming - HTTP Range| GCS
To completely eliminate synchronization anomalies and erratic track-jumping caused by micro-second clock drift between browsers and the server, the .NET backend implements a Transactional Firestore Catch-Up Loop:
- Initial Request: A user visits the website, and C# starts an asynchronous transaction in Firestore.
- Active Track Detection:
- If a document exists in Firestore with
status == "playing"and its play start time (play_start_time) + physical duration is greater than the current server time (nowUTC), that track is considered actively broadcasting. - C# calculates the exact playhead offset:
OffsetSeconds = now - play_start_time. - Blazor instantly tunes the global listener into that exact second of the song.
- If a document exists in Firestore with
- Silent Catch-Up:
- If there is no active track (e.g., the radio was unattended for hours), C# searches for the last played track in history to determine exactly when it ended.
- Knowing its end, C# moves chronologically forward through the
"queued"sequence. It sums the physical track durations and silently marks them as"played"in Firestore, skipping those that "passed" in the past, until it hits the exact song whose duration extends into the future (relative to the current UTC clock). - It updates its database state to
"playing", records itsplay_start_timeas the theoretical start, and brings it live with the calculated offset.
The Python Worker generates a symmetrical and balanced daily buffer of a dynamic number of tracks in the database (controlled by the TRACK_COUNT environment variable, configured to 25 tracks).
To guarantee an immersive listening experience, tracks are grouped into mini-blocks of 5 consecutive songs of the same mood (~14 minutes of total immersion per style) before cycling to the next genre. With 25 tracks daily, the distribution is mathematically exact: 5 tracks per mood every day. The mix sequence is mathematically calculated in Python by the Worker using the formula:
| Sequence Range (Up to TRACK_COUNT) | Resulting Mood | Musical Genre Description | Visual Label on Cassette |
|---|---|---|---|
| Tracks 1 - 5 | ๐
"day" |
Focus Lofi (Warm electric pianos, clean acoustic guitars, rain textures) | ๐
DAY FOCUS |
| Tracks 6 - 10 | ๐ "evening" |
Jazzhop Lofi (Smooth saxophones, jazz hollow-body guitars, cozy fireplace) | ๐ CHILL COFFEE |
| Tracks 11 - 15 | ๐ "night" |
Sleep Lofi (Reverbed ambient pads, celesta bells, gentle rain soundscapes) | ๐ NIGHT GLOW |
| Tracks 16 - 20 | ๐พ "pixel" |
Chiptune Lofi (Playful retro game-console bleeps, 8-bit square-waves) | ๐พ RETRO PIXEL |
| Tracks 21 - 25 | ๐๏ธ "synthwave" |
Outrun Retrowave (Retro-futuristic analog leads, gated reverbed snares, arpeggiated bass) | ๐๏ธ OUTRUN VIBES |
Rather than presenting the tracks in predictable daily blocks, the Python Assembler executes a Global Shuffling Algorithm across all historical data:
- Massive 15-Day Window: The Assembler scans the GCS bucket to retrieve all audio assets and metadata from the last 15 daily folders (yielding a massive pool of 375 unique tracks).
- Global Randomized Shuffle: It compiles all 375 tracks into a single list and shuffles them globally (
random.shuffle()), completely breaking the chronological and mood barriers. - Unified C# Sequence Mapping: It assigns a contiguous sequence index from
1toN(375) and saves it to Firestore. This provides over 17.7 hours of continuous, non-repeating globally randomized music while keeping C#'s strict database sequence contracts 100% intact!
The default TRACK_COUNT is configured to 25 tracks. This is an explicit, senior-level architectural design limit to align with Google Cloud Platform's serverless token policies:
- The Cause: In GCP, serverless containers running Cloud Run Jobs authenticate keylessly via ADC (Application Default Credentials). The Google GenAI SDK (
genai.Client) caches the initial OAuth2 access token in memory at startup. In GCP, these transient tokens have a strict, non-refreshable lifetime of exactly 30 minutes in many security postures. - The Problem with 35+ Tracks: Generating each track takes approximately 53 seconds (composing audio, parsing duration, and uploading). Generating 35+ tracks exceeds the 30-minute window, resulting in an automatic
401 UNAUTHENTICATEDorACCESS_TOKEN_EXPIREDAPI rejection on subsequent generations. - Our Decision (Why we chose not to "fix" it): We intentionally decided not to implement token refreshing bypasses. At 25 tracks, the radio completes its run in ~22 minutes (comfortably under the 30-minute limit). Because GCS retains the previous 15 days of tracks, the assembler unifies 375 total songs, providing nearly 18 hours of continuous, globally shuffled, non-repeating dynamic music daily. This is the absolute "sweet spot" of the platform: it yields an exact 5-in-5 symmetrical distribution across all 5 moods, keeps the codebase lean and elegant, avoids unnecessary API costs, remains 100% stable under standard GCP security limits, and delivers an incredibly rich listening experience!
The src/Radio.Worker/src/main.py script is heavily hardened to ensure that the daily generation Job in the cloud never crashes due to Vertex AI safety filters:
- Trademark Exclusion (Brand-Free): All trademarked and commercial brand names of instruments or retro consoles (such as Fender, Stratocaster, Rhodes, Wurlitzer, NES, or Game Boy) have been completely purged from the codebase. They are replaced by rich acoustic descriptors (e.g., clean electric guitar, vintage handheld console) that pass Google safety filters 100% of the time.
- Dynamic Prompt Assembler: For every single track generated, Python randomly mixes distinct tempos (BPMs), acoustic string instruments, keyboards, drum styles, and environmental textures (rain, beach, fireplace, arcade bleeps), producing thousands of unique prompt combinations and infinite musical variety.
- Self-Healing Loop:
If Google AI Studio rejects a prompt due to an unforeseen safety policy block (
content_blocked400), the Worker catches the exception asynchronously, discards the prompt, immediately assembles a completely new randomized theme, and retries (up to 5 times per song) in a fully transparent, self-healing loop. - Rate-Limit Fallbacks: If the Vertex AI Lyria 3 Pro API limits are temporarily exhausted (Error Code 429), the generator intercepts the exception and seamlessly falls back to a high-quality synthetic mock track, preserving 100% pipeline continuity and preventing job crashes.
The GCP ecosystem is configured with airtight security following the Principle of Least Privilege using Terraform. The web app is not public: Identity-Aware Proxy (IAP) gates every request behind Google login, and only IAM members listed in iap_authorized_domains (users, groups, or domains) are granted roles/iap.httpsResourceAccessor to reach it.
[๐ Google Cloud IAM]
|
+---------------------------+---------------------------+
| |
[lofi-web-sa-dev] [lofi-worker-sa-dev]
(C# Web App Serverless App) (Python Generator Worker Job)
| |
- roles/datastore.user (Read/Write) - roles/datastore.user (Read/Write)
- roles/iam.serviceAccountTokenCreator (URL Signer) - roles/aiplatform.user (Call Lyria Pro API)
- GCS: roles/storage.objectViewer (Stream audio) - roles/bigquery.jobUser (Data Ingestion)
- GCS: roles/storage.objectUser (Cleanup/Create MP3s)
[๐ Identity-Aware Proxy]
|
roles/iap.httpsResourceAccessor
|
Authorized end-users (per `iap_authorized_domains`)
| GCP Resource | Resource Name | Configuration / Operating Range | Service Account (SA) / IAM Role |
|---|---|---|---|
| Web SA | lofi-web-sa-dev |
Exclusive identity for the Web App | roles/datastore.user (Firestore), roles/storage.objectViewer (GCS), roles/iam.serviceAccountTokenCreator (URL Signer) |
| Worker SA | lofi-worker-sa-dev |
Exclusive identity for the Python Worker | roles/datastore.user, roles/aiplatform.user (Vertex AI), roles/bigquery.jobUser, roles/storage.objectUser (List, Create, and Delete objects in GCS) |
| Cloud Run Service | lofi-web-service-dev |
Auto-scalable down to 0 instances when idle, INGRESS_TRAFFIC_ALL fronted by IAP |
Hosts the Interactive Blazor Web App in .NET 10 |
| Identity-Aware Proxy | iap.googleapis.com |
Gates the web app behind Google login instead of allUsers public access |
roles/iap.httpsResourceAccessor granted to iap_authorized_domains; IAP's service agent holds roles/run.invoker to forward authenticated requests |
| Cloud Run Job | lofi-generator-job-dev |
Timeout: 120 minutes (7200s), Task Count: 1 | Executes the sequential daily track purge and generation (dynamic length configured via TRACK_COUNT, e.g., 25 tracks) |
| Cloud Scheduler | trigger-lofi-generator-job-dev |
Schedule: "0 6 * * 1-5" (Mon-Fri at 6:00 AM UTC / 1:00 AM GMT-5) |
roles/run.invoker (Invokes the Cloud Run Job) |
| GCS Bucket | lofi-radio-lofi-audio-dev |
Lifecycle Rule: Delete objects older than 25 days | Secure private storage of .mp3 and .webp audio/visual assets |
| Firestore NoSQL | radio_tracks |
Unified track metadata collection | Indexed Firestore database |
While LofiRadio's hosting and server compute infrastructure (Cloud Run, Firestore) is fully serverless and costs $0.00 USD within Google Cloud's permanent Free Tier allocations, two categories carry real, usage-driven costs: the Generative AI creation APIs (fixed, predictable, scales with TRACK_COUNT) and GCS network egress (variable, scales with listening hours ร concurrent listeners โ see below).
All generative costs are managed under Vertex AI's standard billing rates. By utilizing gemini-3.1-flash-image's native multimodal token-based billing instead of flat-rate dedicated image models (like Imagen 3's $0.04/image), our daily artwork generation costs are optimized by more than 60%:
-
Multimodal Image Token Billing: Gemini 3.1 Flash Image bills generated images based on resolution token count. At the default 1K resolution (1376x768 @ 16:9 widescreen), each generated image consumes 1,120 image output tokens. At a rate of $60.00 per 1M image output tokens, each widescreen pixel art image costs exactly
(1,120 / 1,000,000) * $60.00 = $0.0672 USD(with negligible input prompt token costs).
| GenAI Task | Model / Service | Unit Price (USD) | Daily Cost (Mon-Fri) | Monthly Cost (22 Days) |
|---|---|---|---|---|
| Widescreen Artwork |
gemini-3.1-flash-image (1K 16:9) |
~$0.067 / image (1,120 tokens) | $0.336 (5 images) | $7.39 (110 images) |
| Lofi Audio synthesis | lyria-3-pro-preview |
$0.08 / song | $2.00 (25 songs) | $44.00 (550 songs) |
| GCS Storage & Data | Standard Hot Storage | $0.02 / GB-month | <$0.001 (~400 KB/day) | <$0.001 (~8.8 MB/month) |
| GCS Network Egress | Standard Egress | $0.12 / GB | variable โ see egress estimate below | variable โ see egress estimate below |
| Total Fixed GenAI + Storage Cost | Vertex AI + GCS Storage | โ | $2.34 USD | $51.40 USD |
Egress is billed separately because, unlike GenAI generation and storage, it scales with listener count and listening hours, not with TRACK_COUNT โ see the estimate below to size it for your expected audience.
Since audio streams directly from GCS to each listener's browser (not through a CDN), egress is billed per GB actually downloaded and scales with listening hours ร concurrent listeners. Access is restricted by IAP to authorized users only, keeping this volume naturally bounded. The formula used:
| Step | Formula | Notes |
|---|---|---|
| 1. Tracks played | tracks_played = (listening_hours * 60) / avg_track_duration_min |
avg_track_duration_min pulled from the live bucket, not assumed |
| 2. Data transferred | GB_decimal = (avg_track_size_MiB * tracks_played) / 1000 |
/1000 (not /1024) to match Google's decimal GB billing unit |
| 3. Cost | Cost_USD = GB_decimal * listeners * $0.12 |
Standard GCS egress-to-internet rate; verify current pricing |
avg_track_size_MiB and avg_track_duration_min should be pulled from the live bucket (e.g. gsutil du -a gs://<bucket>/**/*.mp3) rather than assumed. Applying the formula with this repo's own averages (2.5 min/track, Section 3) and a typical ~128 kbps AI-generated MP3 (~2.3 MiB/track โ placeholder, confirm with a real gsutil du on the bucket), per listener:
| Scenario | Tracks/Day | GB/Day | Cost/Day | Cost/Month (30d) |
|---|---|---|---|---|
| 8h listening session | 192 | ~0.44 GB | ~$0.053 | ~$1.58 |
| 24h listening session | 576 | ~1.32 GB | ~$0.158 | ~$4.75 |
Multiply by the number of IAP-authorized listeners for the real total (e.g. 5 listeners streaming 24/7 โ $23.75/month).
- Welcome Trial Credits: Google Cloud provides $300 USD in free welcome credits upon registration. This covers the total operational GenAI cost of LofiRadio for 121 consecutive days of 100% free production broadcasting.
- No-Charge Failed Requests: You are only billed for successful 200 OK responses. If an AI prompt is blocked by Google safety filters and our self-healing worker loop retries with a fresh prompt variation, failed/blocked attempts are never charged.
- In-Memory WebP Compression: Moving from raw PNGs to Pillow-compressed
.webpfiles (reduced from ~1.3MB to ~120KB) keeps GCS storage costs negligible under the standard free tiers. - IAP Access Gating: Because Identity-Aware Proxy restricts the web app to authorized users only (Section 5), egress volume is naturally capped by a small, known audience rather than unbounded public traffic.
The Blazor interface is optimized to deliver a cinematic, high-fidelity retro player:
- Widescreen Cinematic Interface (Desktop): Edge-to-edge layout where the widescreen pixel art background fits perfectly, removing any unnecessary margins, headers, or footers.
- YouTube Music-Style Console (Mobile/Portrait): Responsive mobile media query triggers on portrait devices, centering the pixel art as a gorgeous 1:1 rounded square album cover. It renders the player controls as a compact, touch-friendly, dark bottom console, completely avoiding scroll overflows and keeping buttons comfortable at thumb-level.
- Custom Vector Branding (
favicon.svg): Features a custom-designed, pixel-perfect vector cassette tape radio icon with crisp-edges rendering, completely optimized down to a lightweight 225 KB. - Double-Layer Fallbacks: If a GCS image fails to load or hasn't been generated yet, a smooth opacity transition hides the error and renders beautiful, animated vector SVGs (
day,evening,night,pixel,synthwave) beneath the image layer. Thesynthwavefallback scene features an infinite, interactive 3D perspective scrolling laser grid sunset with a cruising sports car silhouette! - Uncontrolled Timeline Decoupling: To completely eliminate micro-stuttering and jumping caused by SignalR WebSocket latency, the progress bar and current/total time labels are fully decoupled from Blazor's render loops. JavaScript has 100% exclusive, direct DOM write ownership of these elements, achieving ultra-smooth, native 60 FPS live playhead progress.
- Glowing Neon-Pink "NEW" Badge: Tracks generated during the active daily run (today's UTC date) automatically display a glowing, retro-futuristic, cyber-pink
"NEW"badge next to the song title, letting listeners know they are listening to the freshest AI compositions. - High-Contrast Live Badge: The top-left badge has a dark translucent backdrop (
rgba(15, 10, 25, 0.85)), text-shadows, and blur, making it perfectly readable against any light background. - Interactive Volume Slider: The volume control on the right remains 100% interactive and slidable, allowing listeners to adjust, mute, or unmute their music easily.
To verify the stateful transactional loop, the drift silence guard, and the Unit of Work contracts:
dotnet testTo populate your Firestore database with the new symmetrical daily tracks and verify Mutagen's duration parser:
# 1. Configure your local environment variables
$env:GCP_PROJECT_ID="lofi-radio"
$env:GCS_BUCKET_NAME="lofi-radio-lofi-audio-dev"
# 2. Run the Worker (Task 0 will automatically clear GCS and Firestore before starting)
python src/Radio.Worker/src/main.py(Note: Once 3 to 5 tracks are successfully generated in your terminal, you can press Ctrl + C to stop the script and test them in your web browser).
To compile and launch the Blazor Web application on your local machine:
dotnet run --project src/Radio.Web --urls=http://localhost:5162Now open your favorite browser and navigate to: http://localhost:5162. Press Play, turn up the volume, and enjoy the magic of live automated infinite lofi!
