| """ |
| π§ Magenta DJ β real-time AI music station built on Google Magenta RT2. |
| |
| Blends genres live in real time, morphs smoothly between styles, records |
| to WAV. Built for the Build Small Hackathon 2026. |
| |
| Model: Google Magenta RT2 Small β ~600M params, 48 kHz stereo. |
| Transport: gapless Web Audio scheduler (zero seams, no <audio> element gaps). |
| """ |
|
|
| import os |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") |
| os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1") |
|
|
| import json, time, uuid, math, base64, tempfile, warnings |
| import numpy as np |
| import soundfile as sf |
| import torch |
| import spaces |
| import gradio as gr |
| from huggingface_hub import hf_hub_download |
|
|
| warnings.filterwarnings("ignore") |
| from magenta_rt import paths |
|
|
| |
| MAGENTA_HOME = "/data" if os.path.isdir("/data") else "/tmp/magenta" |
| HOME = os.path.join(MAGENTA_HOME, "magenta-rt-v2") |
| os.makedirs(HOME, exist_ok=True) |
|
|
| hf_hub_download("google/magenta-realtime-2", |
| "checkpoints/mrt2_small.safetensors", local_dir=HOME) |
| paths.set_magenta_home(HOME) |
|
|
| from magenta_rt.torch import MagentaRT2 |
| from magenta_rt.torch.musiccoca import MusicCoCa |
|
|
| print("[boot] loading Magenta RT2 Smallβ¦", flush=True) |
| style_model = MusicCoCa(device="cpu") |
| model_main = MagentaRT2(size="mrt2_small", device="cuda", |
| dtype=torch.bfloat16, style_model=style_model) |
| try: |
| model_main.load_compiled(repo_id="magenta-torch/magenta-rt-aoti-small") |
| print("[boot] AOTI fast path active.", flush=True) |
| except Exception as e: |
| print("[boot] AOTI unavailable β eager:", repr(e)[:80], flush=True) |
|
|
| SR = 48000 |
| EMB_DIM = style_model.embedding_dim |
| FRAME_SAMPLES = 1920 |
| SESSION_DIR = "/tmp/mrt_dj" |
| os.makedirs(SESSION_DIR, exist_ok=True) |
|
|
| CHUNK = 32 |
| LEAD = 6.0 |
| MAX_SLEEP = 0.30 |
|
|
| MAX_VARS = 8 |
| DEFAULT_VARS = [ |
| ("upbeat pop", 1.4), |
| ("chill lo-fi hip hop", 0.5), |
| ("deep house", 0.0), |
| ("jazz cafΓ© piano", 0.0), |
| ("80s synthwave", 0.0), |
| ("cinematic strings", 0.0), |
| ] |
| PRESETS = { |
| "π΅ Pop": {"upbeat pop": 2.0}, |
| "π House": {"deep house": 2.0}, |
| "β Lo-fi": {"chill lo-fi hip hop": 2.0}, |
| "π· Jazz": {"jazz cafΓ© piano": 2.0}, |
| } |
|
|
| |
| def _slot(sid): |
| return os.path.join(SESSION_DIR, f"{os.path.basename(sid)}.json") |
|
|
| def write_slot(sid, d): |
| if not sid: return |
| tmp = _slot(sid) + "." + uuid.uuid4().hex |
| with open(tmp, "w") as f: json.dump(d, f) |
| os.replace(tmp, _slot(sid)) |
|
|
| def read_slot(sid): |
| try: |
| with open(_slot(sid)) as f: return json.load(f) |
| except Exception: return None |
|
|
| def build_ctrl(glide, cfg, drums, temp, *tw): |
| prompts, weights = [], [] |
| for i in range(MAX_VARS): |
| t = tw[2*i] if 2*i < len(tw) else "" |
| w = tw[2*i + 1] if 2*i + 1 < len(tw) else 0.0 |
| prompts.append((t or "").strip()) |
| try: weights.append(float(w)) |
| except: weights.append(0.0) |
| return { |
| "prompts": prompts, "weights": weights, |
| "transition": float(glide), "cfg": float(cfg), |
| "cfg_drums": float(drums), "temperature": float(temp), "top_k": 50, |
| } |
|
|
| def push_ctrl(sid, glide, cfg, drums, temp, *tw): |
| prev = read_slot(sid) or {} |
| c = build_ctrl(glide, cfg, drums, temp, *tw) |
| c["playing"] = prev.get("playing", True) |
| write_slot(sid, c) |
|
|
| |
| @spaces.GPU(duration=120) |
| def dj_stream(sid, glide, cfg, drums, temp, *tw): |
| from magenta_rt.torch.system import make_sampler, discretize_cfg, _float_to_int16 |
| if not sid: return |
| c0 = build_ctrl(glide, cfg, drums, temp, *tw) |
| c0["playing"] = True |
| write_slot(sid, c0) |
|
|
| if style_model.device != "cuda": |
| style_model.to("cuda") |
| dev, dt = "cuda", torch.bfloat16 |
| gen = torch.Generator(device=dev).manual_seed(0) |
| txt_cache = {} |
|
|
| def embed(lbl): |
| e = txt_cache.get(lbl) |
| if e is None: |
| e = style_model.embed(lbl).float() |
| txt_cache[lbl] = e |
| return e |
|
|
| model = model_main |
| dstate = model.model.decoder.init_streaming_f(1, dev, dt) |
| decode_state = model.init_decode_state() |
| source = source_target = cur_target_tokens = cur_cfg = cur_emb = None |
| emitted = seq = 0 |
| t0 = time.time() |
| pcm_path = os.path.join(SESSION_DIR, f"{os.path.basename(sid)}.pcm") |
| pcm = open(pcm_path, "wb") |
|
|
| try: |
| while time.time() - t0 < 116.0: |
| c = read_slot(sid) or c0 |
| if not c.get("playing", True): break |
|
|
| toks = [] |
| for _ in range(CHUNK): |
| c = read_slot(sid) or c |
| prompts = c.get("prompts") or ["instrumental music"] |
| weights = c.get("weights") or [1.0] * len(prompts) |
| g = float(c.get("transition", 4.0)) |
|
|
| |
| target = torch.zeros(EMB_DIM, device=dev, dtype=torch.float32) |
| tot = float(sum(w for w in weights if w > 0)) or 1.0 |
| any_on = False |
| for i, p in enumerate(prompts): |
| w = float(weights[i]) if i < len(weights) else 0.0 |
| lbl = (p or "").strip() |
| if w <= 0 or not lbl: continue |
| target += (w / tot) * embed(lbl) |
| any_on = True |
| if not any_on: |
| target = embed("ambient instrumental music") |
|
|
| |
| dtf = FRAME_SAMPLES / SR |
| if cur_emb is None or g <= 0.05: |
| cur_emb = target |
| else: |
| a = 1.0 - math.exp(-dtf / g) |
| cur_emb = cur_emb + a * (target - cur_emb) |
|
|
| |
| |
| |
| |
| target_tokens = style_model.tokenize(target).tolist() |
| cfgsig = (round(float(c.get("cfg", 1.6)), 3), |
| round(float(c.get("cfg_drums", 4.0)), 3)) |
| if source_target is None or target_tokens != cur_target_tokens or cfgsig != cur_cfg: |
| cur_target_tokens = target_tokens |
| cur_cfg = cfgsig |
| cfgs = [discretize_cfg(c.get("cfg", 1.6), 0.2, 40), |
| discretize_cfg(0.0, 0.2, 40), |
| discretize_cfg(c.get("cfg_drums", 4.0), 1.0, 8)] |
| cond = model._conditioning( |
| (target_tokens + [-1]*model.num_musiccoca)[:model.num_musiccoca], |
| [-1]*128, [-1], cfgs) |
| source_target = model.model.encode(cond).to(dt) |
|
|
| if source is None or g <= 0.05: |
| source = source_target |
| else: |
| a_s = 1.0 - math.exp(-dtf / g) |
| source = (source.float() + a_s*(source_target.float()-source.float())).to(dt) |
|
|
| sampler = make_sampler(c.get("temperature", 1.1), c.get("top_k", 50), gen) |
| toks.append(model.model.decoder.step_f( |
| dstate, source, sampler=sampler, |
| temporal_step=model._temporal_step, |
| depth_step=model._depth_step)) |
|
|
| new_codes = torch.cat(toks, dim=1) |
| audio = model.decode_stream(new_codes, decode_state) |
| emitted += audio.shape[1] |
|
|
| if audio.shape[1] > 0: |
| i16 = _float_to_int16(audio[0].float().cpu().numpy()).astype(np.int16) |
| raw = i16.tobytes() |
| pcm.write(raw); pcm.flush() |
| seq += 1 |
| yield f"{seq}|" + base64.b64encode(raw).decode("ascii") |
|
|
| ahead = (emitted / SR) - (time.time() - t0) |
| if ahead > LEAD: |
| time.sleep(min(ahead - LEAD, MAX_SLEEP)) |
| finally: |
| try: pcm.close() |
| except: pass |
|
|
| def stop_stream(sid): |
| c = read_slot(sid) or {} |
| c["playing"] = False |
| write_slot(sid, c) |
| return "" |
|
|
| def render_recording(sid): |
| p = os.path.join(SESSION_DIR, f"{os.path.basename(sid)}.pcm") |
| try: raw = open(p, "rb").read() |
| except: raw = b"" |
| if len(raw) < SR: |
| return None, "Nothing recorded yet β press βΆ Play first." |
| wav = np.frombuffer(raw, dtype=np.int16).reshape(-1, 2) |
| out = os.path.join(tempfile.gettempdir(), |
| f"magenta_dj_{os.path.basename(sid)[:8]}.wav") |
| sf.write(out, wav, SR, subtype="PCM_16") |
| return out, f"β
{wav.shape[0]/SR:.1f}s recorded. Download below." |
|
|
| def clear_recording(sid): |
| try: os.remove(os.path.join(SESSION_DIR, f"{os.path.basename(sid)}.pcm")) |
| except: pass |
| return None, "Cleared." |
|
|
| def apply_preset(name, *texts): |
| mp = PRESETS[name] |
| return [float(next((v for k, v in mp.items() if k.lower()==(t or "").strip().lower()), 0.0)) |
| for t in texts] |
|
|
| |
| CSS = """ |
| :root { |
| --brand: #7c5cff; |
| --teal: #19d3da; |
| --dark: #080910; |
| --card: rgba(255,255,255,.055); |
| --edge: rgba(255,255,255,.11); |
| --muted: rgba(255,255,255,.42); |
| } |
| *, *::before, *::after { box-sizing: border-box; } |
| .gradio-container { max-width: 640px !important; margin: 0 auto !important; padding: 0 12px 40px !important; } |
| .gradio-container, body { |
| background: radial-gradient(ellipse 1000px 500px at 50% -60px, #180d3a 0%, #080910 62%) fixed !important; |
| } |
| footer, .svelte-1ipelgc { display: none !important; } |
| |
| /* ββ hero ββ */ |
| #dj-hero { text-align: center; padding: 22px 4px 6px; } |
| #dj-hero h1 { |
| margin: 0 0 6px; |
| font-size: clamp(1.7rem, 7vw, 2.3rem); |
| font-weight: 900; |
| letter-spacing: -.03em; |
| background: linear-gradient(100deg, var(--brand) 0%, var(--teal) 100%); |
| -webkit-background-clip: text; background-clip: text; color: transparent; |
| } |
| #dj-hero p { margin: 0; font-size: .8rem; color: var(--muted); } |
| .model-badge { |
| display: inline-flex; align-items: center; gap: 7px; |
| margin-top: 10px; |
| padding: 5px 14px; |
| background: var(--card); |
| border: 1px solid var(--edge); |
| border-radius: 999px; |
| font-size: .72rem; |
| color: var(--muted); |
| letter-spacing: .02em; |
| } |
| .model-badge b { color: rgba(255,255,255,.72); font-weight: 600; } |
| |
| /* ββ status bar ββ */ |
| #dj-status-bar { |
| display: flex; align-items: center; gap: 10px; |
| padding: 9px 14px; |
| margin: 10px 0 6px; |
| background: var(--card); |
| border: 1px solid var(--edge); |
| border-radius: 10px; |
| font-size: .82rem; |
| color: rgba(255,255,255,.65); |
| min-height: 38px; |
| } |
| #dj-led { |
| width: 9px; height: 9px; border-radius: 50%; |
| background: #333; flex-shrink: 0; |
| transition: background .4s, box-shadow .4s; |
| } |
| #dj-led.warming { background: #f59e0b; } |
| #dj-led.live { background: var(--teal); box-shadow: 0 0 7px var(--teal); animation: ledpulse 1.6s ease-in-out infinite; } |
| #dj-led.stopped { background: var(--brand); } |
| @keyframes ledpulse { 0%,100%{opacity:1} 50%{opacity:.55} } |
| |
| /* ββ transport ββ */ |
| #transport { gap: 8px !important; margin-bottom: 0 !important; } |
| #transport button { height: 50px; border-radius: 12px; font-weight: 800 !important; font-size: .97rem !important; } |
| #play-btn { |
| background: linear-gradient(130deg, var(--brand) 0%, var(--teal) 100%) !important; |
| color: #fff !important; border: none !important; |
| } |
| #stop-btn { |
| background: var(--card) !important; |
| border: 1px solid var(--edge) !important; |
| color: rgba(255,255,255,.7) !important; |
| } |
| |
| /* ββ canvas ββ */ |
| #dj-viz-wrap { position: relative; margin: 8px 0 4px; border-radius: 12px; overflow: hidden; } |
| #dj_viz { display: block; width: 100%; height: 64px; background: #05060d; } |
| |
| /* ββ section headers ββ */ |
| .sect { |
| font-size: .72rem; font-weight: 700; letter-spacing: .1em; |
| text-transform: uppercase; color: var(--muted); |
| margin: 20px 0 8px; display: flex; align-items: center; gap: 8px; |
| } |
| .sect::after { content:""; flex:1; height:1px; background: var(--edge); } |
| |
| /* ββ genre cards ββ */ |
| .varcard { |
| display: flex; align-items: center; gap: 10px; |
| background: var(--card); |
| border: 1px solid var(--edge); |
| border-radius: 12px; |
| padding: 8px 12px; |
| margin-bottom: 6px; |
| transition: border-color .2s; |
| } |
| .varcard:focus-within { border-color: rgba(124,92,255,.45); } |
| .varcard textarea, .varcard input[type=text] { |
| font-weight: 600 !important; font-size: .88rem !important; |
| background: transparent !important; border: none !important; |
| } |
| |
| /* ββ preset pills ββ */ |
| #presetrow { gap: 8px !important; flex-wrap: wrap; } |
| #presetrow button { |
| border-radius: 999px !important; font-weight: 700 !important; |
| min-height: 38px !important; font-size: .84rem !important; |
| background: var(--card) !important; border: 1px solid var(--edge) !important; |
| color: rgba(255,255,255,.8) !important; |
| transition: border-color .15s, background .15s !important; |
| } |
| #presetrow button:hover { |
| border-color: var(--brand) !important; |
| background: rgba(124,92,255,.12) !important; |
| } |
| |
| /* ββ accordions ββ */ |
| .gr-accordion { background: var(--card) !important; border: 1px solid var(--edge) !important; border-radius: 12px !important; margin-bottom: 6px !important; } |
| .gr-accordion summary { font-size: .85rem !important; font-weight: 600 !important; color: rgba(255,255,255,.65) !important; } |
| |
| /* ββ add-var btn ββ */ |
| #add-var-btn button { border-radius: 10px !important; font-size: .82rem !important; background: var(--card) !important; border: 1px solid var(--edge) !important; color: var(--muted) !important; } |
| |
| /* ββ record section ββ */ |
| #rec-btns { gap: 8px !important; } |
| |
| /* ββ footer ββ */ |
| #dj-footer { |
| text-align: center; margin-top: 24px; padding-top: 14px; |
| border-top: 1px solid var(--edge); |
| font-size: .72rem; color: var(--muted); line-height: 1.8; |
| } |
| #dj-footer a { color: rgba(124,92,255,.8); text-decoration: none; } |
| #dj-footer a:hover { color: var(--teal); } |
| """ |
|
|
| |
| with gr.Blocks(title="Magenta DJ", |
| theme=gr.themes.Soft(primary_hue="purple", neutral_hue="slate"), |
| css=CSS) as demo: |
| sid = gr.State("") |
|
|
| |
| gr.HTML(""" |
| <div id="dj-hero"> |
| <h1>π§ Magenta DJ</h1> |
| <p>Blend any two genres. Watch them morph in real time.</p> |
| <div class="model-badge"> |
| <span>π§ </span> |
| <span><b>Google Magenta RT2</b> Β· ~600M params Β· 48 kHz stereo Β· open-source</span> |
| </div> |
| </div> |
| """) |
|
|
| |
| pcm_pipe = gr.Textbox(visible=False, elem_id="pcm_pipe") |
|
|
| |
| with gr.Row(elem_id="transport"): |
| play_btn = gr.Button("βΆ Play", variant="primary", elem_id="play-btn", scale=4) |
| stop_btn = gr.Button("β Stop", elem_id="stop-btn", scale=1) |
|
|
| |
| gr.HTML(""" |
| <div id="dj-status-bar"> |
| <span id="dj-led"></span> |
| <span id="dj-status-text">Press βΆ Play to start the AI music stream</span> |
| </div> |
| """) |
|
|
| |
| gr.HTML(""" |
| <div id="dj-viz-wrap"> |
| <canvas id="dj_viz" width="640" height="64"></canvas> |
| </div> |
| """) |
|
|
| |
| gr.HTML("<div class='sect'>π Blend Genres</div>") |
| gr.HTML("<p style='font-size:.78rem;color:rgba(255,255,255,.4);margin:-4px 0 10px'>Mix up to 8 styles. Drag the slider to set how much of each genre to add. Changes blend smoothly in real time.</p>") |
|
|
| texts, sliders, cards = [], [], [] |
| for i in range(MAX_VARS): |
| on = i < len(DEFAULT_VARS) |
| t0 = DEFAULT_VARS[i][0] if on else "" |
| w0 = DEFAULT_VARS[i][1] if on else 0.0 |
| with gr.Group(visible=on, elem_classes="varcard") as card: |
| with gr.Row(): |
| tb = gr.Textbox(value=t0, show_label=False, container=False, |
| placeholder="genre or mood (e.g. jazz cafΓ©)", max_lines=1, scale=3) |
| sl = gr.Slider(0.0, 2.0, value=w0, step=0.01, |
| show_label=False, container=False, scale=2) |
| texts.append(tb); sliders.append(sl); cards.append(card) |
|
|
| with gr.Row(elem_id="add-var-btn"): |
| add_btn = gr.Button("οΌ Add a genre", size="sm") |
|
|
| |
| gr.HTML("<div class='sect'>β‘ Quick Vibes</div>") |
| with gr.Row(elem_id="presetrow"): |
| preset_btns = [gr.Button(n, size="sm") for n in PRESETS] |
|
|
| |
| with gr.Accordion("π Fine-tune the sound", open=False, elem_classes="gr-accordion"): |
| glide = gr.Slider(0, 20, value=4, step=0.5, label="Morph speed (s) β how long genre blends take to settle") |
| cfg = gr.Slider(0.5, 7, value=1.6, step=0.1, label="Style strength β how closely the AI follows the genre labels") |
| drums = gr.Slider(1, 6, value=4, step=0.1, label="Drum intensity") |
| temp = gr.Slider(0.5, 2, value=1.1, step=0.05,label="Variation β higher = more surprising, lower = more structured") |
|
|
| |
| with gr.Accordion("β Playback buffer", open=False, elem_classes="gr-accordion"): |
| gr.Markdown("Time the browser pre-loads before starting. Raise if you ever hear a hiccup on a slow connection.") |
| buffer_ms = gr.Slider(200, 3000, value=1500, step=50, label="Startup buffer (ms)") |
|
|
| |
| with gr.Accordion("βΊ Record & download", open=False, elem_classes="gr-accordion"): |
| gr.Markdown("While playing, everything is captured automatically. Hit **Render** to save it as a WAV file.") |
| with gr.Row(elem_id="rec-btns"): |
| render_btn = gr.Button("βΊ Render recording", variant="primary") |
| clear_btn = gr.Button("π Clear") |
| rec_status = gr.Markdown("") |
| rec_file = gr.File(label="Your mix (.wav)", interactive=False) |
|
|
| |
| gr.HTML(""" |
| <div id="dj-footer"> |
| Built on <a href="https://github.com/magenta/magenta-realtime" target="_blank">Google Magenta RT2</a> |
| Β· open-source Β· Apache 2.0 |
| <br> |
| Runs in real time on a single GPU Β· no audio gaps via Web Audio API scheduling |
| </div> |
| """) |
|
|
| |
| ctrl = [glide, cfg, drums, temp] |
| for i in range(MAX_VARS): |
| ctrl += [texts[i], sliders[i]] |
|
|
| demo.load(lambda: uuid.uuid4().hex, outputs=[sid]) |
|
|
| play_btn.click(None, None, None, |
| js="() => { window.djStart && window.djStart(); }") |
| play_btn.click(dj_stream, inputs=[sid]+ctrl, outputs=[pcm_pipe]) |
|
|
| pcm_pipe.change(None, [pcm_pipe], None, |
| js="(b) => { window.djPlay && window.djPlay(b); }") |
| buffer_ms.change(None, [buffer_ms], None, |
| js="(v) => { window.djSetBuffer && window.djSetBuffer(v); }") |
|
|
| stop_btn.click(stop_stream, [sid], [gr.Markdown(visible=False)]) |
| stop_btn.click(None, None, None, |
| js="() => { window.djStop && window.djStop(); }") |
|
|
| for comp in ctrl: |
| ev = comp.release if isinstance(comp, gr.Slider) else comp.change |
| ev(push_ctrl, [sid]+ctrl, None) |
|
|
| vis_state = gr.State([i < len(DEFAULT_VARS) for i in range(MAX_VARS)]) |
| def add_var(vis): |
| vis = list(vis) |
| for i in range(MAX_VARS): |
| if not vis[i]: vis[i] = True; break |
| return [vis] + [gr.update(visible=v) for v in vis] |
| add_btn.click(add_var, [vis_state], [vis_state]+cards) |
|
|
| for b, name in zip(preset_btns, PRESETS): |
| b.click(lambda *t, n=name: apply_preset(n, *t), inputs=texts, outputs=sliders) |
|
|
| render_btn.click(render_recording, [sid], [rec_file, rec_status]) |
| clear_btn.click(clear_recording, [sid], [rec_file, rec_status]) |
|
|
|
|
| |
| HEAD = r""" |
| <script> |
| (function(){ |
| const SR = 48000; |
| var ctx=null, nextTime=0, bufferSec=1.5, lastSeq=0, running=false, started=false; |
| var gainNode=null, analyser=null, freq=null; |
| var chunkCount=0, bufferAhead=0; |
| |
| /* ββ status helper βββββββββββββββββββββββββββββββββββββββββββ */ |
| var STATES = { |
| idle: { dot:'', text:'Press βΆ Play to start the AI music stream' }, |
| warming: { dot:'warming', text:'β³ Warming up Magenta AIβ¦ first sound in ~5β8 s' }, |
| notes: { dot:'warming', text:'β« Generating first notesβ¦' }, |
| live: { dot:'live', text:'β LIVE β drag the genre sliders to morph the music' }, |
| stopped: { dot:'stopped', text:'β Stopped β press βΆ Play to resume' }, |
| }; |
| function setStatus(key) { |
| var s = STATES[key] || STATES.idle; |
| var led = document.getElementById('dj-led'); |
| var text = document.getElementById('dj-status-text'); |
| if (led) { led.className = s.dot; } |
| if (text) { text.textContent = s.text; } |
| } |
| |
| /* ββ AudioContext βββββββββββββββββββββββββββββββββββββββββββ */ |
| function ensureCtx() { |
| if (!ctx) { |
| ctx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: SR }); |
| gainNode = ctx.createGain(); |
| analyser = ctx.createAnalyser(); analyser.fftSize = 256; |
| freq = new Uint8Array(analyser.frequencyBinCount); |
| gainNode.connect(analyser); analyser.connect(ctx.destination); |
| drawViz(); |
| } |
| } |
| |
| /* ββ public API βββββββββββββββββββββββββββββββββββββββββββββ */ |
| window.djStart = function() { |
| ensureCtx(); |
| if (ctx.state === 'suspended') ctx.resume(); |
| running=true; started=false; chunkCount=0; nextTime=0; |
| setStatus('warming'); |
| }; |
| window.djStop = function() { |
| running=false; started=false; bufferAhead=0; |
| if (ctx) try { ctx.suspend(); } catch(e) {} |
| setStatus('stopped'); |
| }; |
| window.djSetBuffer = function(ms) { |
| bufferSec = Math.max(0.1, (parseFloat(ms)||1500)/1000); |
| }; |
| |
| /* ββ scheduler ββββββββββββββββββββββββββββββββββββββββββββββ */ |
| window.djPlay = function(payload) { |
| if (!payload || !running) return; |
| ensureCtx(); |
| if (ctx.state === 'suspended') ctx.resume(); |
| |
| var bar = payload.indexOf('|'); |
| if (bar < 0) return; |
| var seq = parseInt(payload.slice(0,bar), 10); |
| if (seq <= lastSeq) return; |
| lastSeq = seq; |
| |
| var b64 = payload.slice(bar+1); |
| var bin; try { bin = atob(b64); } catch(e) { return; } |
| var bytes = new Uint8Array(bin.length); |
| for (var i=0; i<bin.length; i++) bytes[i] = bin.charCodeAt(i); |
| var i16 = new Int16Array(bytes.buffer); |
| var frames = i16.length >> 1; |
| if (frames < 1) return; |
| |
| var buf = ctx.createBuffer(2, frames, SR); |
| var L=buf.getChannelData(0), R=buf.getChannelData(1); |
| for (var i=0; i<frames; i++) { L[i]=i16[2*i]/32768; R[i]=i16[2*i+1]/32768; } |
| |
| var src = ctx.createBufferSource(); |
| src.buffer = buf; src.connect(gainNode); |
| var now = ctx.currentTime; |
| if (!started) { nextTime = now + bufferSec; started = true; } |
| else if (nextTime < now+0.04) { nextTime = now + 0.04; } |
| src.start(nextTime); |
| nextTime += frames/SR; |
| bufferAhead = nextTime - ctx.currentTime; |
| |
| /* update status based on how many chunks have arrived */ |
| chunkCount++; |
| if (chunkCount === 1) setStatus('notes'); |
| if (chunkCount === 2) setStatus('live'); |
| }; |
| |
| /* ββ canvas visualiser ββββββββββββββββββββββββββββββββββββββ */ |
| function drawViz() { |
| var canvas = document.getElementById('dj_viz'); |
| if (!canvas) { requestAnimationFrame(drawViz); return; } |
| var vc = canvas.getContext('2d'); |
| (function loop() { |
| requestAnimationFrame(loop); |
| var w=canvas.width, h=canvas.height; |
| vc.clearRect(0,0,w,h); |
| if (analyser && running) analyser.getByteFrequencyData(freq); |
| |
| var BARS=56, BAR_H=h-10, bw=w/BARS; |
| for (var i=0; i<BARS; i++) { |
| var v = (analyser && running) |
| ? freq[i % freq.length]/255 |
| : 0.05 + 0.03*Math.sin(Date.now()/350+i*0.6); |
| var bh = Math.max(2, v*BAR_H); |
| var g = vc.createLinearGradient(0, BAR_H, 0, BAR_H-bh); |
| /* colour shifts toward teal as bar gets taller */ |
| var ratio = Math.min(v*1.4, 1); |
| var r1 = Math.round(124 + (25-124)*ratio); |
| var g1 = Math.round(92 + (211-92)*ratio); |
| var b1 = Math.round(255 + (218-255)*ratio); |
| g.addColorStop(0, 'rgba('+r1+','+g1+','+b1+',.9)'); |
| g.addColorStop(1, 'rgba('+r1+','+g1+','+b1+',.25)'); |
| vc.fillStyle = g; |
| vc.fillRect(i*bw+1, BAR_H-bh, bw-2, bh); |
| } |
| |
| /* buffer health strip */ |
| var MAX_BUF=6, ratio2=Math.min(bufferAhead/MAX_BUF, 1); |
| vc.fillStyle='rgba(0,0,0,.5)'; |
| vc.fillRect(0, h-10, w, 10); |
| if (ratio2 > 0) { |
| var hg=vc.createLinearGradient(0,0,w,0); |
| hg.addColorStop(0,'#7c5cff'); hg.addColorStop(1,'#19d3da'); |
| vc.fillStyle=hg; |
| vc.fillRect(0, h-10, w*ratio2, 10); |
| } |
| if (running && bufferAhead > 0.1) { |
| vc.fillStyle='rgba(255,255,255,.3)'; |
| vc.font='8px monospace'; |
| vc.textAlign='right'; |
| vc.fillText('buffer '+bufferAhead.toFixed(1)+'s', w-4, h-1); |
| } |
| })(); |
| } |
| |
| /* grab canvas once Gradio finishes rendering */ |
| var t=setInterval(function(){ |
| if (document.getElementById('dj_viz')) { drawViz(); clearInterval(t); } |
| /* also reset status dot if it isn't set yet */ |
| setStatus('idle'); |
| }, 300); |
| })(); |
| </script> |
| """ |
|
|
| if __name__ == "__main__": |
| demo.queue(default_concurrency_limit=4, max_size=32).launch( |
| show_error=True, ssr_mode=False, head=HEAD) |