Skip to content
fraadapPublic

About

Multimodal music player controlled through Kinect gestures and natural-language voice commands, featuring real-time gesture recognition, AI intent parsing, synchronized lyrics, and reactive audio visualization.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🎵 Immersify

A Multimodal, Next-Generation Music Player

Control music with your hands and your voice, no phone, no remote, no mouse.

Immersify turns a wall projection into a shared listening experience: mid-air gestures captured by a Kinect v2, natural-language voice commands transcribed locally by Whisper and parsed by Gemini, karaoke-synced lyrics, AI-generated trivia, and a reactive audio visualizer, all in one full-screen interface.


Python FastAPI Kinect v2 MediaPipe Whisper Gemini Platform


Gesture Recognition • Speech-to-Text • LLM Intent Parsing • Real-Time SSE • Audio Visualization


Immersify: gesture and voice controlled music player in action

Final project, Next Generation User Interfaces (NGUI), M.Sc. Computer Science, A.Y. 2025–2026


📖 Table of Contents

✨ Why Immersify 🧭 System Overview 👋 Gesture Vocabulary
🎙️ Voice Pipeline 🖥️ The Interface 🧠 Gesture Recognition
🏗️ Architecture 🔌 API Reference ⚡ Performance
🚀 Getting Started 📁 Project Structure 🔭 Future Work

✨ Why Immersify

Streaming a playlist is easy. Sharing music is not.

Music is a social, physical, emotional activity, but every mainstream player targets a single user staring at a phone. The queue becomes a private object passed around a room; the music becomes background noise.

Immersify removes the device from the loop. It is designed for wall projection in a living room, a party, or a bar. Everyone in the room sees the same screen, and anyone can take control by simply raising a hand or speaking out loud.

🖐️ Touchless

9 mid-air gestures for every playback action, recognized in ~200 ms with visual feedback before the action fires.

🗣️ Conversational

Say "play Bohemian Rhapsody". Whisper runs locally for privacy; Gemini turns free speech into structured intents.

🌌 Immersive

A 60 FPS Spectral Mandala visualizer, karaoke-synced lyrics, and AI trivia, the palette re-themes itself for every song.


🧭 System Overview

A single Windows PC, a Kinect v2 on USB 3.0, and a projector. No extra camera and no extra microphone, the Kinect provides both.

flowchart LR
    subgraph HW["Hardware"]
        K["Kinect v2<br/>RGB 1920x1080<br/>IR 512x424<br/>4-mic array"]
        P["Projector<br/>and system audio"]
    end

    subgraph BE["Backend - FastAPI on port 8002"]
        KT["Kinect daemon thread<br/>gestures and voice"]
        API["REST endpoints<br/>search, audio, lyrics, volume"]
        SSE["SSE event bridge"]
    end

    subgraph EXT["External services"]
        G["Genius API<br/>metadata and lyrics"]
        Y["yt-dlp<br/>audio source"]
        L["LRCLib<br/>synced lyrics"]
        GM["Gemini 2.5 Flash<br/>intent and trivia"]
    end

    subgraph FE["Frontend - full-screen browser"]
        UI["Player UI, Canvas visualizer<br/>lyrics, queue, fun facts"]
    end

    K --> KT
    KT --> SSE
    SSE --> UI
    UI --> API
    API --> UI
    API --> G
    API --> Y
    API --> L
    API --> GM
    UI --> P
Loading

Three layers, one process:

Layer Responsibility
Backend (app.py) FastAPI on port 8002. All external API calls, audio download/cache, Windows volume control, SSE fan-out. Every blocking call (yt-dlp, pycaw COM, librosa) is offloaded with asyncio.to_thread so the event loop never stalls.
Perception (src/kinect/) A daemon thread running the dual-stream RGB/IR gesture pipeline and, on demand, the Whisper + Gemini voice pipeline.
Frontend (src/frontend/) Zero-build vanilla HTML/CSS/JS. Web Audio API for the visualizer, EventSource for real-time gesture events.

👋 Gesture Vocabulary

Nine gestures cover the entire playback surface. Each one has a dedicated preview animation that starts the instant the gesture is first detected and completes only if it is held long enough, so the user always knows what is about to happen, and can back out by lowering their hand.

