1

You can view this project on github here: https://github.com/Lkember/MotoIntercom/

The class that is of importance is PhoneViewController.swift

I have an AVAudioPCMBuffer. The buffer is then converted to NSData using this function:

func audioBufferToNSData(PCMBuffer: AVAudioPCMBuffer) -> NSData {
    let channelCount = 1
    let channels = UnsafeBufferPointer(start: PCMBuffer.floatChannelData, count: channelCount)
    let data = NSData(bytes: channels[0], length:Int(PCMBuffer.frameCapacity * PCMBuffer.format.streamDescription.pointee.mBytesPerFrame))

    return data
}

This data needs to be converted to UnsafePointer< UInt8 > according to the documentation on OutputStream.write.

https://developer.apple.com/reference/foundation/outputstream/1410720-write

This is what I have so far:

let data = self.audioBufferToNSData(PCMBuffer: buffer)
let output = self.outputStream!.write(UnsafePointer<UInt8>(data.bytes.assumingMemoryBound(to: UInt8.self)), maxLength: data.length)

When this data is received, it is converted back to an AVAudioPCMBuffer using this method:

func dataToPCMBuffer(data: NSData) -> AVAudioPCMBuffer {

    let audioFormat = AVAudioFormat(commonFormat: AVAudioCommonFormat.pcmFormatFloat32, sampleRate: 8000, channels: 1, interleaved: false)  // given NSData audio format
    let audioBuffer = AVAudioPCMBuffer(pcmFormat: audioFormat, frameCapacity: UInt32(data.length) / audioFormat.streamDescription.pointee.mBytesPerFrame)
    audioBuffer.frameLength = audioBuffer.frameCapacity
    let channels = UnsafeBufferPointer(start: audioBuffer.floatChannelData, count: Int(audioBuffer.format.channelCount))
    data.getBytes(UnsafeMutableRawPointer(channels[0]) , length: data.length)
    return audioBuffer
}

Unfortunately, when I play this audioBuffer, I only hear static. I don't believe that it is an issue with my conversion from AVAudioPCMBuffer to NSData or my conversion from NSData back to AVAudioPCMBuffer. I imagine it is the way that I am writing NSData to the stream.

The reason I don't believe that it is my conversion is because I have created a sample project located here (which you can download and try) that records audio to an AVAudioPCMBuffer, converts it to NSData, converts the NSData back to AVAudioPCMBuffer and plays the audio. In this case there are no problems playing the audio.


EDIT:

I never showed how I actually get Data from the stream as well. Here is how it's done:

