Introduction

Building a custom audio recorder and player in iOS gives you full control over the capture and playback experience, enabling features such as real‑time waveform display, custom compression settings, background recording, or seamless integration with voice‑based interactions. The AVFoundation framework provides a robust, low‑overhead API for managing audio sessions, recording from the microphone, and playing back files. By leveraging AVAudioRecorder and AVAudioPlayer, you can create a production‑quality audio component that respects iOS resource constraints and user privacy.

This article covers every essential step—from requesting microphone permission and configuring the audio session to handling playback controls and advanced error recovery. You will learn how to choose optimal recording settings, manage file storage, and implement delegate methods for real‑time feedback. Each section includes concrete Swift code snippets that you can adapt directly into your project.

Understanding AVFoundation

AVFoundation is Apple’s primary multimedia framework for iOS, macOS, and tvOS. For audio recording and playback, the most important classes are:

  • AVAudioRecorder – Records audio from the built‑in microphone or other input sources. It supports a wide variety of formats and can provide average and peak power levels for metering.
  • AVAudioPlayer – Plays audio files from a file URL or data buffer. It offers synchronous and asynchronous playback, looping, and delegate callbacks for state changes.
  • AVAudioSession – Manages the app’s interaction with the device’s audio hardware. It is essential to configure the session category, mode, and options before any recording or playback operation.

All these classes operate asynchronously, so you must handle delegate methods (or use Combine publishers in modern Swift) to respond to events like recording interruption or playback completion.

Setting Up Microphone Permissions

Starting with iOS 17, user privacy remains a top priority. Before your app can record audio, you must ask for explicit permission and update the Info.plist file.

Info.plist Entry

Add the following key to your Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access to record your voice notes.</string>

The description string must clearly explain why the app requires microphone access, as it is displayed in the system permission dialog.

Requesting Permission in Code

Use AVAudioSession.sharedInstance().requestRecordPermission(_:) to prompt the user. The closure returns a Boolean indicating whether permission was granted.

import AVFoundation

AVAudioSession.sharedInstance().requestRecordPermission { granted in
    DispatchQueue.main.async {
        if granted {
            // Permission granted – proceed with recording setup
        } else {
            // Show an alert explaining that recording is disabled
        }
    }
}

Always check permission status on view appear or before starting a recording. If permission is denied, guide the user to Settings using UIApplication.openSettingsURLString.

Configuring the Audio Session

The audio session defines how your app interacts with the device’s audio hardware. For recording and simultaneous playback (e.g., while reading instructions), set the category to .playAndRecord and select an appropriate mode and options.

let session = AVAudioSession.sharedInstance()
do {
    try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker, .allowBluetooth])
    try session.setActive(true)
} catch {
    // Handle error (e.g., print to console, update UI)
    print("Failed to configure audio session: \(error.localizedDescription)")
}
  • Category .playAndRecord: Allows both recording and playback simultaneously.
  • Mode .default: Suitable for most use cases; other modes like .voiceChat optimize for voice communication.
  • Options: Use .defaultToSpeaker to route audio to the built‑in speaker instead of the earpiece, and .allowBluetooth to support Bluetooth headsets.
  • Always call setActive(true) before recording or playing. Deactivate the session when no longer needed to free system resources.

Note: If your app is interrupted by a phone call or alarm, the session will be deactivated. Listen for AVAudioSession.interruptionNotification and handle pause/resume logic.

Implementing the Audio Recorder

The AVAudioRecorder requires a file URL to store the recorded data and a dictionary of settings that define the audio format.

Creating the Recorder

let settings: [String: Any] = [
    AVFormatIDKey: Int(kAudioFormatMPEG4AAC),           // AAC is a compressed, high-quality format
    AVSampleRateKey: 44100,                              // 44.1 kHz is standard for music; 16000 for voice
    AVNumberOfChannelsKey: 2,                            // 2 for stereo, 1 for mono (microphone is mono)
    AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue,
    AVEncoderBitRateKey: 128000                          // 128 kbps for AAC
]

