You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
150 lines
6.2 KiB
150 lines
6.2 KiB
import CAudioRing
|
|
import Foundation
|
|
import Testing
|
|
@testable import AudioMatrix
|
|
import MusubiCore
|
|
|
|
@Suite struct RingTests {
|
|
@Test func writesAndReadsAcrossWrap() throws {
|
|
let ring = try #require(musubi_ring_create(8, 2))
|
|
defer { musubi_ring_destroy(ring) }
|
|
var input: [Float] = (0..<12).map(Float.init) // 6 frames
|
|
var output = [Float](repeating: 0, count: 12)
|
|
|
|
#expect(musubi_ring_write(ring, &input, 6) == 6)
|
|
#expect(musubi_ring_read(ring, &output, 4) == 4)
|
|
#expect(Array(output[0..<8]) == (0..<8).map(Float.init))
|
|
// Wraps around the end of the buffer.
|
|
#expect(musubi_ring_write(ring, &input, 6) == 6)
|
|
#expect(musubi_ring_available(ring) == 8)
|
|
#expect(musubi_ring_read(ring, &output, 6) == 6)
|
|
#expect(Array(output[0..<4]) == [8, 9, 10, 11])
|
|
#expect(Array(output[4..<12]) == (0..<8).map(Float.init))
|
|
}
|
|
|
|
@Test func dropsWhatDoesNotFit() throws {
|
|
let ring = try #require(musubi_ring_create(4, 1))
|
|
defer { musubi_ring_destroy(ring) }
|
|
var input: [Float] = [1, 2, 3, 4, 5, 6]
|
|
#expect(musubi_ring_write(ring, &input, 6) == 4)
|
|
#expect(musubi_ring_skip(ring, 3) == 3)
|
|
#expect(musubi_ring_available(ring) == 1)
|
|
}
|
|
}
|
|
|
|
@Suite struct ResamplerTests {
|
|
@Test func unitStepPassesSignalThroughWithDelay() {
|
|
var resampler = Resampler()
|
|
let frames = 64
|
|
let input: [Float] = (0..<frames).flatMap { [Float($0), -Float($0)] }
|
|
var output = [Float](repeating: 0, count: frames * 2)
|
|
|
|
#expect(resampler.inputFramesNeeded(outputFrames: frames, step: 1) == frames)
|
|
input.withUnsafeBufferPointer { resampler.process(input: $0.baseAddress!, output: &output, outputFrames: frames, step: 1) }
|
|
// Three frames of latency from the 4-point interpolation history.
|
|
for frame in 3..<frames {
|
|
#expect(output[frame * 2] == Float(frame - 3))
|
|
#expect(output[frame * 2 + 1] == -Float(frame - 3))
|
|
}
|
|
}
|
|
|
|
@Test func convertsRateAndKeepsFrequency() {
|
|
// 1 kHz at 44.1 kHz in, 48 kHz out.
|
|
let step = 44_100.0 / 48_000.0
|
|
var resampler = Resampler()
|
|
let outFrames = 4800
|
|
let inFrames = resampler.inputFramesNeeded(outputFrames: outFrames, step: step)
|
|
#expect(abs(inFrames - 4410) <= 1)
|
|
|
|
let input: [Float] = (0..<inFrames).flatMap { i -> [Float] in
|
|
let v = Float(sin(2 * Double.pi * 1000 * Double(i) / 44_100))
|
|
return [v, v]
|
|
}
|
|
var output = [Float](repeating: 0, count: outFrames * 2)
|
|
input.withUnsafeBufferPointer { resampler.process(input: $0.baseAddress!, output: &output, outputFrames: outFrames, step: step) }
|
|
|
|
// Compare against an ideal 1 kHz sine at 48 kHz (skipping the start-up latency).
|
|
let latency = 3.0 / step
|
|
var maxError: Float = 0
|
|
for i in 100..<outFrames {
|
|
let expected = Float(sin(2 * Double.pi * 1000 * (Double(i) - latency) / 48_000))
|
|
maxError = max(maxError, abs(output[i * 2] - expected))
|
|
}
|
|
#expect(maxError < 0.01)
|
|
}
|
|
}
|
|
|
|
@Suite struct DriftControllerTests {
|
|
@Test func primesBeforePlaying() {
|
|
var drift = DriftController(sourceRate: 48_000, outputRate: 48_000, targetLatency: 0.02) // 960 frames
|
|
#expect(drift.update(available: 500, needed: 512) == .fill)
|
|
#expect(drift.update(available: 960, needed: 512) == .play(step: 1))
|
|
}
|
|
|
|
@Test func trimsBacklogAfterPrimingWithoutCountingOverrun() {
|
|
var drift = DriftController(sourceRate: 48_000, outputRate: 48_000) // target 960
|
|
#expect(drift.update(available: 6700, needed: 512) == .skip(frames: 5740, step: 1))
|
|
#expect(drift.overruns == 0)
|
|
#expect(drift.update(available: 960, needed: 512) == .play(step: 1))
|
|
}
|
|
|
|
@Test func countsUnderrunAndRefills() {
|
|
var drift = DriftController(sourceRate: 48_000, outputRate: 48_000)
|
|
_ = drift.update(available: 1000, needed: 512)
|
|
#expect(drift.update(available: 100, needed: 512) == .fill)
|
|
#expect(drift.underruns == 1)
|
|
#expect(drift.update(available: 600, needed: 512) == .fill) // still priming
|
|
}
|
|
|
|
@Test func skipsOnOverrun() {
|
|
var drift = DriftController(sourceRate: 48_000, outputRate: 48_000)
|
|
_ = drift.update(available: 1000, needed: 512)
|
|
#expect(drift.update(available: 5000, needed: 512) == .skip(frames: 4040, step: 1))
|
|
#expect(drift.overruns == 1)
|
|
}
|
|
|
|
/// Sender clock 200 ppm fast: the fill level must stay bounded without under- or overruns.
|
|
@Test func followsClockDrift() {
|
|
let outputRate = 48_000.0, sourceRate = 48_000.0 * 1.0002
|
|
var drift = DriftController(sourceRate: 48_000, outputRate: outputRate)
|
|
let block = 512
|
|
var fill = 960.0
|
|
var produced = 0.0
|
|
for _ in 0..<(60 * 94) { // ~60 s of 512-frame cycles
|
|
produced += sourceRate * Double(block) / outputRate
|
|
let whole = produced.rounded(.down)
|
|
fill += whole
|
|
produced -= whole
|
|
var resampler = Resampler()
|
|
switch drift.update(available: Int(fill), needed: block + 4) {
|
|
case .play(let step):
|
|
fill -= Double(resampler.inputFramesNeeded(outputFrames: block, step: step))
|
|
case .fill:
|
|
break
|
|
case .skip(let frames, let step):
|
|
fill -= Double(frames) + Double(resampler.inputFramesNeeded(outputFrames: block, step: step))
|
|
}
|
|
}
|
|
#expect(drift.underruns == 0)
|
|
#expect(drift.overruns == 0)
|
|
#expect(fill > 400 && fill < 2000)
|
|
#expect(drift.correction > 0)
|
|
}
|
|
}
|
|
|
|
@Suite struct LevelTests {
|
|
@Test func convertsDecibels() {
|
|
#expect(Level.gain(fromDB: 0) == 1)
|
|
#expect(abs(Level.gain(fromDB: -6) - 0.501) < 0.001)
|
|
#expect(Level.gain(fromDB: -60) == 0)
|
|
#expect(Level.db(fromGain: 0) == Level.floorDB)
|
|
#expect(abs(Level.db(fromGain: 0.5) + 6.02) < 0.01)
|
|
}
|
|
|
|
@Test func selectsChannels() {
|
|
#expect(ChannelSelection.left.sample(left: 1, right: 0) == 1)
|
|
#expect(ChannelSelection.right.sample(left: 1, right: 0) == 0)
|
|
#expect(ChannelSelection.mono.sample(left: 1, right: 0) == 0.5)
|
|
}
|
|
}
|