Gesture Action Preview animation
👌 OK Start a voice command Wave indicator appears at the top
🖐️ STOP Play / Pause A ring pulses around the play button
☝️ UP Volume +10 % A + badge floats above the volume bar
👇 DOWN Volume −10 % A − badge floats above the volume bar
👉 RIGHT Next track Next queue item pulses cyan
👈 LEFT Previous track Previous queue item pulses purple
✊ FIST Mute / Unmute The mute icon shimmers
🤙 COOL Toggle shuffle The shuffle button orbits
✌️ PEACE Toggle repeat The repeat button bounces

Design principle: feedback before commitment. gesture_preview → (held) → execute, or gesture_cancel if the hand moves away.


🎙️ Voice Pipeline

Raise 👌 and speak. Gesture recognition pauses while listening, voice and gesture are treated as mutually exclusive modalities rather than fused, keeping the recognition pipeline simple and predictable.

sequenceDiagram
    autonumber
    participant U as User
    participant K as Kinect thread
    participant W as Whisper local
    participant G as Gemini 2.5 Flash
    participant B as Browser

    U->>K: OK gesture
    K->>B: voice_state listening, music pauses
    U->>K: speaks - play Bohemian Rhapsody
    Note over K: VAD hysteresis<br/>start above RMS 0.003<br/>stop after 1.2 s below 0.0007<br/>plus 500 ms pre-buffer
    K->>B: voice_state elaborating, music resumes
    K->>W: 16 kHz mono, peak-normalized audio
    W-->>K: transcription, 2 to 5 s on CPU
    K->>G: prompt and transcription, search grounding on
    G-->>K: JSON with action, song, artist
    K->>B: voice_state searching, track starts
Loading

Why it feels fast: the elaborating event fires the moment silence is detected, before Whisper even starts. Music resumes immediately instead of waiting 5–8 s for the AI round-trip, cutting perceived latency dramatically.

Why local Whisper: speech never leaves the machine. The base model handles accented speech and moderate background noise well, and stays practical on CPU.


🖥️ The Interface

A single full-screen dark page, tuned for readability on a projected surface: near-black background with cyan and purple accents, plus a dominant color extracted from the album art that re-themes the visualizer and background gradient for every track.

Immersify full-screen interface: queue, Spectral Mandala, synced lyrics
✳️ Spectral Mandala, four reactive layers

Rendered on an HTML5 Canvas at ~60 FPS via requestAnimationFrame, driven by a 256-bin FFT taken straight from the Web Audio API AnalyserNode (no backend round-trip during playback).

Layer Reacts to
Outer glowing halo Bass energy, a slow breathing expansion
90 radial bars Per-band amplitude, fading cyan → purple
Rotating waveform ring Mid-range frequencies
Central pulsing circle Overall audio energy
🎤 Synchronized lyrics, a three-tier fallback chain
  1. LRCLib, true LRC timestamps. parseLRC() builds ordered time–text pairs; the active line is highlighted with a larger font and glow while smooth scrolling keeps it centered.
  2. Onset detection, if no LRC exists, the cached MP3 is analyzed with librosa and the Genius lyrics are distributed across detected note onsets. Not frame-perfect, but it follows the musical structure.
  3. Static text, Genius lyrics as a plain scrollable panel.
🔍 Search & retrieval, a three-stage pipeline

Stage 1, Genius. A bare title, a bare artist, or Title, Artist all work. With a dash separator both orderings are tried. Returns title, artist, album, lyrics and album art (cover_art_url → song_art_image_url → header_image_url).

Stage 2, YouTube. ytsearch5:{title} {artist} fetches five candidates, scored by a hand-tuned heuristic built to beat the classic "first result is a live cover" problem:

Signal Score
YouTube Music auto-generated "Topic" channel +50
VEVO channel +40
Title match in video title +30
Artist in channel name +25
Artist match in video title / verified channel +20
Duration within 3 s / 8 s / 15 s of expected +60 / +30 / +10
Duration off by more than 30 s −25
cover, remix, karaoke, tribute, live, acoustic, instrumental −40 each

Stage 3, Delivery. The winner is downloaded as M4A, transcoded to 192 kbps MP3 with ffmpeg, cached in memory under "title||artist", and served as a StreamingResponse with a correct Content-Length. An asyncio.Lock per cache key means two simultaneous requests for the same song share one download instead of spawning duplicate yt-dlp processes.

💡 Fun facts & voice states

Fun facts. On each new track the frontend calls /api/get_fun_facts; Gemini 2.5 Flash returns five concise facts separated by §, displayed one at a time with auto-rotation every 6 s (manual navigation and an auto-rotate toggle are provided).

