Important Notes

New features were added in v3.1.7, including audio route handling and audio session option customization.

Audio Route Change Handling

1. New Delegate Methods

New delegate methods are available to handle audio route changes (Bluetooth connection/disconnection, headphone connection, etc.).

protocol AsleepSleepTrackingManagerDelegate {
    // Called before route change (before Engine restart)
    func willChangeAudioRoute(from oldRoute: AVAudioSessionRouteDescription,
                              to newRoute: AVAudioSessionRouteDescription)

    // Called after route change (after Engine restart)
    func didChangeAudioRoute(to newRoute: AVAudioSessionRouteDescription)
}

Features:

  • Optional methods (existing behavior maintained if not implemented)
  • Called when Bluetooth/headphones are connected or disconnected
  • Separate calls before and after Engine reconfiguration

2. Interruption vs Route Change

DelegateTrigger ConditionEngine RestartProcessing Speed
didInterrupt() / didResume()Phone call, alarm, VOIPNoFast (~few milliseconds)
willChangeAudioRoute() / didChangeAudioRoute()Bluetooth/headphone connection/disconnectionYesSlow (100-500ms)

3. Usage Examples

Music Playback Apps

extension YourViewController: AsleepSleepTrackingManagerDelegate {
    func willChangeAudioRoute(from oldRoute: AVAudioSessionRouteDescription,
                              to newRoute: AVAudioSessionRouteDescription) {
        // Pause music when route change starts
        if musicPlayer?.isPlaying == true {
            musicPlayer?.pause()
        }
    }

    func didChangeAudioRoute(to newRoute: AVAudioSessionRouteDescription) {
        // Resume music after route change completes
        musicPlayer?.play()
    }
}

Route Information Logging

func willChangeAudioRoute(from oldRoute: AVAudioSessionRouteDescription,
                          to newRoute: AVAudioSessionRouteDescription) {
    let oldOutput = oldRoute.outputs.first?.portType.rawValue ?? "unknown"
    let newOutput = newRoute.outputs.first?.portType.rawValue ?? "unknown"

    print("Audio route changing: \(oldOutput) -> \(newOutput)")
}

Audio Session Management

Design Purpose

Starting from v3.1.7, the SDK fully manages the audio session. If clients directly call AVAudioSession.setActive() or setCategory(), conflicts with the SDK may occur. Required options should be passed to startTracking() so the SDK can apply them at the appropriate time.


1. API Changes

You can now pass audio session options to the startTracking() method.

// Existing (still works)
manager?.startTracking()

// New: Pass additional options
manager?.startTracking(additionalAudioSessionOptions: [.duckOthers])

Important:

  • Do NOT call AVAudioSession.sharedInstance().setActive() directly during tracking
  • Do NOT call AVAudioSession.sharedInstance().setCategory() directly during tracking
  • Pass only required options via additionalAudioSessionOptions

2. SDK Default Configuration

The SDK configures the audio session as follows:

// Category (cannot be changed)
.playAndRecord  // Supports simultaneous recording and playback

// Options (cannot be changed)
[.mixWithOthers, .allowBluetoothA2DP]

Options passed via additionalAudioSessionOptions are added to the defaults:

manager?.startTracking(additionalAudioSessionOptions: [.duckOthers])
// Final configuration: Category = .playAndRecord, Options = [.mixWithOthers, .allowBluetoothA2DP, .duckOthers]

3. Main Use Cases

Automatically Reduce Music Volume

func startTrackingWithMusic() {
    // Automatically reduce volume of other app's audio
    manager?.startTracking(additionalAudioSessionOptions: [.duckOthers])

    // Play music
    musicPlayer?.play()
}

AirPlay Support

func startTrackingWithAirPlay() {
    // Allow AirPlay
    manager?.startTracking(additionalAudioSessionOptions: [.allowAirPlay])
}

Multiple Options Combined

manager?.startTracking(additionalAudioSessionOptions: [
    .duckOthers,     // Reduce music volume
    .allowAirPlay    // Allow AirPlay
])

4. Available Options

OptionDescriptionUse Case
.mixWithOthersMix with other apps' audio (SDK default)Always applied
.allowBluetoothA2DPAllow A2DP Bluetooth (SDK default)Always applied
.duckOthersReduce other apps' audio volumeMinimize interruption during music playback
.allowAirPlayAllow AirPlayHomePod playback
.defaultToSpeakerPrioritize speaker outputUse speakerphone

Note: .mixWithOthers is a SDK default, so no need to pass it separately.


5. Important Notes

Option Accumulation:

  • Options are reset with each startTracking() call
  • Previous options are not retained
// First call
manager?.startTracking(additionalAudioSessionOptions: [.duckOthers])

// After stopTracking(), restart
manager?.stopTracking()
manager?.startTracking(additionalAudioSessionOptions: [.allowAirPlay])
// -> .duckOthers is removed, only .allowAirPlay is applied

Incorrect Usage Example:

// [DO NOT] Direct audio session control during tracking
func startTracking() {
    manager?.startTracking()

    // Conflicts with SDK!
    try? AVAudioSession.sharedInstance().setActive(true)  // <- Don't do this
}

Stability Improvements (v3.1.7)

1. Bluetooth Stability Improvements

Fixed Issues:

  • Recording interruption during Bluetooth connection/disconnection
  • Recording resume failure after VOIP calls
  • Crashes during audio format changes while Bluetooth is connected

Results:

  • Stable recording maintained even during Bluetooth device connection state changes
  • Automatic switching between built-in mic and Bluetooth

2. Interruption Recovery Improvements

Automatic Recovery Mechanisms:

  • Added 10-second automatic recovery timer
  • Automatic recovery attempt when app returns to foreground
  • VOIP detection and waiting

No Code Changes Required:

  • Handled automatically within SDK
  • Existing didResume() delegate still called as before

3. Crash Fixes

Fixed crashes in the following scenarios:

  • Audio route reconfiguration during interruptions
  • Memory access errors
  • Race conditions

Migration Guide

Existing Code (No Changes Required)

class MyViewController: UIViewController, AsleepSleepTrackingManagerDelegate {
    var manager: Asleep.SleepTrackingManager?

    func startTracking() {
        Asleep.shared().createSleepTrackingManager(config: config, delegate: self)
        manager?.startTracking()  // Existing code works as-is
    }

    // Existing delegate methods
    func didCreate() { }
    func didUpload(sequence: Int) { }
    func didClose(sessionId: String) { }
    func didFail(error: Asleep.AsleepError) { }
    func didInterrupt() { }
    func didResume() { }
    func micPermissionWasDenied() { }
    func analysing(session: Asleep.Model.Session) { }
}

Optional Enhancement 1: Music Playback Control

extension MyViewController {
    // Add new delegates (optional)
    func willChangeAudioRoute(from oldRoute: AVAudioSessionRouteDescription,
                              to newRoute: AVAudioSessionRouteDescription) {
        if musicPlayer?.isPlaying == true {
            musicPlayer?.pause()
        }
    }

    func didChangeAudioRoute(to newRoute: AVAudioSessionRouteDescription) {
        musicPlayer?.play()
    }
}

Optional Enhancement 2: Add Audio Options

extension MyViewController {
    func startTrackingWithMusic() {
        Asleep.shared().createSleepTrackingManager(config: config, delegate: self)

        // Automatically reduce music volume
        manager?.startTracking(additionalAudioSessionOptions: [.duckOthers])

        // Play background music
        playBackgroundMusic()
    }
}