Ollin

Record

a run recorded as it plays: R starts and stops a real-time take of picture and sound together, and the take is stamped with the wall clock, so a slow frame lasts longer instead of stretching time

Run
swift run --package-path Examples Example-Export-Record
Imports
OllinAudio
Source
Examples/Export/Record/Sketch.swift
import Ollin
import OllinAudio

/// Record a run while you play it.
///
/// The offline exporters re-render a sketch on a fixed clock, which is how a
/// file reproduces exactly. A performance is the opposite kind of thing: the
/// mouse, the parameters, and the sound happen once, in real time. This sketch
/// keeps that once. Press **R** and play; press **R** again and the take is a
/// movie in `~/Movies/Ollin/`, picture and sound together, stamped with the
/// wall clock so a slow frame lasts longer instead of stretching time.
///
/// The sound needs no wiring: the recorder finds the instruments the sketch
/// is holding, the same way the offline exporters do. In the live hosts the
/// same recording is on ⌘⇧R, and there it even survives an evaluate.
///
/// A movie keeps the pixels; a *take* keeps the performance itself. Run with
/// `--record-take take.json` to write the seed, clock, pointer, and parameter
/// timeline down, then `--replay take.json` to play the run back exactly
/// (space pauses, the arrows step), or `--replay take.json --export-video`
/// to re-render the performance offline. See Docs/Core/Replay.md.
@main
final class Record: Sketch {
    override var canvasSize: CanvasSize { .square(900) }

    let strings = Synth(.nylon, polyphony: 8)
    let tuning = Scale(.minorPentatonic, root: "A2")

    var ripples: [(x: Double, y: Double, born: Double)] = []
    var nextPluck = 0.0

    override func setup() {
        strings.gain = 0.6
        strings.reverb = Reverb(.hall, mix: 0.3)
        noStroke()
    }

    override func draw() {
        background(Color(hex: 0x101318))

        // A quiet pulse of its own, so a hands-off take still has a heartbeat.
        if time >= nextPluck {
            nextPluck = time + 1.5
            strings.play(tuning[0], velocity: 0.35, for: 1.2)
            ripples.append((width / 2, height / 2, time))
        }

        // Dragging plays: height picks the note, the ripple marks it.
        if mouseIsPressed, time >= lastStroke + 0.12 {
            lastStroke = time
            let step = Int((1 - mouseY / height) * 9)
            strings.play(tuning[step], velocity: 0.8, for: 0.8)
            ripples.append((mouseX, mouseY, time))
        }

        ripples.removeAll { time - $0.born > 2 }
        for ripple in ripples {
            let age = time - ripple.born
            let radius = 20 + age * 150
            fill(Color(hue: 0.55 + age * 0.1, saturation: 0.5, brightness: 0.9,
                       alpha: (1 - age / 2) * 0.8))
            drawRing(ripple.x, ripple.y, radius, radius + 4)
        }

        // The level, drawn: the same loudness the recording is hearing.
        fill(Color(white: 0.9, alpha: 0.8))
        let level = Double(strings.amplitude) * 500
        drawRect(width / 2 - level / 2, height - 60, level, 6)
    }

    override func keyPressed() {
        guard key == "r" || key == "R" else { return }
        if isRecording {
            stopRecording()
        } else {
            startRecording()
        }
    }

    private var lastStroke = 0.0
}

↑↓ move ↩ open