Voice states. The wave indicator cycles through four visual states: LISTENING (cyan, music paused) → ELABORATING (purple, music resumed) → SEARCHING (purple + detected song) → ERROR (red, "SORRY, TRY AGAIN" for 2.5 s).


🧠 Gesture Recognition Under the Hood

Getting reliable hand tracking in a dark projection room, where a projector also throws harsh, shifting light onto the user's hands, is the core technical problem of this project. The answer is a per-frame adaptive pipeline.

flowchart TD
    A["Kinect frame at 30 fps"] --> B{"90th percentile<br/>grayscale brightness"}
    B -->|"above 60 of 255"| C["RGB stream"]
    B -->|"at or below 60 of 255"| D["IR stream<br/>16 to 8 bit, gain 2.5x"]
    C --> E["MediaPipe HandLandmarker<br/>VIDEO mode, 21 3D landmarks"]
    D --> E
    E --> F["77-D feature vector<br/>63 normalized XYZ<br/>9 distances, 5 angles"]
    F --> G["Random Forest<br/>RGB or IR model"]
    G --> H{"confidence at least 0.70"}
    H -->|"no, RGB path"| I["Cross-check IR classifier<br/>higher confidence wins"]
    H -->|"yes"| J["Temporal stability filter"]
    I --> J
    J --> K["gesture_preview, then execute or cancel"]
    K --> L["SSE to browser"]
Loading
Adaptive stream switching

The 90th percentile of grayscale intensity is used instead of the mean, so large dark regions (clothing, background) don't drag the estimate down, what matters is the light actually hitting the hand. Above 60/255 the RGB classifier runs; below it, the Kinect's IR frame is boosted ×2.5 during 16→8-bit conversion and sent to the IR classifier. Under mixed lighting, a low-confidence RGB prediction (< 0.70) additionally triggers an IR cross-check, and the more confident prediction wins, no need to wait for the brightness threshold to flip.

Feature engineering

77 dimensions, designed for invariance to hand size, position and wrist tilt: 63 landmark coordinates centered on the wrist and scaled by max inter-landmark distance · 9 distances (5 wrist→fingertip, 4 adjacent fingertip pairs) capturing extension · 5 joint angles at the middle phalanges via three-point cosine similarity, capturing curvature.

Temporal stability filter

A small state machine converts noisy per-frame predictions into intentional actions: STABLE_FRAMES = 6 high-confidence frames to fire · STABLE_TOLERANCE = 2 tolerated glitch frames before reset · NO_HAND_RESET_FRAMES = 3 · plus an emitted_label guard so a held gesture never repeats until the hand leaves and returns.

📊 Dataset & Models

Two datasets were collected, one under normal room lighting (RGB) and one under low/no ambient light (IR), across 9 gesture classes and 10 different people, then split 80/20 with stratification.

Model Key hyperparameters RGB IR
Random Forest ⭐ n_estimators=100, max_depth=15, min_samples_split=10 ✅ ✅
SVM kernel=RBF, C=5, gamma=scale, probability=True ✅ ✅
XGBoost n_estimators=150, max_depth=10, min_child_weight=3 ✅ ✅
LightGBM n_estimators=150, min_child_samples=20, reg_lambda=0.1 ✅ ✅

Hyperparameters were deliberately reduced from initial tuning values (RF started at 300 estimators, SVM at C=10) after observing overfitting on the small per-class sample counts.

⭐ Random Forest is deployed on both streams. It was consistently the most accurate on both datasets, is fast single-threaded at inference (n_jobs=1), and its feature-importance output makes the classifier interpretable, we can see exactly which landmarks drive each decision.

All eight trained models live in src/kinect/models/, and the full evaluation artifacts, confusion matrices for every model/stream combination and feature-importance plots, are generated into src/kinect/reports/:

RGB Random Forest confusion matrix RGB feature importance

ℹ️ LEFT and RIGHT are under-represented in the datasets: the usual horizontal-flip augmentation is unavailable here, because mirroring a frame inverts the very orientation that defines those two gestures.

Retrain everything with:

py src/kinect/train/train_models.py

The script trains all four architectures on both streams, prints a full accuracy / precision / recall / F1 comparison table (macro and weighted) with training times, and writes the reports and .joblib artifacts.


🏗️ Technical Architecture

The threading bridge

The hard constraint: Kinect capture, MediaPipe inference and Whisper are blocking, while SSE delivery must stay on the asyncio event loop. Immersify bridges the two worlds with a queue hand-off:

