Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

slab/menuband: PeakLimiter on master path — no clipping with simultaneous notes

Inserts AVAudioMixerNode → AVAudioUnitEffect (kAudioUnitSubType_PeakLimiter)
between every backend (melodic sampler, drums sampler, MIDISynth) and
mainMixerNode. Chords or stacked sustains across backends now share one
gain stage and get peak-clamped before reaching the output, so the
crackle/clipping at high polyphony is gone.

Limiter tuned for transparency on instrument samples:
attack 2 ms, decay 50 ms, pre-gain 0 dB.

Engine topology:
melodic ─┐
drums ─┼─→ preLimiterMixer ─→ limiter ─→ mainMixerNode ─→ outputNode
midiAU ─┘

Waveform tap stays on mainMixerNode (post-limiter) so the visualizer
shows what the speakers actually emit, including limiter behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+46 -3
+46 -3
slab/menuband/Sources/MenuBand/MenuBandSynth.swift
··· 34 34 /// Fallback backend, always available immediately. 35 35 private let melodic = AVAudioUnitSampler() 36 36 private let drums = AVAudioUnitSampler() 37 + /// Sums every backend before the limiter so simultaneous voices share one 38 + /// gain stage. Without this, each backend would feed `mainMixerNode` 39 + /// directly and a chord across melodic + drums + midiSynth could exceed 40 + /// 0 dBFS at the output (audible clipping/crackle). 41 + private let preLimiterMixer = AVAudioMixerNode() 42 + /// Apple PeakLimiter on the master path. Catches transient peaks from 43 + /// chords or stacked sustains and holds output below 0 dBFS regardless 44 + /// of how many notes are pressed simultaneously. Parameters tuned for 45 + /// transparency on instrument samples (fast attack, gentle release). 46 + private let limiter: AVAudioUnitEffect = { 47 + var desc = AudioComponentDescription( 48 + componentType: kAudioUnitType_Effect, 49 + componentSubType: kAudioUnitSubType_PeakLimiter, 50 + componentManufacturer: kAudioUnitManufacturer_Apple, 51 + componentFlags: 0, 52 + componentFlagsMask: 0) 53 + return AVAudioUnitEffect(audioComponentDescription: desc) 54 + }() 37 55 private var started = false 38 56 private var melodicConnected = false 39 57 private var drumsConnected = false 40 58 private var midiSynthConnected = false 59 + private var limiterConnected = false 41 60 private var waveformCaptureEnabled = false 42 61 private var activeNotes: Set<UInt16> = [] 43 62 private var idleSuspendWorkItem: DispatchWorkItem? ··· 69 88 70 89 func start() { 71 90 guard !started else { return } 91 + engine.attach(preLimiterMixer) 92 + engine.attach(limiter) 72 93 engine.attach(melodic) 73 94 engine.attach(drums) 95 + connectLimiterIfNeeded() 74 96 connectMelodicSamplerIfNeeded() 75 97 connectDrumsSamplerIfNeeded() 76 98 engine.prepare() ··· 161 183 // patch needs them again. 162 184 } 163 185 186 + /// Wire preLimiterMixer → limiter → mainMixerNode and tune the limiter 187 + /// for transparent peak control on instrument samples. Called once from 188 + /// `start()` before any backend is connected so backends can route 189 + /// straight to `preLimiterMixer`. 190 + private func connectLimiterIfNeeded() { 191 + guard !limiterConnected else { return } 192 + engine.connect(preLimiterMixer, to: limiter, format: nil) 193 + engine.connect(limiter, to: engine.mainMixerNode, format: nil) 194 + let au = limiter.audioUnit 195 + // Fast attack catches chord/transient peaks; medium release avoids 196 + // pumping on sustained notes. Pre-gain stays at 0 so quiet input 197 + // doesn't get squashed into the limiter unnecessarily. 198 + AudioUnitSetParameter(au, kLimiterParam_AttackTime, 199 + kAudioUnitScope_Global, 0, 0.002, 0) 200 + AudioUnitSetParameter(au, kLimiterParam_DecayTime, 201 + kAudioUnitScope_Global, 0, 0.050, 0) 202 + AudioUnitSetParameter(au, kLimiterParam_PreGain, 203 + kAudioUnitScope_Global, 0, 0.0, 0) 204 + limiterConnected = true 205 + } 206 + 164 207 private func connectMelodicSamplerIfNeeded() { 165 208 guard !melodicConnected else { return } 166 - engine.connect(melodic, to: engine.mainMixerNode, format: nil) 209 + engine.connect(melodic, to: preLimiterMixer, format: nil) 167 210 melodicConnected = true 168 211 } 169 212 ··· 176 219 177 220 private func connectDrumsSamplerIfNeeded() { 178 221 guard !drumsConnected else { return } 179 - engine.connect(drums, to: engine.mainMixerNode, format: nil) 222 + engine.connect(drums, to: preLimiterMixer, format: nil) 180 223 drumsConnected = true 181 224 } 182 225 ··· 189 232 190 233 private func connectMIDISynthIfNeeded(_ avUnit: AVAudioUnit) { 191 234 guard !midiSynthConnected else { return } 192 - engine.connect(avUnit, to: engine.mainMixerNode, format: nil) 235 + engine.connect(avUnit, to: preLimiterMixer, format: nil) 193 236 midiSynthConnected = true 194 237 } 195 238