Seraph – Open-Source Teensy MIDI Controller Platform
An open-source platform for building USB-MIDI controllers and sensor-based interactive art projects. This repository provides a PCB design and sample demo code to help you build and customize your own MIDI devices.
Getting Started Guide
What You'll Learn
- 1 — Board Overview & Hardware Setup
- 2 — First Wiring Tutorial: Button & LED
- 3 — Analog Sensors: Potentiometers & FSRs
- 4 — I²C Devices: IMUs, Displays & More
- 5 — MIDI Mapping in Code
- 6 — Connecting to a DAW
Seraph is a breakout board for the Teensy 4.1 microcontroller. It handles all the messy wiring, power regulation, and I/O routing so you can focus on building your instrument or installation — not on debugging circuits.
Seraph uses the Arduino IDE with the Teensyduino add-on, which gives your computer the tools to compile code and talk to the Teensy 4.1.
-
Download Arduino IDE from arduino.cc/en/software
-
Download Teensyduino from pjrc.com/teensy/teensyduino.html
-
Run the Teensyduino installer — it will find your Arduino IDE automatically
-
Open Arduino IDE. You should now see Teensy boards in Tools > Board > Teensyduino
-
Solder male header pins to the Teensy 4.1 (if not pre-soldered)
-
Press the Teensy firmly into the MCU socket on the Seraph board — the USB port should face toward the board edge
-
Double-check alignment: all pins seated, none bent
This is the most important setting. Without it, your computer won't recognise the Teensy as a MIDI device.
-
In Arduino IDE, go to Tools > Board and select Teensy 4.1
-
Go to Tools > USB Type and select Serial + MIDI
-
Connect the Teensy to your computer via USB
-
You should see it appear in your system as both a serial port and a MIDI device
💡 On macOS, check Audio MIDI Setup (Applications > Utilities) to confirm the device appears. On Windows, check Device Manager.
This is your first circuit. You'll wire a push button and an LED to Seraph, then upload code that lights the LED when the button is pressed and sends a MIDI Note On message to your computer.
-
Seraph board with Teensy 4.1 mounted
-
1x tactile push button (momentary)
-
1x LED (any color, 3mm or 5mm)
-
1x 470Ω resistor (for LED current limiting)
-
Jumper wires

Create a new sketch in Arduino IDE and paste in the following code. This is adapted from the Seraph_ButtonDemo in the GitHub repository.
// Seraph — Button + LED Demo
// Digital Channel 1 = Button input
// Digital Channel 2 = LED output
const int BUTTON_PIN = 1; // Change to match your wiring
const int LED_PIN = 2; // Change to match your wiring
const int MIDI_CHANNEL = 1; // MIDI channel 1
const int MIDI_NOTE = 60; // Middle C
const int MIDI_VELOCITY = 100;
int lastButtonState = HIGH; // Buttons default HIGH (internal pullup)
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP); // Enable internal pullup resistor
pinMode(LED_PIN, OUTPUT);
}
void loop() {
int buttonState = digitalRead(BUTTON_PIN);
// Button pressed (LOW because of pullup)
if (buttonState == LOW && lastButtonState == HIGH) {
digitalWrite(LED_PIN, HIGH); // Turn LED on
usbMIDI.sendNoteOn(MIDI_NOTE, MIDI_VELOCITY, MIDI_CHANNEL);
}
// Button released
if (buttonState == HIGH && lastButtonState == LOW) {
digitalWrite(LED_PIN, LOW); // Turn LED off
usbMIDI.sendNoteOff(MIDI_NOTE, 0, MIDI_CHANNEL);
}
lastButtonState = buttonState;
// Always flush MIDI at the end of loop()
while (usbMIDI.read()) {}
}Key Concepts
When you use INPUT_PULLUP, the Teensy internally connects the pin to 3.3V through a resistor. This means the pin reads HIGH when nothing is connected, and LOW when the button pulls it to ground. This is why you only need two wires for a button — no external resistor needed.
Notice the code only sends MIDI when the button state changes — not every loop iteration. This is called event-based transmission, and it's important: sending MIDI every millisecond would flood the connection with redundant messages. Always track the previous state and only transmit on transitions.
The line while (usbMIDI.read()) {} at the end of loop() flushes any incoming MIDI data. Even if you're not receiving MIDI, the USB library needs this to maintain a stable connection. Always include it.
-
Select Tools > USB Type > Serial + MIDI in Arduino IDE
-
Upload the sketch (Ctrl+U / Cmd+U)
-
Open a MIDI monitor app (e.g. MIDI Monitor on macOS, MIDI-OX on Windows) to confirm notes are being sent
-
Press the button — the LED should light and you should see a Note On message for Middle C
💡 If nothing happens, check that the Teensy appears as a MIDI device in your system, and that your pin numbers in the code match where you physically wired the components.
Analog sensors output a continuously varying voltage rather than just on/off. Seraph's analog bank reads these voltages and converts them to numbers your code can use. This section covers the two most common types: potentiometers (knobs) and FSRs (force-sensitive resistors).
Potentiometers are the simplest analog sensors — they're essentially volume knobs. They have three leads: power, ground, and a wiper that outputs a voltage proportional to rotation.


