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

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)
}
}

Powered by TurnKey Linux.