func stream(_ aStream: Stream, handle eventCode: Stream.Event) {
    switch (eventCode) {

    case Stream.Event.hasBytesAvailable:
        DispatchQueue.global().async {

            var buffer = [UInt8](repeating: 0, count: 8192)
            let length = self.inputStream!.read(&buffer, maxLength: buffer.count)

            let data = NSData.init(bytes: buffer, length: buffer.count)

            print("\(#file) > \(#function) > \(length) bytes read on queue \(self.currentQueueName()!) buffer.count \(data.length)")

            if (length > 0) {
                let audioBuffer = self.dataToPCMBuffer(data: data)

                self.audioPlayerQueue.async {
                    self.peerAudioPlayer.scheduleBuffer(audioBuffer)

                    if (!self.peerAudioPlayer.isPlaying && self.localAudioEngine.isRunning) {
                        self.peerAudioPlayer.play()
                    }
                }
            }
            else if (length == 0) {
                print("\(#file) > \(#function) > Reached end of stream")
            }
        }

Once I have this data, I use the dataToPCMBuffer method to convert it to an AVAudioPCMBuffer.


EDIT 1:

Here is the AVAudioFormat's that I use:

self.localInputFormat = AVAudioFormat.init(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 1, interleaved: false)

Originally, I was using this:

self.localInputFormat = self.localInput?.inputFormat(forBus: 0)

However, if the channel count does not equal the expected channel count, than I was getting crashes. So I switched it to the above.

The actual AVAudioPCMBuffer I'm using is in the installTap method (where localInput is an AVAudioInputNode):

localInput?.installTap(onBus: 0, bufferSize: 4096, format: localInputFormat) {
        (buffer, time) -> Void in
Kember
  • 63
  • 9
  • Please update your question with the details of how the original AVAudioPCMBuffer was created - specifically the AVAudioFormat and AVAudioPCMBuffer parameters. – quellish Mar 22 '17 at 07:22
  • @quellish I added an update to the end of my question. – Kember Mar 22 '17 at 14:41
  • What is the value of `output` (the return of write)? You might need to loop to write all of the bytes. – Lou Franco Mar 27 '17 at 09:42
  • Similarly, does the length of data in dataToPCMBuffer match the length of data you output? – Lou Franco Mar 27 '17 at 09:44
  • @LouFranco the value of output is the number of bytes that are written onto the stream. But yes, the value of output is 17640, which is equal to the size of "data". – Kember Mar 27 '17 at 15:27
  • @Kember use `NSData`'s `write​To​File:​atomically:​` to save before you output and also when you input into test files. Then diff those files to make sure they are the same. If so, the problem is not in transfer. – Lou Franco Mar 27 '17 at 15:34
  • @LouFranco okay, so write the NSData to a file before I output it. And you're saying to also write the input from the stream to a file? But I can't check to see if those files are the same since the files will be on different devices. The reason I don't believe that there is a problem with the NSData conversion is because I have a sample project (there is a link to it in the project) that records audio, converts it to NSData, converts it back to AVAudioPCMBuffer and plays the audio, with no issues. – Kember Mar 27 '17 at 17:54
  • Get the file from the other device using https://macroplant.com/iexplorer or something similar. You can use `diff -b` to see if they are byte-to-byte exactly the same. Since this code works if you just do the conversion all in one app, it has to be that they are different -- that difference will be a clue to what happened. – Lou Franco Mar 27 '17 at 19:26
  • @LouFranco Well, my assumption is that they are different byte to byte. The reason I think this is because based on my sample project, the AVAudioPCMBuffer to NSData conversion works fine. The only other thing I do with the data is this: UnsafePointer(data.bytes.assumingMemoryBound(to: UInt8.self))---------- Meaning I must be doing something wrong here. – Kember Mar 27 '17 at 19:34
  • I completely agree -- knowing what the difference is will be a big clue to what is wrong. Also, have you shown us how you read the stream and make the NSData object? – Lou Franco Mar 27 '17 at 19:35
  • @LouFranco I must've forgot, I've added the code as an edit. – Kember Mar 27 '17 at 20:03
  • on the input side, what is the return value of `self.inputStream!.read` (what you store in the length var)? – Lou Franco Mar 27 '17 at 21:44
  • @LouFranco So on the input side, the value of read varies a lot. It's never 17640, it's usually a smaller number, but it reads from the output stream far more often than it writes to it. So for example, I just ran the program and here are the numbers I got (without a write). 6570, 5475, 3285, 2190, 120, and then there was a write of 17640. The sum of all the numbers previously is 17640. So I assume all of the bytes are being read as well. – Kember Mar 28 '17 at 14:20
  • @kember -- is there a loop around your input read? You are supposed to use that length when you init the data: `let data = NSData.init(bytes: buffer, length: length)` (and also check to make sure you are done) -- it's hard to tell what to do exactly because I don't have the loop and the way you are making the final data. – Lou Franco Mar 28 '17 at 14:31
  • @LouFranco I can post the code in the question or I could link you to my github? – Kember Mar 28 '17 at 15:02
  • Please copy the loop around the input to the question (and link to github too) – Lou Franco Mar 28 '17 at 15:09
  • @LouFranco Okay, I've added a link to the github project at the top (the relevant class is PhoneViewController), and in the first edit I've added all of the relevant code. And thank you so much for looking into this issue. I've been stuck on it for months now. – Kember Mar 28 '17 at 15:20
  • ok -- I am pretty sure you just need to use `length` to init the data (made an answer) -- let me know how it goes – Lou Franco Mar 28 '17 at 16:15

1 Answers1

1

Pretty sure you want to replace this:

let length = self.inputStream!.read(&buffer, maxLength: buffer.count)
let data = NSData.init(bytes: buffer, length: buffer.count)

With

let length = self.inputStream!.read(&buffer, maxLength: buffer.count)
let data = NSData.init(bytes: buffer, length: length)

Also, I am not 100% sure that the random blocks of data will always be ok to use to make the audio buffers. You might need to collect up the data first into a bigger block of NSData.

Right now, since you always pass in blocks of 8192 (even if you read less), the buffer creation probably always succeeds. It might not now.

Lou Franco
  • 87,846
  • 14
  • 132
  • 192
  • thank you for looking into this issue. I made the changes you suggested, unfortunately the issue still persists. Oddly enough, I hear static on one phone for a few seconds and then it just goes blank, even though I can see that it is still receiving and sending data. Actually, I am receiving an error every so often now. Here is what it says: MotoIntercom(960,0x16e4df000) malloc: *** error for object 0x1741331a0: Invalid pointer dequeued from free list *** set a breakpoint in malloc_error_break to debug – Kember Mar 28 '17 at 17:47
  • @kember I don't think you can just read in some bytes and get an audio buffer. I think you should first write out 4-bytes with the length of the data you are sending. And then follow that with the data. On the input side, read in 4 bytes, convert that to a count and then read in that many bytes and turn that into one NSData with that exact number of bytes. Then turn that NSData into an audio buffer. The problem is that the random stream chunking isn't compatible with the audio object creation. – Lou Franco Mar 28 '17 at 18:24
  • So you think I should first write some sort of string to the other client letting it know how big the upcoming data is? How do you read the next x number of bytes on a stream? In my stream.hasBytesAvailable, it just reads as many bytes as it can as soon as the bytes come in. I should also mention, strangely enough, whenever I actually use the microphone, the audio on the receiving end goes blank. It's very strange. – Kember Mar 28 '17 at 18:31
  • I would write an Int, not a string. On the other side, read the Int, then keep looping around the input.read with the `length == (expectedLen - bytesReadSoFar)`. Append onto the NSData until you read in the same # of bytes that you wrote. Then `bytesReadSoFar += bytesJustRead` If that is smaller than expected, keep reading. – Lou Franco Mar 28 '17 at 18:47
  • Well I know how much every write is before I even write (17640) so I can probably skip the step of writing an Int, do you think? – Kember Mar 28 '17 at 18:57
  • It's always 17640? If so, then yes, you can skip -- on the other side, don't stop reading until you fill up an NSData with 17640 bytes. – Lou Franco Mar 28 '17 at 19:15
  • Okay, so you were definitely right! The issue is that I have to receive the data in the exact amount of bytes as it was sent. However, my new issue is that the audio is VERY jittery. You can't make out any audio, only that when you're talking, it goes louder on the other device. Any ideas? – Kember Mar 28 '17 at 20:03
  • You probably need to queue up enough samples so you can play them without waiting. – Lou Franco Mar 28 '17 at 23:46
  • I suppose I could do that. However, that would make a larger delay between the devices. Is the issue that the iPhone can't transfer the data fast enough? Suppose I found a way to compress the audio (which I have no idea how to do at the moment), would this reduce the latency and jittering? – Kember Mar 29 '17 at 13:22
  • Perhaps, but then you have the compression/decompression time. I would experiment with a very short delay -- might be enough. – Lou Franco Mar 29 '17 at 13:30
  • So I tried adding a counter, and basically once x number of PCMBuffers were scheduled on the AVAudioEngine I would play the audio, and it still jitters badly. I changed x to be as low as 3 and as high as 20 (to the point where there was 2-3 second delay).. I'm not sure why this is happening. – Kember Mar 29 '17 at 14:13
  • Sorry, out of ideas. My main concern with the code in the question is how you are getting frameLength and frameCapacity. I would compare that to your test version that works and make sure it matches – Lou Franco Mar 29 '17 at 14:22
  • Well thank you for all your help, you solved the main issue, I guess I can open another question for the rest. – Kember Mar 29 '17 at 14:36