'How to send an audio file to a server while the user is recording from the microphone?

When the user records an audio note from the microphone, I would like to send the audio file that corresponds to the last x seconds of recording to my server

From my research I have globally understood that I should use AVAudioEngine with the installTap function. My code looks like this :

func start() {
    engine = AVAudioEngine()
    guard let inputNode = engine?.inputNode else { return }
    let bus = 0
    let format = inputNode.inputFormat(forBus: bus)
    engine?.inputNode.installTap(onBus: bus, bufferSize: 2048, format: format) { pcmBuffer, audioTime in
    // This is were I would like to send the audio to my server
    }
    engine?.prepare()
    do {
      try engine?.start()
    } catch {
      print("error:", error.localizedDescription)
    }
  }

However, the closure returns an AVAudioPCMBuffer & AVAudioTime and I can't find any way to convert it into an audio file (m4a, mp3, whatever...). Are there any APIs that could allow me to do this?

I also considered AVAudioRecorder, the problem is that if the audio recording is long, it will take a long time to be sent to my server... That's why I turned to a more on-the-fly solution

Thank you for your help



Solution 1:[1]

I could not find if there was an encoder for m4a or mp3, however I could find a solution to convert the buffer into an aac file.

I think aac is similar if you were to implement mp3 as they could just be chunked together end to end in sequence and they could be played back seamlessly.

So my thought process is:

  1. Create a new aac file in the documents directory and keep it open
  2. Start recording and append the input buffer into this aac file
  3. After x seconds has elapsed, close the current open file
  4. After closing the file, upload the last file to your server
  5. repeat from step 1

The resources I used which will help you understand what I did:

Implementation

You may have done this already, but just for anyone else stumbling upon this, first, add the right permissions in info.plist to get access to the microphone:

    <key>NSMicrophoneUsageDescription</key>
    <string>Test app needs to access your microphone</string>

Next, here are some variables I keep track of:

// AVAudioEngine used to record
var engine = AVAudioEngine()

// Set this as per your liking (512, 1024, 2048)
let estimatedBufferSize: AVAudioFrameCount = 1024

// Will be configured to write the buffer to a file
var file: AVAudioFile?

// Chunk duration in seconds, adjust as needed
let chunkDuration: Float64 = 10

// Will be used to uniquely name the different chunks
var currentChunkCount = 0

// Keeps track of how many frames in current chunk
// Used to check how much time has elapsed
var framesInCurrentChunk: AVAudioFrameCount = 0

Here is the rest of the logic with comments

// Wire this to your start recording button
@objc
private func startRecording()
{
    print("start recording")
    
    // Prepare how the AVAudioEngine should process input
    engine.inputNode.installTap(onBus: 0,
                                bufferSize: 1024,
                                format: engine.inputNode.inputFormat(forBus: 0))
    { [weak self] (buffer, time) -> Void in
        
        // Write the buffer to your file
        self?.writeBufferToFile(buffer: buffer)
    }
    
    // Start recording
    try! engine.start()
}

// Wire this to your stop recording button
@objc
private func stopAndPlayRecording()
{
    print("stop recording")
    
    // Clean up and reset
    engine.inputNode.removeTap(onBus: 0)
    engine.stop()
    file = nil
    framesInCurrentChunk = 0
    
    // Here is where you should upload any files that have not been uploaded
    // Delete files after upload if you want or manage file name duplicates
}

private func writeBufferToFile(buffer: AVAudioPCMBuffer)
{
    let samplesPerSecond = buffer.format.sampleRate
    
    // Check if we have an open file writer
    if file == nil
    {
        // Configure an AVAudioFile to write the audio buffer to file
        prepareOutputFile()
    }
    
    do
    {
        try file?.write(from: buffer)
        framesInCurrentChunk += buffer.frameLength
    }
    catch
    {
        // error appending the chunk to file
        print(error)
    }
    
    // Check if the current chunk has reached it's duration
    if framesInCurrentChunk > AVAudioFrameCount(chunkDuration * samplesPerSecond)
    {
        // Here is where you have a valid chunk that has been saved in
        // the duration you want, put the last saved aac file in a queue to be
        // uploaded to your server here
        
        // De-initialize the current file writer so we can start a new one
        file = nil
    }
}

private func prepareOutputFile()
{
    // Increment the current chunk count to create a new file
    currentChunkCount += 1
    
    // Set the path of where the file will be stored in the document directory
    let documentsURL = FileManager.default.urls(for: .documentDirectory,
                                                in: .userDomainMask)[0]
    
    let outputURL = documentsURL.appendingPathComponent("recording_\(currentChunkCount).aac")
    
    print("Recording audio to path: \(outputURL)")
    
    do
    {
        // Configure the AVAudioFile with the output path and output format
        file = try AVAudioFile(forWriting: outputURL,
                               settings: [AVFormatIDKey: kAudioFormatMPEG4AAC])
        
        
    }
    catch
    {
        // Handle errors in configuring the the AVAudioFile
        print(error)
    }
    
    // Reset the frames saved in the current chunk
    framesInCurrentChunk = 0
}

If you run this on an iOS device, you should see it print out the path of the file currently recording the input stream once you run the function startRecording()

After you run stopAndPlayRecording(), you can check your documents directory and you will see the AAC files saved in your documents directory which can be uploaded. Here is how you can check that on a device

Here is an output from my test:

AVAudioEngine AVAudioFile record from microphone to mp3 aac in documents directory and upload chunks to server

Final thoughts

  • You need to handle errors at your end, I kept this short
  • You might want to delete the files using some logic to avoid overwriting of recording

Give this a try and let me know in the comments if this worked at your end or I could prepare a test project if that helps.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1