There is no more perfect synchronization in nature than to merge all the tracks into one whole.
This tested code makes a mix of tracks and plays the result:
import SwiftUI
import AVFoundation
let AV_ENGINE = AVAudioEngine()
#if os(iOS)
let AV_SESSION = AVAudioSession.sharedInstance()
#endif
@main struct app: App {
var body: some Scene {
WindowGroup {
Text("Audio Mix")
}
}
init() {
/* mix */
let mix = AVAudioEngine().mix([
AVAudioPCMBuffer.etaloneGenerate(channels: 2, size: 7000)!,
AVAudioPCMBuffer.etaloneGenerate(channels: 2, size: 3000)!,
AVAudioPCMBuffer.etaloneGenerate(channels: 2, size: 4000)!,
])!
/* play */
let avPlayerNode = AVAudioPlayerNode()
AV_ENGINE.attach(avPlayerNode)
AV_ENGINE.connect(
avPlayerNode,
to: AV_ENGINE.mainMixerNode,
format: AVAudioEngine.AV_DEFAULT_FORMAT
)
if AV_ENGINE.isRunning == false {
try! AV_ENGINE.start()
}
avPlayerNode.scheduleBuffer(mix)
avPlayerNode.play()
dump(
mix.frameLength
)
}
}
Extension Numeric
extension Numeric {
func fixBounds(min: Self = 0, max: Self) -> Self where Self: Comparable {
if self < min {return min}
if self > max {return max}
return self
}
}
Extension AVAudioEngine (magic is here)
extension AVAudioEngine {
static let AV_DEFAULT_LINEAR_PCM_BIT_DEPTH_FORMAT: AVAudioCommonFormat = .pcmFormatFloat32
static let AV_DEFAULT_SAMPLERATE: Double = 44100.0
static let AV_DEFAULT_CHANNELS: AVAudioChannelCount = 2
static let AV_DEFAULT_IS_INTERLEAVED: Bool = false
static let AV_DEFAULT_FORMAT = AVAudioFormat(
commonFormat: AV_DEFAULT_LINEAR_PCM_BIT_DEPTH_FORMAT,
sampleRate : AV_DEFAULT_SAMPLERATE,
channels : AV_DEFAULT_CHANNELS,
interleaved : AV_DEFAULT_IS_INTERLEAVED
)!
public func mix(_ avBuffers: [AVAudioPCMBuffer], resultFormat: AVAudioFormat? = nil) -> AVAudioPCMBuffer? {
do {
var avPlayerNodes: [AVAudioPlayerNode] = []
var frameLength: AVAudioFrameCount = 0
for avBuffer in avBuffers {
let avPlayerNode = AVAudioPlayerNode()
avPlayerNodes.append(avPlayerNode)
self.attach(avPlayerNode)
self.connect(
avPlayerNode,
to: self.mainMixerNode,
format: avBuffer.format
)
avPlayerNode.scheduleBuffer(avBuffer)
frameLength = max(
frameLength,
avBuffer.frameLength
)
}
try self.enableManualRenderingMode(
AVAudioEngineManualRenderingMode.offline,
format : resultFormat ?? Self.AV_DEFAULT_FORMAT,
maximumFrameCount: frameLength
)
try self.start()
for node in avPlayerNodes {
node.play()
}
let buffer = AVAudioPCMBuffer(
pcmFormat : self.manualRenderingFormat,
frameCapacity: self.manualRenderingMaximumFrameCount
)!
let result = AVAudioPCMBuffer(
pcmFormat : resultFormat ?? Self.AV_DEFAULT_FORMAT,
frameCapacity: frameLength
)!
result.frameLength = frameLength
let renderStep = 1024
for from in stride(from: 0, to: frameLength, by: renderStep) {
let renderResult = try self.renderOffline(
AVAudioFrameCount(renderStep),
to: buffer
)
switch renderResult {
case .success:
result.segmentSet(
buffer,
from: UInt64(from),
size: UInt64(renderStep)
)
case .error : break
case .insufficientDataFromInputNode: break
case .cannotDoInCurrentContext : break
default : break
}
}
for node in avPlayerNodes {
node.stop()
}
self.stop()
return result
} catch {
return nil
}
}
}
Extension AVAudioPCMBuffer
extension AVAudioPCMBuffer {
func segmentSet(_ data: AVAudioPCMBuffer, from: UInt64 = 0, size: UInt64? = nil) {
let srcFullSize = UInt64(data.frameLength)
let dstFullSize = UInt64(self.frameLength)
var size = size ?? srcFullSize
let from = from.fixBounds(max: dstFullSize)
size = size.fixBounds(max: dstFullSize - from)
for i in 0 ..< UInt64((Float(size) / Float(srcFullSize)).rounded(.up)) {
let sampleSize = Int(data.format.streamDescription.pointee.mBytesPerFrame)
let srcPointer = UnsafeMutableAudioBufferListPointer(data.mutableAudioBufferList)
let dstPointer = UnsafeMutableAudioBufferListPointer(self.mutableAudioBufferList)
let size = min(srcFullSize, size - (i * srcFullSize))
let from = from + (i * srcFullSize)
for (src, dst) in zip(srcPointer, dstPointer) {
memcpy(
dst.mData?.advanced(by: Int(from) * sampleSize),
src.mData,
Int(size) * sampleSize
)
}
}
}
static func etaloneGenerate(channels: UInt8 = 2, size: UInt64 = 1000) -> Self? {
let avFormat = AVAudioFormat(
commonFormat: AVAudioEngine.AV_DEFAULT_LINEAR_PCM_BIT_DEPTH_FORMAT,
sampleRate : AVAudioEngine.AV_DEFAULT_SAMPLERATE,
channels : AVAudioChannelCount(channels),
interleaved : AVAudioEngine.AV_DEFAULT_IS_INTERLEAVED
)!
if let avBuffer = Self(pcmFormat: avFormat, frameCapacity: AVAudioFrameCount(size)) {
avBuffer.frameLength = AVAudioFrameCount(size)
let channelL = avBuffer.floatChannelData![0]
let channelR = avBuffer.floatChannelData![1]
for i in 0 ..< size {
let sample: Float = Float(String(format: "%.3f", 0.001 * Float(i)))!
if (channels >= 1) { channelL[Int(i) * avBuffer.stride] = (-1.0...1.0).contains(+sample) ? +sample : 0 }
if (channels == 2) { channelR[Int(i) * avBuffer.stride] = (-1.0...1.0).contains(-sample) ? -sample : 0 }
}
return avBuffer
}
return nil
}
}
How to get buffer from file:
let avFile = try! AVAudioFile(forReading: FILE_URL_PIANO)
let avBuffer = try! AVAudioPCMBuffer(file: avFile)!