MIDILoopback
a self-contained loop: a virtual source sends stepped CC to itself, and a bound, smoothed @Param glides toward each step, so it runs with no hardware (MIDIOutput.openVirtual, MIDIInput.bind, @Param smoothing)
- Run
swift run --package-path Examples Example-Integration-MIDILoopback- Imports
OllinMIDI- Source
- Examples/Integration/MIDILoopback/Sketch.swift
import Ollin
import OllinMIDI
/// A self-contained MIDI round trip — no hardware needed. The sketch opens a
/// *virtual source* and sends control-change messages to itself; a `MIDIInput`
/// connected to that source reads them back, and the picture is what it receives.
///
/// swift run Example-Integration-MIDILoopback
///
/// It also shows the softening a `@Param` can carry: the incoming value steps
/// (like a parameter jumped to a new position every so often), but `radius` is bound to
/// a smoothed param, so the filled circle *glides* toward each new step while the
/// thin ring marks the raw target it's chasing. The companion `MIDIMonitor`
/// example is the one for real gear. See `Docs/Integration/MIDI.md`.
@main
final class MIDILoopback: Sketch {
let out = MIDIOutput()
lazy var midi = MIDIInput()
// Bound to incoming CC 1. The smoothing is what makes the stepped input glide.
@Param(40...400, smoothing: .eased(0.45)) var radius = 120.0
var rawValue = 64 // last value sent/received, 0…127
var nextStepFrame = 0
override func setup() {
// Open the virtual source first, then connect the input to it.
do {
try out.openVirtual(named: "Ollin Loopback")
try midi.start()
} catch {
print("could not open MIDI loopback: \(error)")
}
midi.bind(controlChange: 1, to: $radius) // CC 0…127 → 40…400, glided
noStroke()
}
override func draw() {
background(Color(white: 0.08))
// Every ~0.75s, "adjust the parameter" to a new spot and send it to ourselves.
if frameCount >= nextStepFrame {
rawValue = Int(random(0, 127))
out.controlChange(1, value: rawValue)
nextStepFrame = frameCount + 45
}
// What the input actually received (drives the cache + the bound param).
let received = midi.controlValue(1, default: rawValue)
let targetRadius = map(Double(received), 0, 127, 40, 400) * scale
// The thin ring is the raw target; the filled circle is the smoothed param
// gliding toward it — the gap between them is the softening in flight.
let center = center
stroke(Color(white: 0.35))
strokeWeight(2 * scale)
noFill()
drawCircle(center: center, radius: targetRadius)
noStroke()
fill(Color(red: 0.55, green: 0.5, blue: 0.95))
drawCircle(center: center, radius: radius * scale)
// Labels.
fill(Color(white: 0.95))
textSize(30 * scale)
drawText("MIDI loopback — sending CC 1 to myself", 40 * scale, 70 * scale)
fill(Color(white: 0.55))
textSize(20 * scale)
drawText("raw \(received) → radius \(Int(radius)) (smoothed)", 40 * scale, 105 * scale)
}
}