Files
luma/Sources/LumaCore/Audio/SynthEngine.swift
Ole André Vadla Ravnås f8d25015ae Drop the comments that only restate the code
A comment is a last resort, and these were not one: section banners,
doc headers naming what the declaration below already names, and
notes saying in prose what the next line says in code.

What is left is what the code cannot show -- an ordering that matters,
a platform quirk, why the obvious way fails.
2026-08-13 13:09:18 +02:00

660 lines
26 KiB
Swift

import Foundation
import Synchronization
/// The mixer, running on the audio device's own thread.
///
/// Everything it touches is allocated once up front and reached through
/// unsafe pointers, and the control side speaks to it only through a
/// lock-free ring: no allocation, no reference counting and no locking
/// happens under the callback, which is what keeps a phrase from stuttering.
public enum SynthEngine {
public static let channelCount = 8
public static let sampleRate: Float = 48000
static let voiceCount = 24
static let patternCapacity = 256
/// Three per channel, so staging can always find a buffer that is neither
/// being performed nor waiting to be adopted. Two could not: a second
/// commit landing inside one audio block would overwrite the pattern the
/// mixer was midway through.
static let bufferCount = 3
static let maxTones = 4
static let commandCapacity = 256
/// Two seconds at the rate the device runs at, which is as far back as
/// the echo is allowed to reach.
static let echoCapacity = 96000
public static func prepare() {
_ = storage
}
public static func setPatch(_ patch: PatchValues, channel: Int) {
var command = Command(kind: .patch, channel: Int32(channel))
command.patch = patch
offer(command)
}
@discardableResult
public static func play(frequency: Float, velocity: Float, channel: Int) -> Int32 {
let voice = Int32(storage.nextVoice.wrappingAdd(1, ordering: .relaxed).oldValue % UInt32(voiceCount))
var command = Command(kind: .noteOn, channel: Int32(channel))
command.index = voice
command.frequency = frequency
command.velocity = velocity
offer(command)
return voice
}
public static func release(voice: Int32) {
var command = Command(kind: .noteOff, channel: 0)
command.index = voice
offer(command)
}
public static var level: Float {
get { storage.level }
set { storage.level = newValue }
}
public static func beginPattern(channel: Int) {
let performing = performingSlot(channel)
let offered = storage.lastOffered[channel]
storage.staging[channel] = (0..<bufferCount).first { $0 != performing && $0 != offered }!
storage.stagedCount[channel] = 0
}
public static func addStep(frequency: Float, velocity: Float, steps: Int32, channel: Int) {
let count = storage.stagedCount[channel]
guard count < patternCapacity else { return }
let buffer = storage.staging[channel]
storage.steps[stepSlot(channel: channel, buffer: buffer, index: count)] = PatternStep(
velocity: velocity,
steps: max(steps, 1),
toneCount: 1
)
storage.tones[toneSlot(channel: channel, buffer: buffer, index: count, tone: 0)] = frequency
storage.stagedCount[channel] = count + 1
}
public static func addTone(frequency: Float, channel: Int) {
let count = storage.stagedCount[channel]
guard count > 0 else { return }
let buffer = storage.staging[channel]
let index = count - 1
let slot = stepSlot(channel: channel, buffer: buffer, index: index)
let tone = Int(storage.steps[slot].toneCount)
guard tone < maxTones else { return }
storage.tones[toneSlot(channel: channel, buffer: buffer, index: index, tone: tone)] = frequency
storage.steps[slot].toneCount = Int32(tone + 1)
}
/// Offers the staged pattern. The mixer adopts it at the cycle boundary,
/// so retuning a running loop lands musically rather than mid-bar.
public static func commitPattern(stepSeconds: Float, loops: Bool, channel: Int) {
var command = Command(kind: .patternOffer, channel: Int32(channel))
command.index = Int32(storage.staging[channel])
command.count = Int32(storage.stagedCount[channel])
command.stepSeconds = stepSeconds
command.loops = loops ? 1 : 0
offer(command)
storage.lastOffered[channel] = storage.staging[channel]
}
public static func stopPattern(channel: Int) {
offer(Command(kind: .patternStop, channel: Int32(channel)))
}
static func mix(into frames: UnsafeMutablePointer<Float>, frameCount: Int, channels: Int) {
drainCommands()
for frame in 0..<frameCount {
for channel in 0..<channelCount {
advanceSequencer(channel)
}
var left: Float = 0
var right: Float = 0
for index in 0..<voiceCount where storage.voices[index].stage != .idle {
let sample = render(&storage.voices[index])
// Equal power, so a voice does not get louder as it crosses.
let pan = storage.channels[storage.voices[index].channel].pan
let angle = (min(max(pan, -1), 1) + 1) * (.pi / 4)
left += sample * cos(angle)
right += sample * sin(angle)
}
echoed(&left, &right)
left = min(max(left * storage.level, -1), 1)
right = min(max(right * storage.level, -1), 1)
if channels == 1 {
// Both sides into one, at the level a centred voice had.
frames[frame] = min(max((left + right) * 0.70710677, -1), 1)
} else {
for channel in 0..<channels {
frames[frame * channels + channel] = channel % 2 == 0 ? left : right
}
}
}
}
/// One tap of delay, crossing sides as it comes round: what went out
/// left comes back right, which is what makes a tune sound wide rather
/// than merely wet.
private static func echoed(_ left: inout Float, _ right: inout Float) {
guard storage.echoMix > 0, storage.echoFrames > 0 else { return }
let read = (storage.echoWrite &+ echoCapacity &- storage.echoFrames) % echoCapacity
let delayedLeft = storage.echo[read]
let delayedRight = storage.echoRight[read]
// What goes in enters one side only: fed to both, a centred voice
// would come back centred and the tap would never bounce.
storage.echo[storage.echoWrite] = (left + right) * 0.5 + delayedRight * storage.echoFeedback
storage.echoRight[storage.echoWrite] = delayedLeft * storage.echoFeedback
storage.echoWrite = (storage.echoWrite &+ 1) % echoCapacity
left += delayedLeft * storage.echoMix
right += delayedRight * storage.echoMix
}
/// Stops every pattern and takes every voice with it. What each channel
/// is set to is left alone: this is for silence, not for forgetting.
public static func hush() {
offer(Command(kind: .hush, channel: 0))
}
/// Puts a channel back to how it started, or every channel and the rest
/// of the synth when asked for all of them.
public static func reset(channel: Int?) {
offer(Command(kind: .reset, channel: Int32(channel ?? -1)))
}
private static func hushEverything() {
for index in 0..<voiceCount {
storage.voices[index].stage = .idle
storage.voices[index].envelope = 0
}
for index in 0..<channelCount {
storage.channels[index].performing = -1
storage.channels[index].offered = -1
storage.channels[index].stepIndex = 0
storage.channels[index].framesUntilStep = 0
storage.channels[index].sequencedVoice = 0
publishPerforming(index, -1)
}
}
private static func resetChannel(_ channel: Int) {
storage.channels[channel].patch = PatchValues()
storage.channels[channel].pan = 0
storage.channels[channel].performing = -1
storage.channels[channel].offered = -1
storage.channels[channel].stepIndex = 0
storage.channels[channel].framesUntilStep = 0
storage.channels[channel].sequencedVoice = 0
publishPerforming(channel, -1)
for index in 0..<voiceCount where storage.voices[index].channel == channel {
storage.voices[index].stage = .idle
storage.voices[index].envelope = 0
}
}
/// Where a channel sits between the speakers, -1 to 1.
public static func setPan(_ pan: Float, channel: Int) {
var command = Command(kind: .pan, channel: Int32(channel))
command.frequency = pan
offer(command)
}
/// How far back the tap reaches, how much of it comes round again, and
/// how much of it is heard.
public static func setEcho(seconds: Float, feedback: Float, mix: Float) {
var command = Command(kind: .echo, channel: 0)
command.frequency = seconds
command.velocity = feedback
command.stepSeconds = min(max(mix, 0), 1)
offer(command)
}
private static func drainCommands() {
let head = storage.commandHead.load(ordering: .acquiring)
var tail = storage.commandTail.load(ordering: .relaxed)
while tail != head {
let command = storage.commands[Int(tail % UInt32(commandCapacity))]
let channel = Int(command.channel)
switch command.kind {
case .patch:
storage.channels[channel].patch = command.patch
case .noteOn:
start(&storage.voices[Int(command.index)], command.frequency, command.velocity, channel)
case .noteOff:
storage.voices[Int(command.index)].stage = .release
case .patternOffer:
storage.channels[channel].offered = Int(command.index)
storage.channels[channel].offeredCount = Int(command.count)
storage.channels[channel].offeredStepSeconds = command.stepSeconds
storage.channels[channel].offeredLoops = command.loops == 1
case .echo:
applyEcho(command)
case .pan:
storage.channels[channel].pan = min(max(command.frequency, -1), 1)
case .hush:
hushEverything()
case .reset:
// Channel -1 asks for all of them.
if channel < 0 {
hushEverything()
storage.echoFrames = 0
storage.echoFeedback = 0
storage.echoMix = 0
storage.level = 0.6
for index in 0..<channelCount {
resetChannel(index)
}
} else {
resetChannel(channel)
}
case .patternStop:
storage.channels[channel].performing = -1
storage.channels[channel].offered = -1
// Clear the step clock too, or the channel sits out the rest
// of the note it was holding before it can take anything new.
storage.channels[channel].stepIndex = 0
storage.channels[channel].framesUntilStep = 0
publishPerforming(channel, -1)
}
tail &+= 1
}
storage.commandTail.store(tail, ordering: .releasing)
}
/// Steps one channel a single frame on, sounding whatever the grid lands
/// on. The clock is the device's, so a phrase keeps time whatever the host
/// is doing.
private static func applyEcho(_ command: Command) {
storage.echoFrames = Int(min(max(command.frequency, 0), 2) * sampleRate)
storage.echoFeedback = min(max(command.velocity, 0), 0.95)
storage.echoMix = command.stepSeconds
if storage.echoMix <= 0 {
for index in 0..<echoCapacity {
storage.echo[index] = 0
storage.echoRight[index] = 0
}
}
}
private static func advanceSequencer(_ channel: Int) {
if storage.channels[channel].framesUntilStep > 0 {
storage.channels[channel].framesUntilStep -= 1
return
}
adoptOfferedPattern(channel)
let performing = storage.channels[channel].performing
guard performing >= 0 else { return }
let pattern = storage.patterns[patternSlot(channel: channel, buffer: performing)]
if storage.channels[channel].stepIndex >= Int(pattern.count) {
storage.channels[channel].stepIndex = 0
guard pattern.loops else {
storage.channels[channel].performing = -1
publishPerforming(channel, -1)
return
}
return
}
let index = storage.channels[channel].stepIndex
storage.channels[channel].stepIndex = index + 1
let step = storage.steps[stepSlot(channel: channel, buffer: performing, index: index)]
for tone in 0..<Int(step.toneCount) {
let frequency = storage.tones[toneSlot(channel: channel, buffer: performing, index: index, tone: tone)]
guard frequency > 0 else { continue }
let voice = Int(storage.channels[channel].sequencedVoice) % voiceCount
storage.channels[channel].sequencedVoice &+= 1
start(&storage.voices[voice], frequency, step.velocity, channel)
}
storage.channels[channel].framesUntilStep = Int(step.steps * Int32(pattern.stepSeconds * sampleRate))
}
private static func adoptOfferedPattern(_ channel: Int) {
let offered = storage.channels[channel].offered
guard offered >= 0,
storage.channels[channel].performing < 0 || storage.channels[channel].stepIndex == 0
else { return }
storage.patterns[patternSlot(channel: channel, buffer: offered)] = Pattern(
count: Int32(storage.channels[channel].offeredCount),
stepSeconds: storage.channels[channel].offeredStepSeconds,
loops: storage.channels[channel].offeredLoops
)
storage.channels[channel].offered = -1
storage.channels[channel].performing = offered
storage.channels[channel].stepIndex = 0
publishPerforming(channel, offered)
}
/// Only the audio thread writes this; staging reads it to keep clear of
/// whatever is sounding. A byte per channel, holding the slot plus one so
/// that zero reads as nothing playing.
private static func publishPerforming(_ channel: Int, _ slot: Int) {
let shift = UInt32(channel * 8)
var bits = storage.performingSlots.load(ordering: .relaxed)
bits &= ~(0xFF << shift)
bits |= UInt32(slot + 1) << shift
storage.performingSlots.store(bits, ordering: .releasing)
}
private static func performingSlot(_ channel: Int) -> Int {
let bits = storage.performingSlots.load(ordering: .acquiring)
return Int((bits >> UInt32(channel * 8)) & 0xFF) - 1
}
private static func start(_ voice: inout Voice, _ frequency: Float, _ velocity: Float, _ channel: Int) {
voice.stage = .attack
voice.frequency = frequency
voice.velocity = velocity
voice.phase = 0
voice.detunedPhase = 0
voice.envelope = 0
voice.lowpass = 0
voice.bandpass = 0
voice.lfoPhase = 0
voice.cutoffEnvelope = 1
voice.channel = channel
voice.patch = storage.channels[channel].patch
}
private static func render(_ voice: inout Voice) -> Float {
let patch = voice.patch
let swing = lfo(&voice)
let width = min(max(patch.pulseWidth + swing * patch.lfoToWidth, 0.03), 0.97)
let bend = exp2(swing * patch.lfoToPitch / 12)
var sample = oscillator(patch.waveform, voice.phase, width)
let wrapped = voice.phase + voice.frequency * bend / sampleRate
voice.phase = wrapped - Float(Int(wrapped))
if patch.detuneSemitones != 0 || patch.ringMix > 0 {
let detuned = voice.frequency * bend * exp2(patch.detuneSemitones / 12)
let second = oscillator(patch.waveform, voice.detunedPhase, width)
sample = patch.ringMix > 0
? sample * (1 - patch.ringMix) + sample * second * patch.ringMix * 2
: (sample + second) * 0.5
voice.detunedPhase += detuned / sampleRate
voice.detunedPhase -= Float(Int(voice.detunedPhase))
// Restarting the second on the first's edge is the sync sound.
if patch.syncsDetuned && wrapped >= 1 {
voice.detunedPhase = 0
}
}
advanceEnvelope(&voice)
advanceCutoffEnvelope(&voice)
sample = filtered(&voice, sample, swing: swing)
if patch.drive > 0 {
sample = tanh(sample * (1 + patch.drive * 8)) * (1 / (1 + patch.drive))
}
return sample * voice.envelope * voice.velocity * patch.gain
}
private static func oscillator(_ waveform: Int32, _ phase: Float, _ width: Float) -> Float {
switch waveform {
case 1:
return 4 * abs(phase - 0.5) - 1
case 2:
return 2 * phase - 1
case 3:
return phase < width ? 1 : -1
case 4:
storage.noiseSeed = storage.noiseSeed &* 1664525 &+ 1013904223
return Float(storage.noiseSeed >> 8) / 8388608 - 1
default:
return sin(2 * .pi * phase)
}
}
/// One cycle of the voice's own slow wave, -1 to 1.
private static func lfo(_ voice: inout Voice) -> Float {
let rate = voice.patch.lfoRate
guard rate > 0 else { return 0 }
voice.lfoPhase += rate / sampleRate
voice.lfoPhase -= Float(Int(voice.lfoPhase))
return sin(2 * .pi * voice.lfoPhase)
}
/// Falls from struck to closed on its own clock, so a note can open the
/// filter and let it shut again while it holds.
private static func advanceCutoffEnvelope(_ voice: inout Voice) {
guard voice.patch.cutoffEnvelope != 0 else { return }
voice.cutoffEnvelope -= 1 / max(voice.patch.cutoffDecay * sampleRate, 1)
voice.cutoffEnvelope = max(voice.cutoffEnvelope, 0)
}
private static func advanceEnvelope(_ voice: inout Voice) {
let patch = voice.patch
switch voice.stage {
case .attack:
voice.envelope += 1 / max(patch.attack * sampleRate, 1)
if voice.envelope >= 1 {
voice.envelope = 1
voice.stage = .decay
}
case .decay:
voice.envelope -= (1 - patch.sustain) / max(patch.decay * sampleRate, 1)
if voice.envelope <= patch.sustain {
voice.envelope = patch.sustain
voice.stage = patch.sustain <= 0 ? .idle : .sustain
}
case .release:
voice.envelope -= patch.sustain / max(patch.release * sampleRate, 1)
if voice.envelope <= 0 {
voice.envelope = 0
voice.stage = .idle
}
case .idle, .sustain:
break
}
}
/// Chamberlin state variable, taking the lowpass tap.
private static func filtered(_ voice: inout Voice, _ sample: Float, swing: Float) -> Float {
let patch = voice.patch
guard patch.cutoff > 0 else { return sample }
let opened = patch.cutoff
* exp2(voice.cutoffEnvelope * patch.cutoffEnvelope)
* exp2(swing * patch.lfoToCutoff)
let f = 2 * sin(.pi * min(opened, sampleRate * 0.45) / sampleRate)
let q = 1 - min(patch.resonance, 0.98)
let highpass = sample - voice.lowpass - q * voice.bandpass
voice.bandpass += f * highpass
voice.lowpass += f * voice.bandpass
return voice.lowpass
}
/// Drops the command when the ring is full rather than blocking the
/// caller; a lost blip is cheaper than a stalled UI thread.
private static func offer(_ command: Command) {
let head = storage.commandHead.load(ordering: .relaxed)
let tail = storage.commandTail.load(ordering: .acquiring)
guard head &- tail < UInt32(commandCapacity) else { return }
storage.commands[Int(head % UInt32(commandCapacity))] = command
storage.commandHead.store(head &+ 1, ordering: .releasing)
}
private static func stepSlot(channel: Int, buffer: Int, index: Int) -> Int {
((channel * bufferCount) + buffer) * patternCapacity + index
}
private static func patternSlot(channel: Int, buffer: Int) -> Int {
(channel * bufferCount) + buffer
}
private static func toneSlot(channel: Int, buffer: Int, index: Int, tone: Int) -> Int {
stepSlot(channel: channel, buffer: buffer, index: index) * maxTones + tone
}
nonisolated(unsafe) private static let storage = Storage()
private final class Storage {
let voices: UnsafeMutableBufferPointer<Voice>
let channels: UnsafeMutableBufferPointer<ChannelState>
let steps: UnsafeMutableBufferPointer<PatternStep>
let tones: UnsafeMutableBufferPointer<Float>
let patterns: UnsafeMutableBufferPointer<Pattern>
let commands: UnsafeMutableBufferPointer<Command>
let commandHead = Atomic<UInt32>(0)
let commandTail = Atomic<UInt32>(0)
let nextVoice = Atomic<UInt32>(0)
let performingSlots = Atomic<UInt32>(0)
var level: Float = 0.6
var echo = [Float](repeating: 0, count: SynthEngine.echoCapacity)
var echoRight = [Float](repeating: 0, count: SynthEngine.echoCapacity)
var echoWrite = 0
var echoFrames = 0
var echoFeedback: Float = 0
var echoMix: Float = 0
var noiseSeed: UInt32 = 22222
var staging = [Int](repeating: 0, count: SynthEngine.channelCount)
var stagedCount = [Int](repeating: 0, count: SynthEngine.channelCount)
var lastOffered = [Int](repeating: -1, count: SynthEngine.channelCount)
init() {
voices = .allocate(capacity: SynthEngine.voiceCount)
voices.initialize(repeating: Voice())
channels = .allocate(capacity: SynthEngine.channelCount)
channels.initialize(repeating: ChannelState())
steps = .allocate(capacity: SynthEngine.channelCount * SynthEngine.bufferCount * SynthEngine.patternCapacity)
steps.initialize(repeating: PatternStep())
tones = .allocate(
capacity: SynthEngine.channelCount * SynthEngine.bufferCount
* SynthEngine.patternCapacity * SynthEngine.maxTones)
tones.initialize(repeating: 0)
patterns = .allocate(capacity: SynthEngine.channelCount * SynthEngine.bufferCount)
patterns.initialize(repeating: Pattern())
commands = .allocate(capacity: SynthEngine.commandCapacity)
commands.initialize(repeating: Command(kind: .patch, channel: 0))
}
}
public struct PatchValues {
public var waveform: Int32 = 0
public var detuneSemitones: Float = 0
public var attack: Float = 0.005
public var decay: Float = 0.2
public var sustain: Float = 0
public var release: Float = 0.05
public var cutoff: Float = 0
public var resonance: Float = 0
public var gain: Float = 0.5
/// Where a pulse sits between its edges. Half is a square; away from
/// half is what gives a chip lead its reedy voice.
public var pulseWidth: Float = 0.5
public var lfoRate: Float = 0
public var lfoToPitch: Float = 0
public var lfoToWidth: Float = 0
public var lfoToCutoff: Float = 0
/// The filter's own envelope: how far the cutoff opens, and how long
/// it takes to fall back. This is what a pluck or an acid line is.
public var cutoffEnvelope: Float = 0
public var cutoffDecay: Float = 0.2
/// The second oscillator restarted by the first, which is the sound
/// of one pitch dragging another behind it.
public var syncsDetuned: Bool = false
public var ringMix: Float = 0
/// Soft clipping, for when a voice should sound driven rather than
/// loud.
public var drive: Float = 0
public init() {}
}
private enum Stage: Int32 {
case idle, attack, decay, sustain, release
}
private struct Voice {
var stage: Stage = .idle
var frequency: Float = 0
var velocity: Float = 0
var phase: Float = 0
var detunedPhase: Float = 0
var envelope: Float = 0
var lowpass: Float = 0
var bandpass: Float = 0
var lfoPhase: Float = 0
var cutoffEnvelope: Float = 0
var channel = 0
var patch = PatchValues()
}
private struct ChannelState {
var patch = PatchValues()
/// -1 hard left, 1 hard right.
var pan: Float = 0
var performing = -1
var offered = -1
var offeredCount = 0
var offeredStepSeconds: Float = 0.125
var offeredLoops = true
var stepIndex = 0
var framesUntilStep = 0
var sequencedVoice: UInt32 = 0
}
private struct PatternStep {
var velocity: Float = 0
var steps: Int32 = 1
var toneCount: Int32 = 0
}
private struct Pattern {
var count: Int32 = 0
var stepSeconds: Float = 0.125
var loops = true
}
private enum CommandKind: Int32 {
case patch, noteOn, noteOff, patternOffer, patternStop, echo, pan, hush, reset
}
private struct Command {
var kind: CommandKind
var channel: Int32
var index: Int32 = 0
var count: Int32 = 0
var frequency: Float = 0
var velocity: Float = 0
var stepSeconds: Float = 0
var loops: Int32 = 0
var patch = PatchValues()
}
}
/// Called by the device's audio callback, and directly when rendering with no
/// device at all.
@_cdecl("luma_synth_mix")
public func luma_synth_mix(_ frames: UnsafeMutablePointer<Float>, _ frameCount: Int32, _ channels: Int32) {
SynthEngine.mix(into: frames, frameCount: Int(frameCount), channels: Int(channels))
}