Kinect daemon thread  →  queue.Queue  →  asyncio poller (16 ms)  →  asyncio.Queue per client  →  SSE  →  EventSource

This keeps blocking perception work in a plain thread while every connected browser gets its own non-blocking event stream, end-to-end gesture-to-animation latency lands at ~33–50 ms.

Kinect SDK patch 🐛

During development a genuine bug was found in PyKinect2's PyKinectRuntime.py: color frames were requested in RGBA while OpenCV expects BGRA, swapping the red and blue channels in every frame and silently breaking all RGB-based processing. Fixed by switching the color format constant to ColorImageFormat_Bgra, which restored correct channel ordering across the whole pipeline.

System volume

Windows master volume is driven through pycaw (a WASAPI COM wrapper). Since COM calls are synchronous, every read/set/mute operation is isolated in a helper and dispatched via asyncio.to_thread. The browser polls state at most once every 10 s, while any user action triggers an immediate update.

Audio streaming: why download-then-serve

The first implementation streamed audio to the browser while yt-dlp was still downloading it. It failed: Windows file-locking in yt-dlp intermittently blocked concurrent reads, and unreliable chunked output broke browser buffering and seeking. The final design downloads fully, transcodes, caches in memory, and serves with a proper Content-Length, costing a one-time wait per song in exchange for rock-solid playback and seeking.


🔌 API Reference