let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let audioFilename = documentsPath.appendingPathComponent("recording-\(Date().timeIntervalSince1970).m4a")

var audioRecorder: AVAudioRecorder?
do {
    audioRecorder = try AVAudioRecorder(url: audioFilename, settings: settings)
    audioRecorder?.delegate = self
    audioRecorder?.prepareToRecord()  // Prepares the recording buffer (recommended)
    audioRecorder?.record(forDuration: 3600) // Optional: limit recording to 1 hour
} catch {
    print("AudioRecorder initialization failed: \(error.localizedDescription)")
}

Recording Lifecycle

  • record() – Starts recording immediately.
  • record(forDuration:) – Records for a specified number of seconds, then stops automatically.
  • pause() – Pauses without finalizing the file; resume with record().
  • stop() – Ends the recording and closes the file. After stopping, the file is ready for playback.
  • deleteRecording() – Deletes the recorded file and resets the recorder.

Use the AVAudioRecorderDelegate methods audioRecorderDidFinishRecording(_:successfully:) and audioRecorderEncodeErrorDidOccur(_:error:) to react to completion or errors.

Selecting Optimal Recording Settings

The settings dictionary drastically affects file size, quality, and compatibility. Common format choices:

  • AAC (kAudioFormatMPEG4AAC): Best for general‑purpose voice and music. High compression with good quality.
  • Apple Lossless (kAudioFormatAppleLossless): For archiving high‑fidelity recordings.
  • Linear PCM (kAudioFormatLinearPCM): Uncompressed, large files, but no quality loss.
  • Opus (kAudioFormatOpus): Excellent for low‑bandwidth voice (iOS 14+).

Set AVSampleRateKey to 44100 for music, 16000 for speech‑only apps. Mono (1 channel) is usually sufficient for voice recording and reduces file size by half.

Handling Recording State and UI

Users need clear feedback about the recording state. Use a simple state machine:

enum RecordingState {
    case ready
    case recording
    case paused
    case finished
}

Update UI elements (button images, labels, timers) based on this state. Implement a Timer to display elapsed time while recording:

var recordingTimer: Timer?
var elapsedSeconds = 0

func startTimer() {
    recordingTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
        self.elapsedSeconds += 1
        let time = self.formatTime(seconds: self.elapsedSeconds)
        self.timeLabel.text = time
    }
}

Remember to invalidate the timer when recording stops or is paused.

Implementing Audio Playback

AVAudioPlayer plays audio from a local file URL. It is lightweight and provides built‑in support for loops, volume control, and metering.

Initializing the Player

var audioPlayer: AVAudioPlayer?

do {
    audioPlayer = try AVAudioPlayer(contentsOf: recordedFileURL)
    audioPlayer?.delegate = self
    audioPlayer?.prepareToPlay()  // Pre‑loads audio data into memory
    audioPlayer?.volume = 1.0
} catch {
    print("AudioPlayer initialization failed: \(error.localizedDescription)")
}

Basic Playback Controls

  • play() – Starts playback from the current position.
  • pause() – Pauses playback; current position is retained.
  • stop() – Stops playback and resets the current position to the beginning.
  • currentTime – A read‑write property to seek to a specific time (in seconds).
  • duration – Read‑only, returns the total file length.
  • numberOfLoops – Set to a negative value for infinite loop, or a positive integer for a finite number of repeats.
  • enableRate – Set to true and then adjust rate (0.5 to 2.0) for slow/fast playback.
// Play from the beginning
audioPlayer?.currentTime = 0
audioPlayer?.play()

// Fast‑forward to 30 seconds
audioPlayer?.currentTime = 30

// Enable speed control
audioPlayer?.enableRate = true
audioPlayer?.rate = 1.5

Playback Delegates