// Seraph — Potentiometer Demo
// Maps pot position to MIDI CC (Control Change)
const int POT_PIN = A1; // Analog channel 1
const int MIDI_CC = 74; // CC 74 = Filter Cutoff (common choice)
const int MIDI_CHANNEL = 1;
int lastCCValue = -1; // Track last sent value to avoid redundant messages
void setup() {
// No pinMode needed for analog inputs
}
void loop() {
int rawValue = analogRead(POT_PIN); // 0-1023
// Map 10-bit ADC range to 7-bit MIDI CC range (0-127)
int ccValue = map(rawValue, 0, 1023, 0, 127);
// Only send if value has changed
if (ccValue != lastCCValue) {
usbMIDI.sendControlChange(MIDI_CC, ccValue, MIDI_CHANNEL);
lastCCValue = ccValue;
}
while (usbMIDI.read()) {}
}Force-Sensitive Resistors (FSRs) and Light-Dependent Resistors (LDRs) change their resistance based on physical input. Unlike potentiometers, they only have two leads — you need to add a pulldown resistor to create the voltage divider that makes them readable.
An FSR is just a variable resistor. To read it as a voltage, you pair it with a fixed resistor to form a voltage divider. Seraph has a resistor footprint on each analog channel strip for exactly this purpose — no breadboard required.

// Seraph — FSR Demo
// FSR maps pressure to MIDI velocity / aftertouch
const int FSR_PIN = A2;
const int MIDI_CHANNEL = 1;
// Calibration — adjust these to match your sensor's actual range
const int FSR_MIN = 50; // Value when barely touched
const int FSR_MAX = 900; // Value at maximum pressure
int lastCCValue = -1;
void loop() {
int rawValue = analogRead(FSR_PIN);
int ccValue = map(
constrain(rawValue, FSR_MIN, FSR_MAX),
FSR_MIN,
FSR_MAX,
0,
127
);
if (ccValue != lastCCValue) {
// CC 11 = Expression — works well for pressure control
usbMIDI.sendControlChange(11, ccValue, MIDI_CHANNEL);
lastCCValue = ccValue;
}
while (usbMIDI.read()) {}
}💡 Use constrain() before map() when your sensor's real-world range doesn't match the theoretical 0–1023. This prevents the mapped value from going outside 0–127.
I²C (Inter-Integrated Circuit) is a communication protocol that lets you connect multiple sensors to just two wires: SDA (data) and SCL (clock). Seraph exposes three I²C blocks (A, B, C), all defaulting to the Teensy's I2C0 port for maximum compatibility.
-
IMU (MPU-6050, BNO055) — 6-axis or 9-axis motion sensing for gesture control
-
OLED Display (SSD1306) — small screen for parameter feedback
-
ADC Expander (ADS1115) — adds 4 extra high-resolution analog inputs
-
Distance Sensor (VL53L0X) — time-of-flight distance measuring
-
Color Sensor (TCS34725) — reads RGB color values

