4

I am trying to figure out how to use Apple's Core Audio APIs to record and play back linear PCM audio without any file I/O. (The recording side seems to work just fine.)

The code I have is pretty short, and it works somewhat. However, I am having trouble with identifying the source of clicks and pops in the output. I've been beating my head against this for many days with no success.

I have posted a git repo here, with a command-line program program that shows where I'm at: https://github.com/maxharris9/AudioRecorderPlayerSwift/tree/main/AudioRecorderPlayerSwift

I put in a couple of functions to prepopulate the recording. The tone generator (makeWave) and noise generator (makeNoise) are just in here as debugging aids. I'm ultimately trying to identify the source of the messed up output when you play back a recording in audioData:

// makeWave(duration: 30.0, frequency: 441.0) // appends to `audioData`
// makeNoise(frameCount: Int(44100.0 * 30)) // appends to `audioData`
_ = Recorder() // appends to `audioData`
_ = Player() // reads from `audioData`

Here's the player code:

var lastIndexRead: Int = 0

func outputCallback(inUserData: UnsafeMutableRawPointer?, inAQ: AudioQueueRef, inBuffer: AudioQueueBufferRef) {
    guard let player = inUserData?.assumingMemoryBound(to: Player.PlayingState.self) else {
        print("missing user data in output callback")
        return
    }

    let sliceStart = lastIndexRead
    let sliceEnd = min(audioData.count, lastIndexRead + bufferByteSize - 1)
    print("slice start:", sliceStart, "slice end:", sliceEnd, "audioData.count", audioData.count)

    if sliceEnd >= audioData.count {
        player.pointee.running = false
        print("found end of audio data")
        return
    }

    let slice = Array(audioData[sliceStart ..< sliceEnd])
    let sliceCount = slice.count

    // doesn't fix it
    // audioData[sliceStart ..< sliceEnd].withUnsafeBytes {
    //     inBuffer.pointee.mAudioData.copyMemory(from: $0.baseAddress!, byteCount: Int(sliceCount))
    // }

    memcpy(inBuffer.pointee.mAudioData, slice, sliceCount)
    inBuffer.pointee.mAudioDataByteSize = UInt32(sliceCount)
    lastIndexRead += sliceCount + 1

    // enqueue the buffer, or re-enqueue it if it's a used one
    check(AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nil))
}

struct Player {
    struct PlayingState {
        var packetPosition: UInt32 = 0
        var running: Bool = false
        var start: Int = 0
        var end: Int = Int(bufferByteSize)
    }

    init() {
        var playingState: PlayingState = PlayingState()
        var queue: AudioQueueRef?

        // this doesn't help
        // check(AudioQueueNewOutput(&audioFormat, outputCallback, &playingState, CFRunLoopGetMain(), CFRunLoopMode.commonModes.rawValue, 0, &queue))

        check(AudioQueueNewOutput(&audioFormat, outputCallback, &playingState, nil, nil, 0, &queue))

        var buffers: [AudioQueueBufferRef?] = Array<AudioQueueBufferRef?>.init(repeating: nil, count: BUFFER_COUNT)

        print("Playing\n")
        playingState.running = true

        for i in 0 ..< BUFFER_COUNT {
            check(AudioQueueAllocateBuffer(queue!, UInt32(bufferByteSize), &buffers[i]))
            outputCallback(inUserData: &playingState, inAQ: queue!, inBuffer: buffers[i]!)

            if !playingState.running {
                break
            }
        }

        check(AudioQueueStart(queue!, nil))

        repeat {
            CFRunLoopRunInMode(CFRunLoopMode.defaultMode, BUFFER_DURATION, false)
        } while playingState.running

        // delay to ensure queue emits all buffered audio
        CFRunLoopRunInMode(CFRunLoopMode.defaultMode, BUFFER_DURATION * Double(BUFFER_COUNT + 1), false)

        check(AudioQueueStop(queue!, true))
        check(AudioQueueDispose(queue!, true))
    }
}

I captured the audio with Audio Hijack, and noticed that the jumps are indeed correlated with the size of the buffer: captured audio output using Audio Hijack and Sound Studio

Why is this happening, and what can I do to fix it?