Conform to AVAudioPlayerDelegate to know when playback finishes or encounters an error:

func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
    if flag {
        // Update UI to show playback ended
    } else {
        // Playback was interrupted (e.g., by a phone call)
    }
}

func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: Error?) {
    // Handle decoding errors (corrupted file, unsupported format)
}

Error Handling and Edge Cases

Robust error handling is critical for a professional audio app. Common failure points include:

  • Permission denied: The recorder will fail if the microphone is not authorized. Check permission before initializing AVAudioRecorder.
  • Session activation failure: If another app (e.g., a phone call) has taken over the audio hardware, setActive(true) throws an error. Catch it and retry after the interruption ends.
  • File system issues: Ensure the output directory exists and is writable. For example:
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let recordingsPath = documentsPath.appendingPathComponent("Recordings")
try? FileManager.default.createDirectory(at: recordingsPath, withIntermediateDirectories: true, attributes: nil)
  • Invalid file format for player: AVAudioPlayer supports a limited set of formats (CAF, WAV, AIFF, MP3, AAC, etc.). Ensure the recorded file uses one of these.
  • Interruptions: Register for AVAudioSession.interruptionNotification and pause/resume recording or playback accordingly.
NotificationCenter.default.addObserver(self,
                                       selector: #selector(handleInterruption),
                                       name: AVAudioSession.interruptionNotification,
                                       object: AVAudioSession.sharedInstance())

@objc func handleInterruption(notification: Notification) {
    guard let userInfo = notification.userInfo,
          let type = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt else { return }
    if AVAudioSession.InterruptionType(rawValue: type) == .began {
        // Pause recording/playback and disable controls
    } else {
        // Interruption ended – optionally resume
    }
}

File Management and Storage

Recorded files accumulate quickly. Implement a strategy to manage disk usage:

  • Naming: Use timestamps or UUIDs to avoid collisions.
  • Deletion: Provide a button or swipe‑to‑delete gesture in your list of recordings.
  • Export: Let users share recordings via UIActivityViewController or save to Files app.
  • Compression: Optionally re‑encode larger files into a smaller format after recording using AVAudioConverter.

Advanced Features

Level Metering

Enable isMeteringEnabled = true on the recorder or player, then call updateMeters() periodically to get average and peak power levels. These values are in decibels (range about -160 to 0). Use them to drive a visual waveform or level indicator.

audioRecorder?.isMeteringEnabled = true

func updateMeter() {
    audioRecorder?.updateMeters()
    let averagePower = audioRecorder?.averagePower(forChannel: 0) ?? -160
    // Convert dB to 0.0-1.0 scale for UI
    let normalized = pow(10, averagePower / 20)
    levelIndicator.progress = Float(normalized)
}

Background Recording

If your app needs to record while in the background (e.g., voice memo app), enable the “Audio, AirPlay, and Picture in Picture” background mode in the Capabilities tab of Xcode. Then set the session category and activate it as described. Note that iOS may still suspend the app after a few minutes if no audio is being recorded; keep the session active and recording to stay alive.

Selecting Input Source

Use AVAudioSession.sharedInstance().availableInputs to list all connected microphones. You can then set the preferred input with setPreferredInput(_:error:).

External Resources

To deepen your understanding of AVFoundation and audio handling, refer to these official documentation pages:

Conclusion

Building a custom audio recorder and player in iOS with AVFoundation gives you fine‑grained control over audio capture, storage, and playback. By carefully configuring the audio session, requesting permissions, selecting appropriate recording settings, and handling errors, you can deliver a reliable and feature‑rich experience. The pattern described here—delegates for state changes, metering for real‑time feedback, and interruption handling—forms a solid foundation that you can extend with transcoding, waveform visualization, or cloud synchronization.

Begin by implementing the core recording and playback loops, then layer on advanced features as your app’s requirements grow. The documentation and sample code provided should accelerate your development while maintaining best practices for performance and user privacy.