Method Endpoint Description
GET / Serves the full-screen player (src/frontend/index.html)
GET /static/* CSS / JS assets
GET /api Service metadata
GET /api/search Genius search → normalized song metadata
GET /api/audio Cached MP3 bytes for a track
GET /api/stream Streaming audio response
GET /api/audio_nowplaying Downloads + serves the current track as MP3
GET /api/lyrics_lrc LRCLib synced lyrics, Genius fallback
GET /api/analyze_lyrics Onset-detection timing for unsynced lyrics
GET /api/spectrum Mel-spectrogram bands for the visualizer
GET /api/get_fun_facts Gemini-generated song trivia
POST /api/clear_fun_facts_cache Invalidate the trivia cache
GET POST /api/volume Read / set Windows system volume (0–100)
POST /api/volume/mute Toggle system mute
GET /api/status · /api/playlist Player state and queue
POST /api/play · /api/pause · /api/resume · /api/next Playback control
GET /api/gesture_events 📡 SSE stream, real-time gesture & voice events
POST /api/gesture Inject a gesture event manually (testing without hardware)

⚡ Performance

Event Measured latency
Gesture preview → browser animation ~33–50 ms 🟢 well inside the 100 ms direct-manipulation threshold
Gesture execute (stable → action) ~200 ms 🟢 6 frames @ 30 fps
SSE poller wakeup interval 16 ms 🟢
LRCLib lyrics fetch ~200–500 ms 🟢
Gemini intent parsing ~1–3 s 🟡
Whisper transcription (CPU, 3–5 words) ~2–5 s 🟡 GPU would cut this sharply
Voice command round-trip ~4–10 s 🟡 dominated by CPU Whisper
First play (yt-dlp + ffmpeg) ~10–30 s 🟠 once per song, scales with duration
Subsequent plays (in-memory cache) < 1 ms 🟢

👥 User study

A User Experience Questionnaire (UEQ) evaluation was run with 10 participants across the six standard dimensions, Attractiveness, Perspicuity, Efficiency, Dependability, Stimulation, Novelty, comparing two interface generations: V1 (initial Tkinter design) vs V2 (the current browser interface).

V1 scored 0.6–1.5 across dimensions, with Perspicuity and Efficiency as the clear weak points while Stimulation and Novelty trended higher, the gesture interaction itself held interest, but the interface got in the way. V2 responded with a clearer visual hierarchy, explicit gesture labelling, a shortened interaction path for common tasks, stronger recognition feedback, and improved contrast and typography. The largest gains landed exactly on Perspicuity and Efficiency, the two dimensions that most gate adoption of gesture interfaces.

⚠️ Stated limitation: participants were recruited from close contacts of the team, introducing selection and social-desirability bias and limiting generalizability. The comparison between iterations remains informative; the absolute scores should be read with that caveat.

Full methodology, charts and related-work discussion: 📄 doc/final_report.pdf


🚀 Getting Started

📋 Requirements

🐍 Python 3.11 Required, the Kinect and audio stack has strict version constraints
🪟 Windows 10 / 11 pycaw and PyKinect2 are Windows-only
🎬 ffmpeg On PATH, used for MP3 transcoding
📷 Kinect for Windows v2 + SDK v2 Optional, the web player runs fine without it
🎤 Microphone The Kinect 4-mic array is auto-detected and preferred

1️⃣ Environment

py -3.11 -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt

2️⃣ Gesture & voice extras (optional, skip for web-only mode)

pip install sounddevice openai-whisper mediapipe joblib scikit-learn pykinect2

⚠️ PyKinect2 requires the Kinect for Windows SDK v2 to be installed system-wide.

3️⃣ API keys

copy .env.example .env
GENIUS_ACCESS_TOKEN=your_genius_access_token_here
GEMINI_API_KEY=your_gemini_api_key_here
Key Where to get it Cost
GENIUS_ACCESS_TOKEN genius.com/api-clients Free
GEMINI_API_KEY Google AI Studio Free tier

4️⃣ Run

py app.py

Open http://localhost:8002 and go full-screen. The Kinect gesture thread starts automatically when the hardware is detected, and the player stays fully usable with mouse and keyboard when it isn't.

🔧 Standalone debug entry points
py src/kinect/multicam_kinect.py    # live gesture inference with a camera preview window
py src/kinect/vocal_commands.py     # Whisper + Gemini voice command loop
py src/kinect/create_dataset_kinect.py  # capture new labelled gesture samples
py src/kinect/train/train_models.py     # retrain all 8 classifiers + reports

Hardware smoke tests (camera, IR, microphone, volume) live in src/kinect/test/. No Kinect? Drive the UI by POSTing to /api/gesture.


📁 Project Structure

Immersify/
├── app.py                          # 🎛️  FastAPI server, REST, SSE, audio cache, volume
├── requirements.txt
├── .env.example                    #     API key template (.env is git-ignored)
├── doc/
│   └── final_report.pdf            # 📄  Full academic report: design, evaluation, related work
└── src/
    ├── audio_player.py             # 🔊  MP3 playback via pygame-ce
    ├── audio_visualizer.py         # 📊  Mel spectrogram / FFT analysis
    ├── genius_searcher.py          # 🎼  Genius search + lyrics + album art
    ├── youtube_player.py           # ⬇️  yt-dlp extraction with the scoring heuristic
    ├── playlist_manager.py         # 📃  Queue state
    ├── fun_facts_manager.py        # 💡  Gemini trivia generation
    ├── frontend/
    │   ├── index.html              # 🖥️  Single-page player
    │   ├── immersify.css           # 🎨  Full dark theme + gesture preview animations
    │   └── immersify.js            # ⚙️  Visualizer, LRC parser, SSE client, controls
    └── kinect/
        ├── multicam_kinect.py      # 👋  RGB/IR capture, landmarks, features, inference
        ├── multimodal_manager.py   # 🔀  Gesture ID → player action mapping
        ├── vocal_commands.py       # 🎙️  VAD + Whisper + Gemini intent parsing
        ├── copilot.py              # 🤖  Gemini API wrapper
        ├── create_dataset_kinect.py# 📥  Dataset capture tool
        ├── music_assistant_prompt.txt
        ├── hand_landmarker.task    #     MediaPipe hand landmark model
        ├── models/                 # 🧠  8 trained classifiers (RF · SVM · XGB · LGBM × RGB · IR)
        ├── reports/                # 📈  Confusion matrices + feature importance plots
        ├── train/                  # 🏋️  Datasets and training pipeline
        └── test/                   # 🔧  Standalone hardware test scripts

🔭 Future Work

  • ⚡ GPU-accelerated Whisper, target sub-2 s voice command round-trip
  • 🎯 Gesture calibration tool, per-user adaptation to hand size and posture
  • 🎬 Music video rendering, synchronized visuals beyond the Mandala
  • 🧩 Song quiz mode, the one feature cut from the original requirements to protect the core interaction

🧾 Built with

FastAPI · uvicorn · PyKinect2 · MediaPipe · OpenCV · scikit-learn · XGBoost · LightGBM openai-whisper · google-genai · lyricsgenius · yt-dlp · librosa · pygame-ce · pycaw · sounddevice


Immersify, making shared music listening active, gestural, and immersive.

Next Generation User Interfaces · M.Sc. Computer Science · A.Y. 2025–2026

About

Multimodal music player controlled through Kinect gestures and natural-language voice commands, featuring real-time gesture recognition, AI intent parsing, synchronized lyrics, and reactive audio visualization.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages