Bowing
a note you keep playing: a bowed string or a blown tube. Both are worked out continuously, so the sound follows the hand for as long as it lasts instead of being decided at its start (BowedString, BlownTube)
- Run
swift run --package-path Examples Example-Audio-Bowing- Imports
OllinAudio- Source
- Examples/Audio/Bowing/Sketch.swift
import Ollin
import OllinAudio
/// A note you keep playing.
///
/// Every other instrument here is set going and then let go: a string is
/// plucked, a shape is struck, and the whole note is decided at its start. A
/// bow and a breath are not like that. They go on happening, so a note has a
/// middle, and the middle is yours.
///
/// Hold the mouse down to play. Moving it up and down is the drive: how fast
/// the bow is drawn, or how hard the tube is blown. Let go and the note stops,
/// because you stopped. Moving left and right changes the note.
///
/// Press `B` to swap the bow for a reed. Watch the ring while you play: it is
/// the sound the sketch is making, read back off its own playing.
@main
final class Bowing: Sketch {
enum Instrument: String, ParamOption { case bowed, blown }
@Param(icon: "hand.draw", group: "Playing") var instrument = Instrument.bowed
@Param(0.02 ... 0.5, icon: "arrow.left.and.right", group: "Bow") var position = 0.16
@Param(0 ... 1, icon: "arrow.down.circle", group: "Bow") var force = 0.6
@Param(0 ... 1, icon: "mouth", group: "Reed") var embouchure = 0.45
let synth = Synth(.cello, polyphony: 4)
let tuning = Scale(.minorPentatonic, root: "D2")
var playing: Pitch?
var trail: [Double] = []
var lastBuilt = Instrument.bowed
override func setup() {
synth.gain = 0.7
synth.reverb = Reverb(.hall, mix: 0.24)
synth.pressure = 0
rebuild()
}
/// The whole difference between the two, in one call each.
private func rebuild() {
switch instrument {
case .bowed:
synth.voice = Voice(bowed: BowedString(position: position, force: force,
decay: 2.2, damping: 0.45),
gain: 0.75)
case .blown:
synth.voice = Voice(blown: BlownTube(embouchure: embouchure, breathiness: 0.14,
decay: 0.17, damping: 0.42),
gain: 0.62)
}
lastBuilt = instrument
}
override func draw() {
background(Color(hex: 0x0C0E13))
if instrument != lastBuilt { rebuild() }
// Up the window is a faster bow or a harder breath. This is read every
// frame while the note sounds, which is the thing an envelope cannot do.
let wanted = mouseIsPressed ? map(mouseY, height, 0, 0, 1) : 0
synth.pressure = clamp(wanted, 0, 1)
if mouseIsPressed {
let degree = Int(map(mouseX, 0, width, 0, 12))
let pitch = tuning[degree]
if playing != pitch {
if playing != nil { synth.allNotesOff() }
synth.noteOn(pitch, velocity: 0.85)
playing = pitch
}
} else if playing != nil {
synth.allNotesOff()
playing = nil
}
drawInstrument()
drawCaption("Hold to play. Up and down is the bow speed, or the breath.", edge: .top)
drawCaption(instrument == .bowed
? "A bowed string: it sounds for as long as the bow keeps moving."
: "A stopped tube: only the odd harmonics, which is why it is hollow.")
}
private func drawInstrument() {
let middle = center
let level = Double(synth.amplitude)
trail.append(level)
if trail.count > 260 { trail.removeFirst() }
// What is actually sounding, read back off the sketch's own playing.
noFill()
stroke(Color(hex: 0x7FD4C1, alpha: 0.5))
strokeWeight(2 * scale)
let radius = (150 + level * 700) * scale
drawCircle(middle.x, middle.y, radius)
// The drive, as a wedge: how hard the player is leaning on it now.
stroke(Color(hex: 0xE8A33D, alpha: 0.8))
strokeWeight(6 * scale)
let start: Double = -Double.pi / 2
let sweep: Double = synth.pressure * Double.tau * 0.999
let ring = 120 * scale
drawArc(center: middle, radiusX: ring, radiusY: ring,
start: start, stop: start + sweep, mode: .open)
// The last few seconds of what came out, so a held note visibly has a
// middle rather than only an attack.
noFill()
stroke(Color(hex: 0x6FA8DC, alpha: 0.75))
strokeWeight(2 * scale)
let frame = Rectangle(x: width * 0.12, y: height * 0.78,
width: width * 0.76, height: height * 0.12)
if trail.count > 2 {
drawPolyline(trail.enumerated().map { index, value in
Vector2(frame.x + frame.width * Double(index) / Double(max(1, trail.count - 1)),
frame.y + frame.height * (1 - min(value * 5, 1)))
})
}
if let playing {
noStroke()
fill(Color(white: 0.8))
textSize(22 * scale)
textAlign(.center)
drawText("\(playing)", at: Vector2(middle.x, middle.y + 8 * scale))
}
}
override func keyPressed() {
guard key == "b" || key == "B" else { return }
instrument = instrument == .bowed ? .blown : .bowed
}
}