SoundInAnExport
a piece whose sound goes into the export: the same score is rendered offline into the exported video rather than played to the room
- Run
swift run --package-path Examples Example-Audio-SoundInAnExport- Imports
OllinAudio- Source
- Examples/Audio/SoundInAnExport/Sketch.swift
import Ollin
import OllinAudio
/// A piece that carries its own sound out of the window.
///
/// Everything else in this folder makes sound you have to be there to hear.
/// This one exports:
///
/// ```sh
/// swift run --package-path Examples Example-Audio-SoundInAnExport --export-video piece.mp4 --frames 480
/// ```
///
/// and the file has the music in it. There is no recording step. The exporters
/// drive a sketch on a fixed clock with nothing playing, so the notes it asks
/// for are written down instead, and the soundtrack is rendered at the end
/// through the same code that would have fed the speakers. That works because
/// the renderer takes events and gives back samples and has no clock of its
/// own: an export is that same code with the waiting taken out.
///
/// Which also means the file reproduces. Export it twice and you get the same
/// music, because the notes come from a seeded chain over a Euclidean rhythm
/// and nothing here reads a clock it does not own.
@main
final class SoundInAnExport: Sketch {
override var canvasSize: CanvasSize { .square(720) }
/// Eight bars at 96 beats a minute, so `--export-loop` closes cleanly.
override var loopDuration: Double? { tempo.seconds(bars: 8) }
let tempo: Tempo = 96
let steps = 16
let bass = Synth(.nylon, polyphony: 6)
let bells = Synth(.chime, polyphony: 10)
let pulse = Rhythm(3, in: 16)
let sparkle = Rhythm(7, in: 16)
let tuning = Scale(.minorPentatonic, root: "A2")
var counter = StepCounter(perBeat: 4)
var melody = MarkovChain<Int>(seed: 9)
var marks: [(x: Double, y: Double, start: Double, tone: Double, big: Bool)] = []
override func setup() {
seed(4321)
noStroke()
bass.gain = 0.5
bells.gain = 1
bells.reverb = Reverb(.hall, mix: 0.32)
melody.learn([0, 2, 4, 2, 0, -3, 0, 5], loops: true)
melody.start(at: 0)
}
override func draw() {
background(Color(hex: 0x0D0F14))
for step in counter.steps(upTo: tempo.beats(at: time)) {
play(step)
}
marks.removeAll { time - $0.start > 2.4 }
blendMode(.add)
for mark in marks {
let age = (time - mark.start) / 2.4
let radius = (mark.big ? 26.0 : 9.0) * (1 + age * 2.6) * scale
fill(Colormap.magma.color(at: 0.3 + mark.tone * 0.55)
.withAlpha(pow(1 - age, 1.6) * 0.8))
drawCircle(mark.x, mark.y, radius)
}
blendMode(.normal)
drawCaption("Export it and the file has the music in it.")
}
private func play(_ step: Int) {
let across = Double(width)
let down = Double(height)
if pulse[step] {
let degree = melody.next() ?? 0
bass.play(tuning[degree], velocity: 0.9, for: 0.5)
marks.append((x: across * 0.5 + Double(degree) * across * 0.055,
y: down * 0.66, start: time,
tone: Double((degree + 6) % 8) / 8, big: true))
}
if sparkle[step] {
let inBar = ((step % steps) + steps) % steps
let pitch = tuning[6 + inBar % 5]
bells.play(pitch, velocity: 0.5, for: 1.6)
marks.append((x: across * (0.12 + Double(inBar) / Double(steps) * 0.76),
y: down * 0.34, start: time,
tone: (pitch.midi - 45) / 40, big: false))
}
}
}