removed unwanted bar now with waveforms, major updates and enhancements
This commit is contained in:
+185
-52
@@ -51,51 +51,63 @@ import CoreAudio
|
||||
guard let range = scheduledLoop,position >= range.upperBound else { return position }
|
||||
return range.lowerBound+(position-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
|
||||
}
|
||||
private func exitNativeLoopWithoutRestart() -> Bool {
|
||||
guard playing,let file,let render = node.lastRenderTime,let clock = node.playerTime(forNodeTime:render) else { return false }
|
||||
let sourceTime = rawAudioPosition,pending = pendingLoopSwitch
|
||||
let range = pending.map{sourceTime >= $0.at ? $0.range : scheduledLoop} ?? scheduledLoop
|
||||
let raw = pending.map{sourceTime >= $0.at ? $0.raw : scheduledRawLoop} ?? scheduledRawLoop
|
||||
guard let range,let raw,let source = raw.floatChannelData,!raw.format.isInterleaved else { return false }
|
||||
let rate = file.processingFormat.sampleRate,now = mach_absolute_time()
|
||||
let elapsed = render.isHostTimeValid && now >= render.hostTime ? min(0.1,AVAudioTime.seconds(forHostTime:now-render.hostTime))*tempo : 0
|
||||
let sample = clock.sampleTime+AVAudioFramePosition(elapsed*rate)+512
|
||||
let position = loopPosition(anchor+Double(sample)/rate)
|
||||
let when = AVAudioTime(sampleTime:sample,atRate:rate)
|
||||
let offset = max(0,min(Int(raw.frameLength)-1,Int((position-range.lowerBound)*rate)))
|
||||
let count = min(2048,Int(raw.frameLength)-offset)
|
||||
guard count > 0,let bridge = AVAudioPCMBuffer(pcmFormat:raw.format,frameCapacity:AVAudioFrameCount(count)),let output = bridge.floatChannelData else { return false }
|
||||
bridge.frameLength = AVAudioFrameCount(count)
|
||||
for channel in 0..<Int(raw.format.channelCount) { memcpy(output[channel],source[channel].advanced(by:offset),count*MemoryLayout<Float>.size) }
|
||||
anchor = position-Double(sample)/rate;time = position
|
||||
generation = UUID();let token = generation
|
||||
nativeLoopActive = false;pendingLoopSwitch = nil;scheduledLoop = nil;scheduledRawLoop = nil
|
||||
// Interrupt only the scheduled loop, leaving the running node and its clock intact.
|
||||
let next = min(file.length,AVAudioFramePosition(position*rate)+AVAudioFramePosition(count))
|
||||
let finished: AVAudioPlayerNodeCompletionHandler = { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
guard let self,self.generation == token else { return }
|
||||
if self.repeating { self.schedule(at:0,play:true) }
|
||||
else { self.playing = false;self.time = self.duration;self.timer?.invalidate();self.timer = nil;self.render();self.onFinished?() }
|
||||
private var loopHandoff: Task<Void,Never>?
|
||||
private var handoffPresentation: (until: UInt64,began: UInt64,position: Double,range: ClosedRange<Double>?)?
|
||||
private func cancelLoopHandoff() {
|
||||
loopHandoff?.cancel();loopHandoff = nil;handoffPresentation = nil
|
||||
let other = node === firstNode ? secondNode : firstNode
|
||||
other.stop();gainUnit(for:other)?.setGain(0,from:0,seconds:0);gainUnit(for:node)?.setGain(1,from:1,seconds:0);node.volume = Float(outputOpacity)
|
||||
}
|
||||
/// Prepare the replacement on an idle node, then overlap matching source audio.
|
||||
/// Never interrupt or flush the node that is currently feeding the output.
|
||||
private func handoffLoopAudio(at position: Double) {
|
||||
cancelLoopHandoff()
|
||||
let outgoing = node,outgoingGain = gainUnit(for:node)
|
||||
let began = mach_absolute_time()
|
||||
let lead = loopEnabled ? 0.05 : min(0.05,max(0,(duration-position)/max(0.01,tempo)*0.25))
|
||||
let host = began+AVAudioTime.hostTime(forSeconds:lead)
|
||||
let previousRange = nativeLoopActive ? scheduledLoop : nil
|
||||
var target = position+lead*tempo
|
||||
if loopEnabled,nativeLoopActive,let range = scheduledLoop,target >= range.upperBound {
|
||||
target = range.lowerBound+(target-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
|
||||
} else if loopEnabled,let range = preparedLoop,target >= range.upperBound {
|
||||
target = range.lowerBound+(target-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
|
||||
}
|
||||
node = outgoing === firstNode ? secondNode : firstNode
|
||||
schedule(at:target,play:false,notifyPlayback:false,cancelHandoff:false)
|
||||
time = position
|
||||
let incoming = node,incomingGain = gainUnit(for:node)
|
||||
incoming.volume = Float(outputOpacity)
|
||||
incomingGain?.setGain(1,from:0,seconds:0.012,at:host)
|
||||
outgoingGain?.setGain(0,seconds:0.012,at:host)
|
||||
incoming.play(at:AVAudioTime(hostTime:host));playing = true
|
||||
handoffPresentation = (host,began,position,previousRange)
|
||||
loopHandoff = Task { @MainActor [weak self] in
|
||||
// Wait for the envelopes to finish on the audio render thread, rather
|
||||
// than assuming a UI timer ran in time for a hardware render quantum.
|
||||
while outgoingGain?.settled == false || incomingGain?.settled == false {
|
||||
do { try await Task.sleep(nanoseconds:5_000_000) } catch { return }
|
||||
}
|
||||
guard !Task.isCancelled else { return }
|
||||
outgoing.stop()
|
||||
guard let self else { return }
|
||||
incoming.volume = Float(outputOpacity)
|
||||
loopHandoff = nil;handoffPresentation = nil
|
||||
}
|
||||
if next < file.length {
|
||||
node.scheduleBuffer(bridge,at:when,options:.interrupts,completionHandler:nil)
|
||||
node.scheduleSegment(file,startingFrame:next,frameCount:AVAudioFrameCount(min(file.length-next,Int64(UInt32.max))),at:nil,completionCallbackType:.dataPlayedBack,completionHandler:finished)
|
||||
} else {
|
||||
node.scheduleBuffer(bridge,at:when,options:.interrupts,completionCallbackType:.dataPlayedBack,completionHandler:finished)
|
||||
}
|
||||
startTimer();self.render();return true
|
||||
startTimer()
|
||||
}
|
||||
private func exitNativeLoopWithoutRestart() -> Bool {
|
||||
guard playing,file != nil else { return false }
|
||||
let position = displayTime
|
||||
handoffLoopAudio(at:position)
|
||||
return true
|
||||
}
|
||||
private func activatePreparedLoop() -> Bool {
|
||||
guard playing,let range = preparedLoop,preparedRawLoop != nil,loopBuffer != nil else { return false }
|
||||
let position = displayTime
|
||||
guard position >= range.lowerBound else { return false }
|
||||
let handoff = position < range.upperBound ? position : range.lowerBound+(position-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
|
||||
// File commands cannot be interrupted by scheduling a PCM buffer. Replace
|
||||
// the node's commands once, with all loop audio already prepared. Keep the
|
||||
// engine, effects and output route running throughout the handoff.
|
||||
schedule(at:handoff,play:true,notifyPlayback:false)
|
||||
handoffLoopAudio(at:handoff)
|
||||
return nativeLoopActive
|
||||
}
|
||||
private func prepareNativeLoop() {
|
||||
@@ -157,6 +169,11 @@ import CoreAudio
|
||||
var canJog: Bool { currentSongID != nil && duration > 0 }
|
||||
/// Read the audio clock for display frames without publishing the entire player at 60 Hz.
|
||||
var displayTime: Double {
|
||||
if playing,let pending = handoffPresentation,mach_absolute_time() < pending.until {
|
||||
var value = pending.position+AVAudioTime.seconds(forHostTime:mach_absolute_time()-pending.began)*tempo
|
||||
if let range = pending.range,value >= range.upperBound { value = range.lowerBound+(value-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound) }
|
||||
return min(duration,max(0,value))
|
||||
}
|
||||
guard playing,node.isPlaying,let file,let render = node.lastRenderTime,
|
||||
let current = node.playerTime(forNodeTime:render) else { return time }
|
||||
let now = mach_absolute_time()
|
||||
@@ -178,7 +195,7 @@ import CoreAudio
|
||||
cancelPendingPlayback()
|
||||
if slideImage != nil,playing { time = min(duration,slideAnchor+ProcessInfo.processInfo.systemUptime-slideStart);slideAnchor = time }
|
||||
else if let file,playing,let render = node.lastRenderTime,let current = node.playerTime(forNodeTime:render) { time = min(duration,max(0,loopPosition(anchor+Double(current.sampleTime)/file.processingFormat.sampleRate))) }
|
||||
node.pause();playing = false;timer?.invalidate();timer = nil
|
||||
cancelLoopHandoff();node.pause();playing = false;timer?.invalidate();timer = nil
|
||||
}
|
||||
func hotCue(_ index: Int,clear: Bool = false) {
|
||||
guard hotCues.indices.contains(index),canJog else { return }
|
||||
@@ -226,7 +243,7 @@ import CoreAudio
|
||||
}
|
||||
}
|
||||
}
|
||||
private func setFade(_ value: Double) { let next = min(1,max(0,value));if abs(outputOpacity-next) > 0.00001 { outputOpacity = next;node.volume = Float(next) } }
|
||||
private func setFade(_ value: Double) { let next = min(1,max(0,value));if abs(outputOpacity-next) > 0.00001 { outputOpacity = next;if loopHandoff == nil { node.volume = Float(next) } } }
|
||||
private var videoPlayer: AVPlayer?
|
||||
private var videoOutput: AVPlayerItemVideoOutput?
|
||||
private let videoContext = CIContext()
|
||||
@@ -249,7 +266,7 @@ import CoreAudio
|
||||
tick()
|
||||
let running = engine.isRunning,resume = playing,position = time,wasWaiting = waitingForBackground
|
||||
var previousMap = outputChannelMap()
|
||||
cancelPendingPlayback();generation = UUID();node.stop();engine.stop()
|
||||
cancelLoopHandoff();cancelPendingPlayback();generation = UUID();node.stop();engine.stop()
|
||||
do {
|
||||
var value = device
|
||||
let result = AudioUnitSetProperty(unit,kAudioOutputUnitProperty_CurrentDevice,kAudioUnitScope_Global,0,&value,UInt32(MemoryLayout.size(ofValue:value)))
|
||||
@@ -339,7 +356,15 @@ import CoreAudio
|
||||
private(set) var currentSongID: Int64?
|
||||
var onFinished: (() -> Void)?
|
||||
private let engine = AVAudioEngine()
|
||||
private let node = AVAudioPlayerNode()
|
||||
private let firstNode = AVAudioPlayerNode()
|
||||
private let secondNode = AVAudioPlayerNode()
|
||||
private lazy var node = firstNode
|
||||
private let transportMixer = AVAudioMixerNode()
|
||||
private let firstEnvelope = AVAudioUnitEffect(audioComponentDescription:DJTransportEnvelopeUnit.component)
|
||||
private let secondEnvelope = AVAudioUnitEffect(audioComponentDescription:DJTransportEnvelopeUnit.component)
|
||||
private func gainUnit(for node: AVAudioPlayerNode) -> DJTransportEnvelopeUnit? {
|
||||
(node === firstNode ? firstEnvelope : secondEnvelope).auAudioUnit as? DJTransportEnvelopeUnit
|
||||
}
|
||||
private let effect = AVAudioUnitTimePitch()
|
||||
private var file: AVAudioFile?
|
||||
private var anchor: Double = 0
|
||||
@@ -350,7 +375,7 @@ import CoreAudio
|
||||
init(waveformCacheDirectory: URL? = nil, persistsMix: Bool = true) {
|
||||
self.persistsMix = persistsMix
|
||||
self.waveformCacheDirectory = waveformCacheDirectory ?? FileManager.default.urls(for:.applicationSupportDirectory,in:.userDomainMask)[0].appendingPathComponent("KJamsClone/Waveforms")
|
||||
engine.attach(node);engine.attach(effect);engine.attach(tone);engine.attach(graphicEQ);engine.attach(vocalReduction);graphicEQ.bypass = true
|
||||
engine.attach(firstNode);engine.attach(secondNode);engine.attach(transportMixer);engine.attach(firstEnvelope);engine.attach(secondEnvelope);engine.attach(effect);engine.attach(tone);engine.attach(graphicEQ);engine.attach(vocalReduction);graphicEQ.bypass = true
|
||||
if persistsMix,let data = try? Data(contentsOf:mixURL),let mix = try? JSONDecoder().decode(GlobalMix.self,from:data) {
|
||||
volume = min(1,max(0,mix.volume));lowGain = min(12,max(-12,mix.low));midGain = min(12,max(-12,mix.mid));highGain = min(12,max(-12,mix.high));filter = min(1,max(-1,mix.filter));toneEnabled = mix.enabled
|
||||
}
|
||||
@@ -370,7 +395,11 @@ import CoreAudio
|
||||
let loaded = try AVAudioFile(forReading:URL(fileURLWithPath:song.audio))
|
||||
let graphics = try song.cdg.map { try Data(contentsOf:URL(fileURLWithPath:$0)) } ?? Data()
|
||||
guard loaded.length > 0 else { throw LibraryError.message("Audio file is empty") }
|
||||
engine.connect(node,to:effect,format:loaded.processingFormat)
|
||||
engine.connect(firstNode,to:firstEnvelope,format:loaded.processingFormat)
|
||||
engine.connect(secondNode,to:secondEnvelope,format:loaded.processingFormat)
|
||||
engine.connect(firstEnvelope,to:transportMixer,fromBus:0,toBus:0,format:loaded.processingFormat)
|
||||
engine.connect(secondEnvelope,to:transportMixer,fromBus:0,toBus:1,format:loaded.processingFormat)
|
||||
engine.connect(transportMixer,to:effect,format:loaded.processingFormat)
|
||||
engine.connect(effect,to:tone,format:loaded.processingFormat)
|
||||
engine.connect(tone,to:graphicEQ,format:loaded.processingFormat)
|
||||
engine.connect(graphicEQ,to:vocalReduction,format:loaded.processingFormat)
|
||||
@@ -436,7 +465,8 @@ import CoreAudio
|
||||
self.onPlaybackReady?();self.node.play()
|
||||
}
|
||||
}
|
||||
private func schedule(at seconds: Double, play: Bool,notifyPlayback: Bool = true) {
|
||||
private func schedule(at seconds: Double, play: Bool,notifyPlayback: Bool = true,cancelHandoff: Bool = true) {
|
||||
if cancelHandoff { cancelLoopHandoff() }
|
||||
guard let file else { return }
|
||||
cancelPendingPlayback();generation = UUID(); let token = generation
|
||||
node.stop();audioScheduleRestartCount += 1;if !fading { setFade(1) }
|
||||
@@ -446,9 +476,23 @@ import CoreAudio
|
||||
guard start < file.length else { playing = false; render(); return }
|
||||
nativeLoopActive = false;pendingLoopSwitch = nil;scheduledLoop = nil;scheduledRawLoop = nil
|
||||
if loopEnabled,let range = preparedLoop,range.lowerBound == loopIn,range.upperBound == loopOut,let loopBuffer,seconds < range.upperBound {
|
||||
let endFrame = AVAudioFramePosition(range.upperBound*rate)
|
||||
let remainder = AVAudioFrameCount(max(1,endFrame-start))
|
||||
node.scheduleSegment(file,startingFrame:start,frameCount:remainder,at:nil)
|
||||
let first = AVAudioFramePosition(range.lowerBound*rate)
|
||||
if start < first {
|
||||
node.scheduleSegment(file,startingFrame:start,frameCount:AVAudioFrameCount(first-start),at:nil)
|
||||
} else {
|
||||
// The first partial pass must use the same seam treatment as later
|
||||
// passes; a raw file prefix would click at the first wrap only.
|
||||
let offset = min(Int(loopBuffer.frameLength)-1,max(0,Int(start-first)))
|
||||
let count = Int(loopBuffer.frameLength)-offset
|
||||
if let tail = AVAudioPCMBuffer(pcmFormat:loopBuffer.format,frameCapacity:AVAudioFrameCount(count)),
|
||||
let input = loopBuffer.floatChannelData,let output = tail.floatChannelData {
|
||||
tail.frameLength = AVAudioFrameCount(count)
|
||||
for channel in 0..<Int(loopBuffer.format.channelCount) {
|
||||
memcpy(output[channel],input[channel].advanced(by:offset),count*MemoryLayout<Float>.size)
|
||||
}
|
||||
node.scheduleBuffer(tail,at:nil)
|
||||
}
|
||||
}
|
||||
node.scheduleBuffer(loopBuffer,at:nil,options:.loops)
|
||||
nativeLoopActive = true;scheduledLoop = range;scheduledRawLoop = preparedRawLoop
|
||||
if play { if notifyPlayback { startNodePlayback() } else { node.play() } }
|
||||
@@ -479,11 +523,12 @@ import CoreAudio
|
||||
if playing { tick();slideAnchor = time;playing = false } else { if time >= duration { time = 0;slideAnchor = 0 };slideStart = ProcessInfo.processInfo.systemUptime;playing = true;startTimer() };return
|
||||
}
|
||||
guard file != nil else { return }
|
||||
if playing { cancelPendingPlayback();tick(); node.pause(); playing = false }
|
||||
if playing { cancelPendingPlayback();tick();cancelLoopHandoff(); node.pause(); playing = false }
|
||||
else if time >= duration { schedule(at:0,play:true); startTimer() }
|
||||
else { startNodePlayback(); playing = true; startTimer() }
|
||||
}
|
||||
func stop() {
|
||||
cancelLoopHandoff()
|
||||
endScrubbing()
|
||||
cancelPendingPlayback()
|
||||
graphicsGeneration = UUID();graphicsTask?.cancel();graphicsTask = nil;checkpointTask?.cancel();checkpointTask = nil;checkpoints = [:];lastGraphicsPacket = -1
|
||||
@@ -495,7 +540,7 @@ import CoreAudio
|
||||
generation = UUID(); timer?.invalidate(); timer = nil; node.stop(); engine.stop()
|
||||
if metering { engine.mainMixerNode.removeTap(onBus:0); metering = false }
|
||||
meter.reset(); leftLevel = 0; rightLevel = 0
|
||||
engine.disconnectNodeOutput(node); engine.disconnectNodeOutput(effect); engine.disconnectNodeOutput(tone);engine.disconnectNodeOutput(graphicEQ);engine.disconnectNodeOutput(vocalReduction)
|
||||
engine.disconnectNodeOutput(firstNode);engine.disconnectNodeOutput(secondNode);engine.disconnectNodeOutput(transportMixer);engine.disconnectNodeOutput(firstEnvelope);engine.disconnectNodeOutput(secondEnvelope); engine.disconnectNodeOutput(effect); engine.disconnectNodeOutput(tone);engine.disconnectNodeOutput(graphicEQ);engine.disconnectNodeOutput(vocalReduction)
|
||||
file = nil; playing = false; time = 0; anchor = 0; duration = 0; image = nil; currentSongID = nil; title = "Select a song"
|
||||
}
|
||||
var audibleScrubbing = false
|
||||
@@ -504,6 +549,8 @@ import CoreAudio
|
||||
private var lastScrubAudio = -Double.infinity
|
||||
private var scrubTarget: Double?
|
||||
var scrubOutputLevel: Float { let levels = meter.snapshot();return max(levels.0,levels.1) }
|
||||
func beginAudioContinuityCheck() { meter.beginContinuityCheck() }
|
||||
var audioContinuity: (silentFrames: Int,largestStep: Float) { meter.continuity }
|
||||
/// Preview short grains through the deck's normal EQ, fader and output route.
|
||||
/// Keep transport paused so scrubbing cannot start Automix or change sync state.
|
||||
func scrub(to value: Double) {
|
||||
@@ -590,8 +637,8 @@ import CoreAudio
|
||||
let levels = meter.snapshot()
|
||||
let left = playing ? levels.0 : 0,right = playing ? levels.1 : 0
|
||||
if leftLevel != left { leftLevel = left };if rightLevel != right { rightLevel = right }
|
||||
if playing, let file, let renderTime = node.lastRenderTime, let playerTime = node.playerTime(forNodeTime:renderTime) {
|
||||
let next = min(duration,max(0,loopPosition(anchor+Double(playerTime.sampleTime)/file.processingFormat.sampleRate)))
|
||||
if playing,node.lastRenderTime != nil {
|
||||
let next = displayTime
|
||||
if abs(time-next) > 0.0001 { time = next }
|
||||
if !nativeLoopActive,loopEnabled,loopBuffer != nil { _ = activatePreparedLoop() }
|
||||
if !nativeLoopActive,loopEnabled,let loopIn,let loopOut,time >= loopOut { schedule(at:loopIn,play:true);return }
|
||||
@@ -706,6 +753,17 @@ enum AudioTone {
|
||||
final class AudioLevelMeter: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var levels: (Float,Float) = (0,0)
|
||||
private var checkingContinuity = false
|
||||
private var lastSample: Float?
|
||||
private var silentFrames = 0, longestSilence = 0
|
||||
private var largestStep: Float = 0
|
||||
func beginContinuityCheck() {
|
||||
lock.lock();defer { lock.unlock() }
|
||||
checkingContinuity = true;lastSample = nil;silentFrames = 0;longestSilence = 0;largestStep = 0
|
||||
}
|
||||
var continuity: (silentFrames: Int,largestStep: Float) {
|
||||
lock.lock();defer { lock.unlock() };return (longestSilence,largestStep)
|
||||
}
|
||||
func consume(_ buffer: AVAudioPCMBuffer) {
|
||||
guard let data = buffer.floatChannelData else { return }
|
||||
let channels = Int(buffer.format.channelCount), frames = Int(buffer.frameLength)
|
||||
@@ -713,7 +771,17 @@ final class AudioLevelMeter: @unchecked Sendable {
|
||||
for i in 0..<frames {
|
||||
left = max(left,abs(data[0][i]));right = max(right,abs(data[min(1,channels-1)][i]))
|
||||
}
|
||||
lock.lock();levels = (left,right);lock.unlock()
|
||||
lock.lock();levels = (left,right)
|
||||
if checkingContinuity {
|
||||
for i in 0..<frames {
|
||||
let sample = data[0][i]
|
||||
if let lastSample { largestStep = max(largestStep,abs(sample-lastSample)) }
|
||||
lastSample = sample
|
||||
silentFrames = abs(sample) < 0.000001 ? silentFrames+1 : 0
|
||||
longestSilence = max(longestSilence,silentFrames)
|
||||
}
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
func snapshot() -> (Float,Float) { lock.lock();defer { lock.unlock() };return levels }
|
||||
func reset() { lock.lock();levels = (0,0);lock.unlock() }
|
||||
@@ -799,3 +867,68 @@ final class DJVocalReductionUnit: AUAudioUnit {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gain envelopes advance per audio frame; the main thread never performs the fade.
|
||||
private final class DJEnvelopeCommand: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var revision: UInt64 = 0,completed: UInt64 = 0
|
||||
private var target: Float = 0,initial: Float?
|
||||
private var seconds: Double = 0
|
||||
private var host: UInt64 = 0
|
||||
func set(_ target: Float,from initial: Float?,seconds: Double,host: UInt64) {
|
||||
lock.lock();defer { lock.unlock() }
|
||||
revision &+= 1;self.target = target;self.initial = initial;self.seconds = seconds;self.host = host
|
||||
}
|
||||
func read(after version: UInt64) -> (UInt64,Float,Float?,Double,UInt64)? {
|
||||
guard lock.try() else { return nil };defer { lock.unlock() }
|
||||
guard revision != version else { return nil };return (revision,target,initial,seconds,host)
|
||||
}
|
||||
func finish(_ version: UInt64) { guard lock.try() else { return };completed = version;lock.unlock() }
|
||||
var settled: Bool { lock.lock();defer { lock.unlock() };return completed == revision }
|
||||
}
|
||||
final class DJTransportEnvelopeUnit: AUAudioUnit {
|
||||
private var inputs: AUAudioUnitBusArray!,outputs: AUAudioUnitBusArray!
|
||||
private let command = DJEnvelopeCommand()
|
||||
static let component: AudioComponentDescription = {
|
||||
let value = AudioComponentDescription(componentType:kAudioUnitType_Effect,componentSubType:0x6B6A656E,componentManufacturer:0x4B4A4D58,componentFlags:0,componentFlagsMask:0)
|
||||
AUAudioUnit.registerSubclass(DJTransportEnvelopeUnit.self,as:value,name:"KJMix: Transport Envelope",version:1);return value
|
||||
}()
|
||||
override init(componentDescription: AudioComponentDescription,options: AudioComponentInstantiationOptions = []) throws {
|
||||
try super.init(componentDescription:componentDescription,options:options)
|
||||
let format = AVAudioFormat(standardFormatWithSampleRate:44100,channels:2)!
|
||||
inputs = AUAudioUnitBusArray(audioUnit:self,busType:.input,busses:[try AUAudioUnitBus(format:format)])
|
||||
outputs = AUAudioUnitBusArray(audioUnit:self,busType:.output,busses:[try AUAudioUnitBus(format:format)])
|
||||
maximumFramesToRender = 4096
|
||||
}
|
||||
override var inputBusses: AUAudioUnitBusArray { inputs }
|
||||
override var outputBusses: AUAudioUnitBusArray { outputs }
|
||||
func setGain(_ value: Float,from initial: Float? = nil,seconds: Double,at host: UInt64 = 0) { command.set(value,from:initial,seconds:seconds,host:host) }
|
||||
var settled: Bool { command.settled }
|
||||
override var internalRenderBlock: AUInternalRenderBlock {
|
||||
let command = self.command,rate = outputBusses[0].format.sampleRate
|
||||
var version: UInt64 = 0,startHost: UInt64 = 0,gain: Float = 0,target: Float = 0,step: Float = 0,remaining = 0
|
||||
let ticksPerFrame = Double(AVAudioTime.hostTime(forSeconds:1))/rate
|
||||
return { flags,time,frames,_,data,_,pull in
|
||||
guard let pull else { return kAudioUnitErr_NoConnection }
|
||||
let result = pull(flags,time,frames,0,data)
|
||||
guard result == noErr else { return result }
|
||||
if let next = command.read(after:version) {
|
||||
version = next.0;target = next.1;startHost = next.4
|
||||
if let initial = next.2 { gain = initial }
|
||||
remaining = Int(next.3*rate);step = remaining > 0 ? (target-gain)/Float(remaining) : 0
|
||||
if remaining == 0 { gain = target }
|
||||
}
|
||||
let buffers = UnsafeMutableAudioBufferListPointer(data)
|
||||
for frame in 0..<Int(frames) {
|
||||
if remaining > 0,(startHost == 0 || time.pointee.mHostTime+UInt64(Double(frame)*ticksPerFrame) >= startHost) { gain += step;remaining -= 1;if remaining == 0 { gain = target } }
|
||||
for buffer in buffers {
|
||||
guard let values = buffer.mData?.assumingMemoryBound(to:Float.self) else { continue }
|
||||
let channels = Int(buffer.mNumberChannels)
|
||||
for channel in 0..<channels { values[frame*channels+channel] *= gain }
|
||||
}
|
||||
}
|
||||
if remaining == 0 { command.finish(version) }
|
||||
return noErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user