Tara Harris
  • 85
  • 10
  • It appears that my question is similar to this one from four years ago, which *also* was never answered successfully: https://stackoverflow.com/questions/38509920/popping-noise-between-audioqueuebuffers?rq=1 – Tara Harris Nov 19 '20 at 06:28
  • 1
    I suspect I know the answer. Just going to take a peek at your repo. Will get back to you soon. – idz Nov 19 '20 at 06:53

1 Answers1

3

I believe you were beginning to zero in on, or at least suspect, the cause of the popping you are hearing: it's caused by discontinuities in your waveform.

My initial hunch was that you were generating the buffers independently (i.e. assuming that each buffer starts at time=0), but I checked out your code and it wasn't that. I suspect some of the calculations in makeWave were at fault. To check this theory I replaced your makeWave with the following:

func makeWave(offset: Double, numSamples: Int, sampleRate: Float64, frequency: Float64, numChannels: Int) -> [Int16] {
    var data = [Int16]()
    for sample in 0..<numSamples / numChannels {
        // time in s
        let t = offset + Double(sample) / sampleRate
        let value = Double(Int16.max) * sin(2 * Double.pi * frequency * t)
        for _ in 0..<numChannels {
            data.append(Int16(value))
        }
    }
    return data
}

This function removes the double loop in the original, accepts an offset so it knows which part of the wave is being generated and makes some changes to the sampling of the sine wave.

When Player is modified to use this function you get a lovely steady tone. I'll add the changes to player soon. I can't in good conscience show the quick and dirty mess it is now to the public.


Based on your comments below I refocused on your player. The issue was that the audio buffers expect byte counts but the slice count and some other calculations were based on Int16 counts. The following version of outputCallback will fix it. Concentrate on the use of the new variable bytesPerChannel.

func outputCallback(inUserData: UnsafeMutableRawPointer?, inAQ: AudioQueueRef, inBuffer: AudioQueueBufferRef) {
    guard let player = inUserData?.assumingMemoryBound(to: Player.PlayingState.self) else {
        print("missing user data in output callback")
        return
    }

    let bytesPerChannel = MemoryLayout<Int16>.size
    let sliceStart = lastIndexRead
    let sliceEnd = min(audioData.count, lastIndexRead + bufferByteSize/bytesPerChannel)

    if sliceEnd >= audioData.count {
        player.pointee.running = false
        print("found end of audio data")
        return
    }

    let slice = Array(audioData[sliceStart ..< sliceEnd])
    let sliceCount = slice.count
    
        print("slice start:", sliceStart, "slice end:", sliceEnd, "audioData.count", audioData.count, "slice count:", sliceCount)

    // need to be careful to convert from counts of Ints to bytes
    memcpy(inBuffer.pointee.mAudioData, slice, sliceCount*bytesPerChannel)
    inBuffer.pointee.mAudioDataByteSize = UInt32(sliceCount*bytesPerChannel)
    lastIndexRead += sliceCount

    // enqueue the buffer, or re-enqueue it if it's a used one
    check(AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, nil))
}

I did not look at the Recorder code, but you may want to check if the same sort of error crept in there.

idz
  • 12,825
  • 1
  • 29
  • 40
  • Whoa, thanks! That tone generator was really there just to help debug another playback issue with recorded audio. You can hear the original playback issue I was trying to solve if you invoke the recorder and player in series, and use it to record and play back something else, such as music played over the system speakers: ``` _ = Recorder(); _ = Player(); ``` – Tara Harris Nov 19 '20 at 08:57
  • it is very late, and I'm very tired, and I'm typing this in the dark, and I'm going to re-read your reply tomorrow morning – Tara Harris Nov 19 '20 at 09:03
  • 2
    Hmmm! That may be a separate issue. Try to mention such things in the question in future. – idz Nov 19 '20 at 09:03
  • 1
    Later here too. Might take a peek at the other issue tomorrow. – idz Nov 19 '20 at 09:04
  • I had put it in the original one, but I haven't used stack overflow in years, and I edited it out because I was worried that my question was bad, and wouldn't get attention. I will definitely put some of this back in the description to help other people :facepalm: - good night https://stackoverflow.com/posts/64865774/revisions – Tara Harris Nov 19 '20 at 09:05
  • 1
    No worries, g'night! – idz Nov 19 '20 at 09:15
  • 1
    You’re welcome, To be honest I completely missed the issue when I first looked at your code. Good luck with your project! – idz Nov 19 '20 at 17:21