USB/Serial controller for SK6812 RGBW LED strips with Piano Mode support for Arturia Keylab 49 MKII and visualisation of 60+ musical scales, integrated with Bitwig Studio via OSC.
🌱 Help Keep This Going Your support makes a real difference. If you value my work and want to help me continue creating, please consider making a donation. 💙 Donate here: https://paypal.me/TomasMark Every contribution is truly appreciated ✨
# 1. Upload firmware to Arduino
# Arduino IDE → Open → ds_usb_controller/ds_usb_controller.ino → Upload
# 2. Install Python dependencies
pip install -r requirements.txt
# 3. Run the launcher
python launcher.py # interactive menu
python launcher.py --mode osc # OSC bridge (Bitwig → LED)
python launcher.py --mode piano # Piano interactive tool🎹 Piano Mode
- Arturia Keylab 49 MKII (49 keys → 107 LEDs)
- 60+ musical scales (Major, Minor, Pentatonic, Blues, Exotic…)
- Scale degree colour-coding on the LED strip
- GUI scale selector (Tkinter dark theme)
- Chord progression explorer
- Demo effects: rainbow, knight rider, fire, wave, gradient
🔵 LED Controller
- Fast binary protocol (~16–21 FPS full strip updates)
- SK6812 RGBW (4-channel)
- Pixel / Range / Stream / Buffer / Bulk modes
- Hardware gradient and global brightness
- Up to 1000 LEDs
🎼 Scale Visualisation
- Real-time LED visualisation of 60+ musical scales directly on the strip
- Each scale degree mapped to a distinct colour (Root = Red, 5th = Blue, …)
- Instant visual feedback when root note or scale type changes
- Works both in live Piano Mode and via the Bitwig OSC Bridge
🎛️ Bitwig OSC Bridge
- Receives
/dreamscaler/scaleOSC messages from Bitwig Studio - Thread-safe serial access (concurrent calls debounced via
Lock) - Buffer mode writes (single atomic update per scale change)
Arduino Pin 6 → SK6812 Data In
Arduino GND → SK6812 GND → PSU GND (common ground!)
PSU 5 V → SK6812 5 V
Requirements:
- Arduino Uno / Nano / Mega
- SK6812 RGBW LED strip (not WS2812 — 4 channels required)
- External 5 V PSU (107 LEDs ≈ 8.6 A, 144 LEDs ≈ 11.5 A)
Tested PSU for 1 m / 144-LED strip: A 5 V 10 A 50 W mains adapter (switching power supply) is the ideal choice. It provides enough headroom for full-brightness RGBW operation while keeping heat and voltage drop under control.
Main components:
Firmware defaults: 107 LEDs, Pin 6, 115200 baud, Protocol v11
- Open
ds_usb_controller/ds_usb_controller.inoin Arduino IDE - Upload (Ctrl+U)
No external Arduino libraries are required. The SK6812 driver is bundled in ds_usb_controller/.
pip install -r requirements.txtFind your port:
- Windows: Device Manager → Ports (COM & LPT)
- Linux:
ls /dev/ttyUSB* /dev/ttyACM* - macOS:
ls /dev/tty.usbserial-*
DreamScaler/
├── config.py # ⚙️ Central config — port, brightness, colours
├── DreamScaler.control.js # Bitwig controller script (sends OSC and stores project settings)
├── launcher.py # Single CLI entry point (--mode osc|piano)
├── osc_bridge.py # OSC listener → LED driver
├── piano.py # Unified piano tool (GUI, scales, chords, demos)
├── controller_api.py # Low-level Arduino serial API
├── arturia_keylab49_map.py # 49-key → 107-LED mapping
├── arturia_keylab61_map.py # 61-key mapping variant
├── scales.json # Single source of truth for scale definitions
├── scales_data.js # Auto-generated scale data used by Bitwig
├── generate_scales_js.py # Regenerates scales_data.js and markdown references
├── scales_en.md # Generated English scale reference
├── scales_cs.md # Generated Czech scale reference
├── release_port.py # Force-release a blocked serial port
├── install_task.bat # Register Windows auto-start task (double-click)
├── install_task.ps1 # PowerShell script behind install_task.bat
├── remove_task.ps1 # Remove Windows auto-start task
├── requirements.txt
└── ds_usb_controller/
└── ds_usb_controller.ino # Arduino firmware (Protocol v11)
Bitwig Studio
└─ DreamScaler.control.js
└─ OSC /dreamscaler/scale {rootIndex, scaleName} → UDP:9001
└─ osc_bridge.py
└─ controller_api.py → Serial 115200 baud
└─ Arduino ds_usb_controller.ino
└─ SK6812 RGBW LED strip (Pin 6)
piano.py can also drive the strip directly through controller_api.py, without Bitwig.
| File | Role |
|---|---|
launcher.py |
Starts either OSC Bridge or Piano Mode from one entry point |
osc_bridge.py |
Receives OSC from Bitwig and translates it into LED updates |
piano.py |
Interactive tool for scales, chords, layout checks and demo effects |
controller_api.py |
Owns the serial protocol and Arduino communication |
config.py |
Default port, LED intensity and scale-degree colours |
scales.json |
Single source of truth for all scale definitions and metadata |
scales_data.js |
Generated JavaScript data consumed by the Bitwig controller |
generate_scales_js.py |
Rebuilds scales_data.js, scales_en.md and scales_cs.md |
DreamScaler.control.js |
Bitwig controller script with per-project settings and OSC output |
All LED operations are sent as compact binary commands at 115200 baud. The Arduino replies with 0xF0 for success or 0xFE plus an error code.
| Command | Code | Parameters |
|---|---|---|
PING |
0x01 |
— |
SET_PIXEL_RGBW |
0x20 |
[idx] [R G B W] |
SET_ALL_RGBW |
0x31 |
[R G B W] |
STREAM_START |
0x50 |
[count] |
SYNC |
0x60 |
Push frame to strip |
All user-facing settings are in one place — edit this file, no need to touch anything else:
# =============================================================
# DreamScaler — central configuration
# Edit this file to change port, brightness and LED colours.
# =============================================================
import sys
# Serial port of the Arduino USB controller — selected automatically by OS.
if sys.platform == 'win32':
COM_PORT = 'COM4'
else:
COM_PORT = '/dev/ttyUSB0'
# LED brightness (R, G, B, W values are multiplied by this)
# Evening / dark room : 1–3
# Daytime / bright room : 10–30
LED_INTENSITY = 1
# Maximum brightness for a note that is actively played (velocity 127).
# Scales linearly with velocity: brightness = NOTE_PLAYING_INTENSITY * velocity / 127
# The playing note keeps its scale-degree colour, just brighter.
# Notes outside the current scale flash white at this intensity.
NOTE_PLAYING_INTENSITY = 10
# Scale degree colours (R, G, B, W)
# Each tuple maps a scale degree (1 = root … 7 = leading tone)
# to an RGBW colour. Adjust to taste.
SCALE_DEGREE_COLORS = {
1: (LED_INTENSITY, 0, 0, 0), # Root – Red
2: (0, 0, 0, LED_INTENSITY), # Second – White
3: (0, LED_INTENSITY, 0, 0), # Third – Green
4: (LED_INTENSITY, LED_INTENSITY, 0, 0), # Fourth – Yellow
5: (0, 0, LED_INTENSITY, 0), # Fifth – Blue
6: (0, LED_INTENSITY, LED_INTENSITY, 0), # Sixth – Cyan
7: (LED_INTENSITY, 0, LED_INTENSITY, 0), # Seventh – Purple
'dim': (1, 0, 0, 0), # Diminished – dim red
}After changing COM_PORT, re-run install_task.bat to update the scheduled task.
SCALE_DEGREE_COLORS controls the default root-to-degree colour mapping, and NOTE_PLAYING_INTENSITY sets how bright actively played notes are relative to the static scale display.
The OSC bridge can start automatically every time you log into Windows:
- Double-click
install_task.bat— registers a Windows Scheduled Task - From that point on,
osc_bridge.pystarts silently on every login - It will auto-reconnect to the Arduino if the USB is plugged in after login
Task name : DreamScaler OSC Bridge
Trigger : At log on (current user)
Action : pythonw osc_bridge.py --port COM4
Restart : up to 3× if it crashes
To remove the task:
powershell -ExecutionPolicy Bypass -File remove_task.ps1To update the task (e.g. after changing COM_PORT in config.py):
Double-click install_task.bat again
Single entry point for the whole project.
python launcher.py # interactive menu
python launcher.py --mode osc # OSC bridge
python launcher.py --mode piano # Piano tool
python launcher.py --mode osc --port COM5 --osc-port 9001
python launcher.py --mode piano --port COM5 --jump 12 # jump to GUI selector| Argument | Default | Description |
|---|---|---|
--mode |
(menu) | osc or piano |
--port |
config.py |
Arduino serial port |
--osc-port |
9001 |
UDP port for incoming OSC (osc mode) |
--jump |
— | Skip to a specific piano menu option |
Interactive tool — run via launcher or directly:
python piano.py COM4
python piano.py COM4 12 # jump straight to the GUI scale selectorMenu options:
| Option | Description |
|---|---|
1 |
All keys (white = warm white, black = green) |
2 |
White keys only |
3 |
Black keys only |
4 |
Keys coloured by octave |
5 |
Key test animation (sweeps through all 107 LEDs) |
7 |
Print key map (C2–C6, 49 keys) |
10 |
Show a scale on LEDs (category → scale → root note) |
11 |
Chord progressions explorer |
12 |
GUI scale selector (Tkinter window) |
20 |
Demo effects (rainbow, knight rider, fire, wave, gradient) |
0 |
Quit |
Scale degree colour coding (LEDs):
| Degree | Colour |
|---|---|
| 1 – Root | Red |
| 2 – Major 2nd | White |
| 3 – Major 3rd | Green |
| 4 – Perfect 4th | Yellow |
| 5 – Perfect 5th | Blue |
| 6 – Major 6th | Cyan |
| 7 – Major 7th | Purple |
Receives scale changes from Bitwig Studio and updates the LED strip.
python osc_bridge.py --port COM4 --osc-port 9001OSC message from Bitwig:
/dreamscaler/scale <rootIndex: int 0–11> <scaleName: string>
rootIndex 0 = C, 1 = C#, …, 11 = B.
scaleName is the English scale name generated from scales.json and consumed via scales_data.js.
Thread safety: Two rapid OSC messages (root + scale type from Bitwig) are handled by a Lock — the second concurrent call is silently dropped (debounce effect). The LED is updated on the next message when the lock is free.
Auto-reconnect: If the Arduino is not connected at startup (or disconnects), the bridge keeps running and automatically retries the connection on the next incoming OSC message (cooldown: 10 s).
from controller_api import LEDController
with LEDController('COM4') as led:
# Single pixel
led.set_pixel(0, r=255, g=0, b=0, w=0)
# Range (same colour)
led.set_range(0, 10, 255, 0, 0, 0)
# All pixels
led.set_all(0, 0, 0, 100) # all white (W channel)
led.clear_all() # all off
# Stream (fastest for animations — sends all 107 pixels at once)
pixels = [(r, g, b, w)] * 107
led.stream_update(pixels)
# Buffer mode (atomic update — no partial flicker)
led.buffer_begin()
for i, (r, g, b, w) in enumerate(pixels):
led.buffer_set_pixel(i, r, g, b, w)
led.buffer_end()
# Hardware gradient (computed on Arduino)
led.fill_gradient(0, 106, 255, 0, 0, 0, 0, 0, 255, 0)
# Global brightness
led.set_brightness(128) # 50%
# Utility
led.ping()
print(led.get_info()) # {'protocol_version': 11, 'led_count': 107, ...}
# Colour helpers
r, g, b = led.hsv_to_rgb(180, 1.0, 0.5)| Method | FPS | Best used for |
|---|---|---|
stream_update() |
16–21 | Full-strip animations |
buffer_begin/end() |
~16 | Atomic scale display |
set_range() |
instant | Solid-colour regions |
fill_gradient() |
instant | Hardware gradients |
set_all() |
instant | Whole strip one colour |
Tips:
- Use
stream_update()for animations, notset_pixel()in a loop. - Use
buffer_begin/end()when you need a flicker-free update (e.g. scale display). - Use
set_brightness()globally instead of scaling each colour value manually.
| Problem | Solution |
|---|---|
| Arduino not connecting | Check USB cable, port name, driver. Windows: check Device Manager. |
| LEDs not lighting | External PSU required. Verify common GND. Use Pin 6. SK6812, not WS2812. |
| Brightness not working | Call set_brightness() before set_all(). |
| Port already in use | python release_port.py COM5 |
| Low FPS | 16 FPS is normal at 115200 baud. Use stream_update() for best throughput. |
| Random LED 0 lights up | Caused by reset_input_buffer() — already fixed in controller_api.py. |
| Write timeout | Concurrent OSC messages — already handled by Lock in osc_bridge.py. |
- Copy
DreamScaler.control.jsandscales_data.jsto your Bitwig controller scripts folder. - In Bitwig: Settings → Controllers → Add controller → Generic → DreamScaler.
- Start
osc_bridge.py(orlauncher.py --mode osc). - Change root note or scale type in Bitwig — the LED strip updates immediately.
The controller script sends:
OSC /dreamscaler/scale rootIndex(0–11) scaleName(string)
to 127.0.0.1:9001 by default.
Debounce: Bitwig sends two separate OSC messages (root note + scale type) in quick succession. The JS side debounces them with a 200 ms token pattern so only the final state is sent.
Per-project settings: Scale settings (Root Note, Scale Type, Scale Display, LED Intensity) are stored via Bitwig's documentState API and saved inside each .bwproject file. Every project remembers its own scale configuration independently. The Language preference is global (stored via host.getPreferences()) and shared across all projects.
If you edit scales.json, run python generate_scales_js.py before copying DreamScaler.control.js into Bitwig so the controller script and scale reference stay in sync.
Made with ❤️