| Component Pin | → | Wire Color | → | Seraph Header |
| Sensor — SDA | → | Blue | → | I²C Block A — SDA |
| Sensor — SCL | → | Yellow | → | I²C Block A — SCL |
| Sensor — VCC | → | Red | → | I²C Block A — Power (+) |
| Sensor — GND | → | Black | → | I²C Block A — GND (−) |
💡 If your sensor has additional pins (INT, ADDR, etc.) beyond the four core I²C pins, refer to your sensor's datasheet. ADDR pins often let you change the I²C address to avoid conflicts when using multiple devices of the same type.
Multiple I²C devices can share the same two wires as long as each has a unique address. Seraph's three I²C blocks (A, B, C) are all wired to the same I2C0 bus — they're just convenient connection points, not separate buses.
-
Connecting two MPU-6050s: bridge the ADDR pin on one to 3.3V to change its address from 0x68 to 0x69
-
Connecting an OLED + IMU: different device types usually have different addresses by default — no configuration needed
Install the Adafruit MPU6050 library via Tools > Manage Libraries in Arduino IDE before uploading this sketch.
// Seraph — IMU (MPU-6050) to MIDI Pitch Bend Demo
#include "Wire.h"
#include "Adafruit_MPU6050.h"
Adafruit_MPU6050 mpu;
const int MIDI_CHANNEL = 1;
void setup() {
Wire.begin();
mpu.begin();
mpu.setAccelerometerRange(MPU6050_RANGE_2_G);
mpu.setGyroRange(MPU6050_RANGE_250_DEG);
}
void loop() {
sensors_event_t accel, gyro, temp;
mpu.getEvent(&accel, &gyro, &temp);
// Map X-axis tilt (-10 to +10 m/s^2)
// to MIDI pitch bend (-8192 to +8191)
int pitchBend = map(
constrain(accel.acceleration.x * 100, -1000, 1000),
-1000,
1000,
-8192,
8191
);
usbMIDI.sendPitchBend(pitchBend, MIDI_CHANNEL);
delay(10); // ~100 updates/second
while (usbMIDI.read()) {}
}This section explains the core MIDI message types you'll use in Seraph projects and how to send them using the Teensyduino USB-MIDI library. Understanding these building blocks lets you map any sensor to any parameter in your DAW or audio software.
These are the most commonly used usbMIDI functions in Seraph projects:
// Note On — play a note
usbMIDI.sendNoteOn(note, velocity, channel);
// note: 0–127 (60 = Middle C)
// velocity: 0–127 (0 = silent, 127 = max)
// channel: 1–16
// Note Off — stop a note
usbMIDI.sendNoteOff(note, 0, channel);
// Control Change — continuous parameter
usbMIDI.sendControlChange(cc_number, value, channel);
// cc_number: 0–127 (see MIDI CC list below)
// value: 0–127
// Pitch Bend — continuous pitch movement
usbMIDI.sendPitchBend(value, channel);
// value: -8192 to +8191
// 0 = center / no bend
// Program Change — switch patches
usbMIDI.sendProgramChange(program, channel);
// program: 0–127
// Always flush MIDI messages at end of loop()
while (usbMIDI.read()) {}CC numbers are standardized. Here are the ones most useful for NIME and music production:
Real Seraph projects typically combine several sensors sending different MIDI messages. Here's the structure to follow:
// Multi-sensor Seraph template
const int POT_PIN = A1;
const int FSR_PIN = A2;
const int BUTTON_PIN = 1;
int lastPotCC = -1;
int lastFSRCC = -1;
int lastBtn = HIGH;
void setup() {
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
// --- Potentiometer -> CC 74 (Filter) ---
int potCC = map(
analogRead(POT_PIN),
0,
1023,
0,
127
);
if (potCC != lastPotCC) {
usbMIDI.sendControlChange(74, potCC, 1);
lastPotCC = potCC;
}
// --- FSR -> CC 11 (Expression) ---
int fsrCC = map(
constrain(analogRead(FSR_PIN), 50, 900),
50,
900,
0,
127
);
if (fsrCC != lastFSRCC) {
usbMIDI.sendControlChange(11, fsrCC, 1);
lastFSRCC = fsrCC;
}
// --- Button -> Note On/Off ---
int btn = digitalRead(BUTTON_PIN);
if (btn == LOW && lastBtn == HIGH) {
usbMIDI.sendNoteOn(60, 100, 1);
}
if (btn == HIGH && lastBtn == LOW) {
usbMIDI.sendNoteOff(60, 0, 1);
}
lastBtn = btn;
// Always flush MIDI messages
while (usbMIDI.read()) {}
}Once your Seraph is sending MIDI, the final step is getting that data into your DAW or audio software. This section covers Ableton Live, Max/MSP, and general MIDI routing — the three most common environments in NIME and music production contexts.
-
Go to Live > Preferences (Mac) or Options > Preferences (PC)
-
Click the Link / Tempo / MIDI tab
-
Find your Teensy in the MIDI Ports list — it will appear as something like "Teensy MIDI"
-
Turn ON Track under the Input column for your Teensy
-
Optionally turn ON Remote if you want to use it for Live's remote control
💡 If your Teensy doesn't appear, check that USB Type is set to Serial + MIDI in Arduino IDE and that the sketch is running (the board is powered and the code is uploaded).
-
Enter MIDI Map Mode (Cmd+M on Mac, Ctrl+M on PC) — everything mappable turns blue
-
Click the parameter you want to control (e.g. a filter knob on an instrument)
-
Turn your potentiometer on Seraph — Live will detect the CC and assign it
-
Exit MIDI Map Mode (Cmd+M / Ctrl+M again)
💡 In MIDI Map Mode you can also set min/max ranges for each mapping. This lets you limit a potentiometer to only sweep part of a parameter's range.
-
Enter MIDI Map Mode
-
Click a clip slot in Session View
-
Press your button on Seraph — Live assigns it
-
Exit MIDI Map Mode
Max/MSP is the most common environment for custom interactive music systems. The Lumaphone and H.A.I. projects from the Seraph case studies both used Max for audio processing.
Use the midiin or notein / ctlin objects to receive MIDI from Seraph:
// In a Max patch, create these objects:
[notein] // receives Note On/Off from any MIDI device
[ctlin] // receives Control Change messages
[bendin] // receives Pitch Bend
// To receive from Seraph specifically:
[notein Teensy MIDI] // specify device name in the argument
[ctlin Teensy MIDI]
// Example: map CC 74 to a filter cutoff
[ctlin 74 Teensy MIDI] // only listens to CC 74 from Teensy
|
[/ 127.] // normalize 0-127 to 0.0-1.0
|
[cutoff~] // connect to your audio object💡 If Max doesn't list your Teensy as a MIDI device, go to Max > Preferences > MIDI and click Refresh. Make sure the Teensy is plugged in and the sketch is running.
Seraph defaults to USB-MIDI, but some advanced Seraph projects (like the Lumaphone) communicate over serial instead, sending raw data rather than formatted MIDI messages. For USB-MIDI, use notein/ctlin as above. For serial, use the serial object and parse the data manually.
Any DAW that accepts MIDI input will work with Seraph. The general process is the same across tools:
| DAW / Software | Where to Enable MIDI |
|---|---|
| Ableton Live | Preferences → MIDI → Enable Track Input for Teensy |
| Logic Pro | File → Project Settings → MIDI → Input Devices |
| Bitwig Studio | Preferences → Controllers → Add Controller → Generic MIDI |
| Reaper | Options → Preferences → MIDI Devices → Enable Teensy Input |
| GarageBand | Auto-detects USB MIDI — just connect and play |
| Pure Data | Use [notein] / [ctlin] objects (same as Max/MSP) |
| SuperCollider | MIDIClient.init; → MIDIIn.connectAll; → MIDIdef.cc(...) |
-
MIDI device not appearing: Confirm USB Type is Serial + MIDI in Arduino IDE and re-upload the sketch
-
Notes stuck on: Add usbMIDI.sendNoteOff() for every Note On — always pair them
-
CC values jumpy or noisy: Add smoothing in your sketch (see below), or adjust the sensor's pulldown resistor value
-
Wrong channel: Make sure the MIDI channel in your sketch matches what the DAW is listening on
// Exponential moving average — reduces sensor noise
float smoothed = 0;
const float ALPHA = 0.1;
// Lower = smoother, less responsive
// Higher = faster response, more jitter
void loop() {
float raw = analogRead(A1);
// Smooth incoming sensor data
smoothed = ALPHA * raw + (1.0 - ALPHA) * smoothed;
// Convert analog range (0-1023)
// into MIDI range (0-127)
int ccValue = map(
(int)smoothed,
0,
1023,
0,
127
);
// Send MIDI as normal
// usbMIDI.sendControlChange(CC, ccValue, CHANNEL);
while (usbMIDI.read()) {}
}You're ready to build.
| # | Component | SKU / MPN | Qty | Supplier |
|---|---|---|---|---|
| 1 | Teensy 4.1 — ARM Cortex-M7 @ 600 MHz, 8MB Flash, 1MB RAM | DEV-16771 | 1 | SparkFun |
| 2 | ADXL335 Triple-Axis Accelerometer Breakout — ±3g analog output | SEN-09269 | 1 | Adafruit |
| 24 | STMicroelectronics ULN2003A — 7-ch Darlington Array DIP-16 | 497-2344-5-ND | 1 | DigiKey |
| 4 | Male Pin Headers — 2.54mm straight breakaway 40-pin strip | PRT-00116 | 3 | SparkFun |
| 20 | Molex 3-Pin Vertical Header 2.54mm | WM4201-ND | 16 | DigiKey |
| 21 | Molex 2-Pin Vertical Header 2.54mm | 900-0022232021-ND | 5 | DigiKey |
| 22 | Molex 4-Pin Vertical Header 2.54mm (I²C Blocks A/B/C) | WM4202-ND | 3 | DigiKey |
| 23 | Sullins 24-Pin Female Socket Header (Teensy Socket) | S7057-ND | 2 | DigiKey |
| 25 | TE Connectivity 16-Pin DIP IC Socket (ULN2003A) | A120349-ND | 1 | DigiKey |
| 26 | Molex KK 2-Pin Pre-Crimped Wire Assembly 300mm | 900-2177971022-ND | 5 | DigiKey |
| 27 | Molex KK 3-Pin Pre-Crimped Wire Assembly 300mm | 900-2177971032-ND | 8 | DigiKey |
| 28 | Molex KK 4-Pin Pre-Crimped Wire Assembly 300mm | 900-2177971042-ND | 1 | DigiKey |
| 7 | 10kΩ Resistors — 1/4W Through-Hole (100 pack) | CF14JT10K0CT-ND | 1 | DigiKey |
| 8 | 220Ω Resistors — 1/4W Through-Hole (100 pack) | CF14JT220RCT-ND | 1 | DigiKey |
| 9 | Push Buttons — 12mm Momentary | COM-09190 | 10 | SparkFun |
| 10 | Rotary Potentiometers — 10kΩ Linear Taper | COM-09939 | 8 | SparkFun |
| 11 | Force Sensitive Resistors — FSR 402 | SEN-09375 | 4 | SparkFun |
| 12 | LEDs — 5mm Assorted Colors | COM-12062 | 1 | SparkFun |
| 13 | MPU-6050 IMU — Gyro + Accelerometer (I²C) | SEN-11028 | 1 | SparkFun |
| 14 | SSD1306 OLED Display — 0.96" 128×64 I²C | LCD-17153 | 1 | SparkFun |
| 15 | TCS34725 RGB Color Sensor (I²C) | SEN-12829 | 1 | SparkFun |
| 16 | VL53L0X Time-of-Flight Distance Sensor (I²C) | SEN-14032 | 1 | SparkFun |
| 17 | Lead-Free Rosin Core Solder | TOL-09325 | 1 | SparkFun |
| 18 | Jumper Wires — Female-to-Female 6" | PRT-12796 | 2 | SparkFun |
| 19 | USB Micro-B Cable | CAB-13244 | 1 | SparkFun |
Creative Computing at California Institute of the Arts is a forward-thinking interdisciplinary program that fuses the power of computational engineering skills with the limitless possibilities of artistic expression. This innovative degree encourages students to explore the intersection of technology and creativity, using computational tools to craft work that is both personally and culturally meaningful, while preparing them for industry. Our program is designed to provide an integrative learning experience that equips students with the skills to push the boundaries of art, music, and technology. With a strong foundation in computer science, electrical engineering, signal processing, and emerging technologies including virtual/augmented reality, robotics, and machine learning, students will be empowered to innovate, experiment, and reimagine what technology can do in artistic contexts.








