886 lines
31 KiB
Plaintext
886 lines
31 KiB
Plaintext
/*
|
|
Copyright (C) 2024 Rohith Namboothiri
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#import <AVFoundation/AVFoundation.h>
|
|
#import <UIKit/UIKit.h>
|
|
#import <MediaPlayer/MediaPlayer.h>
|
|
#include "droidstar.h"
|
|
|
|
// Forward declarations for callbacks
|
|
extern "C" void callProcessConnect();
|
|
extern "C" void clearAudioBuffer();
|
|
|
|
// Callback function pointers for PTT control from remote commands
|
|
static void (*g_pttPressCallback)(void) = NULL;
|
|
static void (*g_pttReleaseCallback)(void) = NULL;
|
|
|
|
#pragma mark - AudioSessionManager Interface
|
|
|
|
@interface AudioSessionManager : NSObject {
|
|
UIBackgroundTaskIdentifier _bgTask;
|
|
BOOL _isAudioSessionActive;
|
|
BOOL _isHandlingRouteChange;
|
|
BOOL _isConnected;
|
|
BOOL _isTransmitting;
|
|
BOOL _isReceiving;
|
|
|
|
// Silent audio player to keep audio session alive
|
|
AVAudioPlayer *_silentPlayer;
|
|
NSTimer *_keepAliveTimer;
|
|
|
|
// Timer to refresh Now Playing periodically
|
|
NSTimer *_nowPlayingTimer;
|
|
NSTimeInterval _playbackStartTime;
|
|
|
|
// Current state info for Now Playing
|
|
NSString *_currentCallsign;
|
|
NSString *_currentName;
|
|
NSString *_currentCountry;
|
|
NSString *_currentMode;
|
|
NSString *_currentHost;
|
|
}
|
|
|
|
@property (nonatomic, assign) BOOL isConnected;
|
|
@property (nonatomic, assign) BOOL isTransmitting;
|
|
@property (nonatomic, assign) BOOL isReceiving;
|
|
|
|
+ (instancetype)sharedManager;
|
|
|
|
// Audio Session
|
|
- (void)setupAVAudioSession;
|
|
- (void)setupBackgroundAudio;
|
|
- (void)setPreferredInputDevice;
|
|
|
|
// Now Playing & Remote Commands
|
|
- (void)setupRemoteCommandCenter;
|
|
- (void)updateNowPlayingInfo;
|
|
- (void)clearNowPlayingInfo;
|
|
|
|
// State updates from app
|
|
- (void)setConnectionState:(BOOL)connected host:(NSString *)host mode:(NSString *)mode;
|
|
- (void)setCurrentRX:(NSString *)callsign name:(NSString *)name country:(NSString *)country;
|
|
- (void)setTransmitting:(BOOL)transmitting;
|
|
- (void)clearCurrentRX;
|
|
|
|
// Background task
|
|
- (void)startBackgroundTask;
|
|
- (void)stopBackgroundTask;
|
|
|
|
// Keep-alive audio
|
|
- (void)startKeepAliveAudio;
|
|
- (void)stopKeepAliveAudio;
|
|
|
|
// Now Playing refresh timer
|
|
- (void)startNowPlayingTimer;
|
|
- (void)stopNowPlayingTimer;
|
|
|
|
@end
|
|
|
|
#pragma mark - AudioSessionManager Implementation
|
|
|
|
@implementation AudioSessionManager
|
|
|
|
@synthesize isConnected = _isConnected;
|
|
@synthesize isTransmitting = _isTransmitting;
|
|
@synthesize isReceiving = _isReceiving;
|
|
|
|
+ (instancetype)sharedManager {
|
|
static AudioSessionManager *shared = nil;
|
|
static dispatch_once_t onceToken;
|
|
dispatch_once(&onceToken, ^{
|
|
shared = [[AudioSessionManager alloc] init];
|
|
});
|
|
return shared;
|
|
}
|
|
|
|
- (instancetype)init {
|
|
self = [super init];
|
|
if (self) {
|
|
_bgTask = UIBackgroundTaskInvalid;
|
|
_isAudioSessionActive = NO;
|
|
_isHandlingRouteChange = NO;
|
|
_isConnected = NO;
|
|
_isTransmitting = NO;
|
|
_isReceiving = NO;
|
|
|
|
_currentCallsign = @"";
|
|
_currentName = @"";
|
|
_currentCountry = @"";
|
|
_currentMode = @"";
|
|
_currentHost = @"";
|
|
|
|
_nowPlayingTimer = nil;
|
|
_playbackStartTime = 0;
|
|
|
|
[self setupNotificationObservers];
|
|
[self setupRemoteCommandCenter];
|
|
[self createSilentAudioFile];
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (void)setupNotificationObservers {
|
|
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
|
|
// Audio session notifications
|
|
[nc addObserver:self
|
|
selector:@selector(handleAudioInterruption:)
|
|
name:AVAudioSessionInterruptionNotification
|
|
object:session];
|
|
|
|
[nc addObserver:self
|
|
selector:@selector(handleAudioRouteChange:)
|
|
name:AVAudioSessionRouteChangeNotification
|
|
object:session];
|
|
|
|
[nc addObserver:self
|
|
selector:@selector(handleMediaServicesWereReset:)
|
|
name:AVAudioSessionMediaServicesWereResetNotification
|
|
object:session];
|
|
|
|
// App lifecycle notifications
|
|
[nc addObserver:self
|
|
selector:@selector(handleAppDidEnterBackground:)
|
|
name:UIApplicationDidEnterBackgroundNotification
|
|
object:nil];
|
|
|
|
[nc addObserver:self
|
|
selector:@selector(handleAppWillEnterForeground:)
|
|
name:UIApplicationWillEnterForegroundNotification
|
|
object:nil];
|
|
|
|
[nc addObserver:self
|
|
selector:@selector(handleAppDidBecomeActive:)
|
|
name:UIApplicationDidBecomeActiveNotification
|
|
object:nil];
|
|
|
|
[nc addObserver:self
|
|
selector:@selector(handleAppWillResignActive:)
|
|
name:UIApplicationWillResignActiveNotification
|
|
object:nil];
|
|
}
|
|
|
|
- (void)dealloc {
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
[self stopNowPlayingTimer];
|
|
[self stopKeepAliveAudio];
|
|
[self stopBackgroundTask];
|
|
[self clearNowPlayingInfo];
|
|
}
|
|
|
|
#pragma mark - Silent Audio for Keep-Alive
|
|
|
|
- (void)createSilentAudioFile {
|
|
// Create a silent audio file in the temp directory
|
|
// This will be used to keep the audio session active
|
|
NSString *tempDir = NSTemporaryDirectory();
|
|
NSString *silentPath = [tempDir stringByAppendingPathComponent:@"silence.wav"];
|
|
|
|
// Check if file already exists
|
|
if ([[NSFileManager defaultManager] fileExistsAtPath:silentPath]) {
|
|
return;
|
|
}
|
|
|
|
// Create a minimal WAV file with silence (1 second of silence at 8kHz mono)
|
|
// WAV header + 8000 samples of silence
|
|
int sampleRate = 8000;
|
|
int numSamples = sampleRate; // 1 second
|
|
int bitsPerSample = 16;
|
|
int numChannels = 1;
|
|
int byteRate = sampleRate * numChannels * bitsPerSample / 8;
|
|
int blockAlign = numChannels * bitsPerSample / 8;
|
|
int dataSize = numSamples * blockAlign;
|
|
int fileSize = 36 + dataSize;
|
|
|
|
NSMutableData *wavData = [NSMutableData data];
|
|
|
|
// RIFF header
|
|
[wavData appendBytes:"RIFF" length:4];
|
|
uint32_t fileSizeLE = CFSwapInt32HostToLittle(fileSize);
|
|
[wavData appendBytes:&fileSizeLE length:4];
|
|
[wavData appendBytes:"WAVE" length:4];
|
|
|
|
// fmt chunk
|
|
[wavData appendBytes:"fmt " length:4];
|
|
uint32_t fmtSize = CFSwapInt32HostToLittle(16);
|
|
[wavData appendBytes:&fmtSize length:4];
|
|
uint16_t audioFormat = CFSwapInt16HostToLittle(1); // PCM
|
|
[wavData appendBytes:&audioFormat length:2];
|
|
uint16_t channels = CFSwapInt16HostToLittle(numChannels);
|
|
[wavData appendBytes:&channels length:2];
|
|
uint32_t sampleRateLE = CFSwapInt32HostToLittle(sampleRate);
|
|
[wavData appendBytes:&sampleRateLE length:4];
|
|
uint32_t byteRateLE = CFSwapInt32HostToLittle(byteRate);
|
|
[wavData appendBytes:&byteRateLE length:4];
|
|
uint16_t blockAlignLE = CFSwapInt16HostToLittle(blockAlign);
|
|
[wavData appendBytes:&blockAlignLE length:2];
|
|
uint16_t bitsPerSampleLE = CFSwapInt16HostToLittle(bitsPerSample);
|
|
[wavData appendBytes:&bitsPerSampleLE length:2];
|
|
|
|
// data chunk
|
|
[wavData appendBytes:"data" length:4];
|
|
uint32_t dataSizeLE = CFSwapInt32HostToLittle(dataSize);
|
|
[wavData appendBytes:&dataSizeLE length:4];
|
|
|
|
// Silent samples (zeros)
|
|
void *silence = calloc(dataSize, 1);
|
|
[wavData appendBytes:silence length:dataSize];
|
|
free(silence);
|
|
|
|
[wavData writeToFile:silentPath atomically:YES];
|
|
NSLog(@"[AudioSessionManager] Created silent audio file at: %@", silentPath);
|
|
}
|
|
|
|
- (void)startKeepAliveAudio {
|
|
if (_silentPlayer && _silentPlayer.isPlaying) {
|
|
return; // Already playing
|
|
}
|
|
|
|
NSString *tempDir = NSTemporaryDirectory();
|
|
NSString *silentPath = [tempDir stringByAppendingPathComponent:@"silence.wav"];
|
|
NSURL *silentURL = [NSURL fileURLWithPath:silentPath];
|
|
|
|
NSError *error = nil;
|
|
_silentPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:silentURL error:&error];
|
|
|
|
if (error) {
|
|
NSLog(@"[AudioSessionManager] Error creating silent player: %@", error.localizedDescription);
|
|
return;
|
|
}
|
|
|
|
_silentPlayer.numberOfLoops = -1; // Loop indefinitely
|
|
// Use tiny volume instead of 0 - iOS may not register 0 volume as "playing"
|
|
_silentPlayer.volume = 0.001;
|
|
|
|
if ([_silentPlayer play]) {
|
|
NSLog(@"[AudioSessionManager] Keep-alive audio started");
|
|
// Become the "Now Playing" app
|
|
[[UIApplication sharedApplication] beginReceivingRemoteControlEvents];
|
|
} else {
|
|
NSLog(@"[AudioSessionManager] Failed to start keep-alive audio");
|
|
}
|
|
}
|
|
|
|
- (void)stopKeepAliveAudio {
|
|
if (_silentPlayer) {
|
|
[_silentPlayer stop];
|
|
_silentPlayer = nil;
|
|
NSLog(@"[AudioSessionManager] Keep-alive audio stopped");
|
|
}
|
|
|
|
if (_keepAliveTimer) {
|
|
[_keepAliveTimer invalidate];
|
|
_keepAliveTimer = nil;
|
|
}
|
|
|
|
// Stop being the "Now Playing" app
|
|
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
|
|
}
|
|
|
|
#pragma mark - Audio Session Setup
|
|
|
|
- (void)setupAVAudioSession {
|
|
NSLog(@"[AudioSessionManager] Setting up audio session...");
|
|
@try {
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
NSError *error = nil;
|
|
|
|
// Use PlayAndRecord for VoIP-style audio
|
|
BOOL success = [session setCategory:AVAudioSessionCategoryPlayAndRecord
|
|
withOptions:(AVAudioSessionCategoryOptionDefaultToSpeaker |
|
|
AVAudioSessionCategoryOptionAllowBluetooth |
|
|
AVAudioSessionCategoryOptionAllowBluetoothA2DP)
|
|
error:&error];
|
|
|
|
if (!success || error) {
|
|
NSLog(@"[AudioSessionManager] Error setting category: %@", error.localizedDescription);
|
|
return;
|
|
}
|
|
|
|
// VoiceChat mode for echo cancellation and voice processing
|
|
success = [session setMode:AVAudioSessionModeVoiceChat error:&error];
|
|
if (!success || error) {
|
|
NSLog(@"[AudioSessionManager] Error setting mode: %@", error.localizedDescription);
|
|
}
|
|
|
|
// Set preferences
|
|
error = nil;
|
|
[session setPreferredSampleRate:48000.0 error:&error];
|
|
error = nil;
|
|
[session setPreferredIOBufferDuration:0.02 error:&error]; // ~20ms buffer
|
|
|
|
[self configureAudioRoute:session];
|
|
[self setPreferredInputDevice];
|
|
|
|
// Activate session
|
|
error = nil;
|
|
success = [session setActive:YES error:&error];
|
|
if (!success || error) {
|
|
NSLog(@"[AudioSessionManager] Error activating session: %@", error.localizedDescription);
|
|
return;
|
|
}
|
|
|
|
_isAudioSessionActive = YES;
|
|
NSLog(@"[AudioSessionManager] Audio session activated successfully");
|
|
|
|
[self startBackgroundTask];
|
|
|
|
// Start keep-alive if connected
|
|
if (_isConnected) {
|
|
[self startKeepAliveAudio];
|
|
}
|
|
}
|
|
@catch (NSException *exception) {
|
|
NSLog(@"[AudioSessionManager] Exception: %@", exception.reason);
|
|
}
|
|
}
|
|
|
|
- (void)setupBackgroundAudio {
|
|
NSLog(@"[AudioSessionManager] Setting up background audio...");
|
|
@try {
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
NSError *error = nil;
|
|
|
|
// Configure for background playback
|
|
BOOL success = [session setCategory:AVAudioSessionCategoryPlayAndRecord
|
|
withOptions:(AVAudioSessionCategoryOptionDefaultToSpeaker |
|
|
AVAudioSessionCategoryOptionAllowBluetooth |
|
|
AVAudioSessionCategoryOptionAllowBluetoothA2DP)
|
|
error:&error];
|
|
|
|
if (!success || error) {
|
|
NSLog(@"[AudioSessionManager] Error setting background category: %@", error.localizedDescription);
|
|
}
|
|
|
|
error = nil;
|
|
[session setMode:AVAudioSessionModeVoiceChat error:&error];
|
|
|
|
// Activate session
|
|
error = nil;
|
|
success = [session setActive:YES error:&error];
|
|
if (!success || error) {
|
|
NSLog(@"[AudioSessionManager] Error activating background session: %@", error.localizedDescription);
|
|
} else {
|
|
_isAudioSessionActive = YES;
|
|
NSLog(@"[AudioSessionManager] Background audio session activated");
|
|
}
|
|
|
|
[self configureAudioRoute:session];
|
|
[self startBackgroundTask];
|
|
|
|
// CRITICAL: Start keep-alive audio when entering background
|
|
if (_isConnected) {
|
|
[self startKeepAliveAudio];
|
|
}
|
|
}
|
|
@catch (NSException *exception) {
|
|
NSLog(@"[AudioSessionManager] Exception in background setup: %@", exception.reason);
|
|
}
|
|
}
|
|
|
|
- (void)configureAudioRoute:(AVAudioSession *)session {
|
|
NSError *error = nil;
|
|
AVAudioSessionRouteDescription *currentRoute = session.currentRoute;
|
|
|
|
BOOL hasExternalOutput = NO;
|
|
for (AVAudioSessionPortDescription *output in currentRoute.outputs) {
|
|
if ([output.portType isEqualToString:AVAudioSessionPortBluetoothA2DP] ||
|
|
[output.portType isEqualToString:AVAudioSessionPortBluetoothLE] ||
|
|
[output.portType isEqualToString:AVAudioSessionPortBluetoothHFP] ||
|
|
[output.portType isEqualToString:AVAudioSessionPortHeadphones]) {
|
|
hasExternalOutput = YES;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasExternalOutput) {
|
|
BOOL success = [session overrideOutputAudioPort:AVAudioSessionPortOverrideSpeaker error:&error];
|
|
if (!success || error) {
|
|
NSLog(@"[AudioSessionManager] Error overriding to speaker: %@", error.localizedDescription);
|
|
} else {
|
|
NSLog(@"[AudioSessionManager] Audio output set to speaker");
|
|
}
|
|
} else {
|
|
NSLog(@"[AudioSessionManager] External audio output detected");
|
|
}
|
|
}
|
|
|
|
- (void)setPreferredInputDevice {
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
NSError *error = nil;
|
|
NSArray<AVAudioSessionPortDescription *> *availableInputs = session.availableInputs;
|
|
|
|
for (AVAudioSessionPortDescription *input in availableInputs) {
|
|
if ([input.portType isEqualToString:AVAudioSessionPortBluetoothHFP] ||
|
|
[input.portType isEqualToString:AVAudioSessionPortBluetoothLE]) {
|
|
BOOL success = [session setPreferredInput:input error:&error];
|
|
if (success) {
|
|
NSLog(@"[AudioSessionManager] Preferred input: Bluetooth");
|
|
}
|
|
return;
|
|
} else if ([input.portType isEqualToString:AVAudioSessionPortHeadsetMic]) {
|
|
BOOL success = [session setPreferredInput:input error:&error];
|
|
if (success) {
|
|
NSLog(@"[AudioSessionManager] Preferred input: Headset Mic");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
NSLog(@"[AudioSessionManager] Using default input device");
|
|
}
|
|
|
|
#pragma mark - Remote Command Center (Control Center / Lock Screen / Headphones)
|
|
|
|
- (void)setupRemoteCommandCenter {
|
|
MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter];
|
|
|
|
// Play command - can be used to start receiving or toggle PTT
|
|
commandCenter.playCommand.enabled = YES;
|
|
[commandCenter.playCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) {
|
|
NSLog(@"[AudioSessionManager] Remote play command received");
|
|
// For DroidStar, this could toggle TX or just acknowledge
|
|
return MPRemoteCommandHandlerStatusSuccess;
|
|
}];
|
|
|
|
// Pause command
|
|
commandCenter.pauseCommand.enabled = YES;
|
|
[commandCenter.pauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) {
|
|
NSLog(@"[AudioSessionManager] Remote pause command received");
|
|
return MPRemoteCommandHandlerStatusSuccess;
|
|
}];
|
|
|
|
// Toggle play/pause (headphone button single press)
|
|
commandCenter.togglePlayPauseCommand.enabled = YES;
|
|
[commandCenter.togglePlayPauseCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) {
|
|
NSLog(@"[AudioSessionManager] Remote toggle play/pause received - PTT toggle");
|
|
// Toggle PTT
|
|
if (g_pttPressCallback && g_pttReleaseCallback) {
|
|
if (self->_isTransmitting) {
|
|
g_pttReleaseCallback();
|
|
} else {
|
|
g_pttPressCallback();
|
|
}
|
|
}
|
|
return MPRemoteCommandHandlerStatusSuccess;
|
|
}];
|
|
|
|
// Stop command - could be used to disconnect
|
|
commandCenter.stopCommand.enabled = YES;
|
|
[commandCenter.stopCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) {
|
|
NSLog(@"[AudioSessionManager] Remote stop command received");
|
|
return MPRemoteCommandHandlerStatusSuccess;
|
|
}];
|
|
|
|
// Disable skip commands (not applicable for radio)
|
|
commandCenter.nextTrackCommand.enabled = NO;
|
|
commandCenter.previousTrackCommand.enabled = NO;
|
|
commandCenter.skipForwardCommand.enabled = NO;
|
|
commandCenter.skipBackwardCommand.enabled = NO;
|
|
commandCenter.seekForwardCommand.enabled = NO;
|
|
commandCenter.seekBackwardCommand.enabled = NO;
|
|
|
|
NSLog(@"[AudioSessionManager] Remote command center configured");
|
|
}
|
|
|
|
#pragma mark - Now Playing Info (Lock Screen / Control Center display)
|
|
|
|
- (void)updateNowPlayingInfo {
|
|
if (!_isConnected) {
|
|
[self clearNowPlayingInfo];
|
|
return;
|
|
}
|
|
|
|
// Must run on main thread
|
|
if (![NSThread isMainThread]) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self updateNowPlayingInfo];
|
|
});
|
|
return;
|
|
}
|
|
|
|
NSMutableDictionary *nowPlayingInfo = [NSMutableDictionary dictionary];
|
|
|
|
// Title: Show current activity
|
|
NSString *title;
|
|
if (_isTransmitting) {
|
|
title = @"📡 Transmitting";
|
|
} else if (_isReceiving && _currentCallsign.length > 0) {
|
|
title = [NSString stringWithFormat:@"📻 %@", _currentCallsign];
|
|
} else {
|
|
title = @"📻 Listening...";
|
|
}
|
|
nowPlayingInfo[MPMediaItemPropertyTitle] = title;
|
|
|
|
// Artist: Show name and country for RX, or "TX" for transmit
|
|
NSString *artist;
|
|
if (_isTransmitting) {
|
|
artist = @"Push-to-Talk Active";
|
|
} else if (_currentName.length > 0 || _currentCountry.length > 0) {
|
|
NSMutableArray *parts = [NSMutableArray array];
|
|
if (_currentName.length > 0) [parts addObject:_currentName];
|
|
if (_currentCountry.length > 0) [parts addObject:_currentCountry];
|
|
artist = [parts componentsJoinedByString:@" • "];
|
|
} else {
|
|
artist = @"Awaiting Signal";
|
|
}
|
|
nowPlayingInfo[MPMediaItemPropertyArtist] = artist;
|
|
|
|
// Album: Show connection info
|
|
NSString *album = @"DroidStar";
|
|
if (_currentHost.length > 0) {
|
|
album = [NSString stringWithFormat:@"%@ - %@", _currentMode, _currentHost];
|
|
}
|
|
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album;
|
|
|
|
// Calculate elapsed time since connection (forces UI refresh)
|
|
NSTimeInterval elapsed = 0;
|
|
if (_playbackStartTime > 0) {
|
|
elapsed = [[NSDate date] timeIntervalSince1970] - _playbackStartTime;
|
|
}
|
|
|
|
// Playback state - changing elapsed time forces Control Center refresh
|
|
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = @(1.0);
|
|
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(elapsed);
|
|
|
|
// Set "Live" indicator (shows LIVE badge on Control Center)
|
|
nowPlayingInfo[MPNowPlayingInfoPropertyIsLiveStream] = @(YES);
|
|
|
|
// Update the info center
|
|
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nowPlayingInfo;
|
|
}
|
|
|
|
- (void)clearNowPlayingInfo {
|
|
[MPNowPlayingInfoCenter defaultCenter].nowPlayingInfo = nil;
|
|
NSLog(@"[AudioSessionManager] Now Playing info cleared");
|
|
}
|
|
|
|
#pragma mark - State Updates from App
|
|
|
|
- (void)setConnectionState:(BOOL)connected host:(NSString *)host mode:(NSString *)mode {
|
|
_isConnected = connected;
|
|
_currentHost = host ?: @"";
|
|
_currentMode = mode ?: @"";
|
|
|
|
NSLog(@"[AudioSessionManager] Connection state: %@ (%@ - %@)",
|
|
connected ? @"Connected" : @"Disconnected", mode, host);
|
|
|
|
if (connected) {
|
|
// Record playback start time for elapsed time calculation
|
|
_playbackStartTime = [[NSDate date] timeIntervalSince1970];
|
|
|
|
// Start keep-alive FIRST so we become the "now playing" app
|
|
[self startKeepAliveAudio];
|
|
|
|
// Start timer to periodically refresh Now Playing (every 2 seconds)
|
|
[self startNowPlayingTimer];
|
|
|
|
// Small delay to let audio system register playback, then update Now Playing
|
|
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
|
|
[self updateNowPlayingInfo];
|
|
});
|
|
} else {
|
|
_playbackStartTime = 0;
|
|
[self stopNowPlayingTimer];
|
|
[self stopKeepAliveAudio];
|
|
[self clearCurrentRX];
|
|
[self clearNowPlayingInfo];
|
|
[[UIApplication sharedApplication] endReceivingRemoteControlEvents];
|
|
}
|
|
}
|
|
|
|
- (void)setCurrentRX:(NSString *)callsign name:(NSString *)name country:(NSString *)country {
|
|
_currentCallsign = callsign ?: @"";
|
|
_currentName = name ?: @"";
|
|
_currentCountry = country ?: @"";
|
|
_isReceiving = (callsign.length > 0);
|
|
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
- (void)setTransmitting:(BOOL)transmitting {
|
|
_isTransmitting = transmitting;
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
- (void)clearCurrentRX {
|
|
_currentCallsign = @"";
|
|
_currentName = @"";
|
|
_currentCountry = @"";
|
|
_isReceiving = NO;
|
|
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
#pragma mark - Now Playing Refresh Timer
|
|
|
|
- (void)startNowPlayingTimer {
|
|
[self stopNowPlayingTimer];
|
|
|
|
// Update Now Playing every 2 seconds to keep Control Center/Lock Screen fresh
|
|
_nowPlayingTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
|
|
target:self
|
|
selector:@selector(nowPlayingTimerFired)
|
|
userInfo:nil
|
|
repeats:YES];
|
|
|
|
// Allow timer to fire even when scrolling
|
|
[[NSRunLoop mainRunLoop] addTimer:_nowPlayingTimer forMode:NSRunLoopCommonModes];
|
|
|
|
NSLog(@"[AudioSessionManager] Now Playing timer started");
|
|
}
|
|
|
|
- (void)stopNowPlayingTimer {
|
|
if (_nowPlayingTimer) {
|
|
[_nowPlayingTimer invalidate];
|
|
_nowPlayingTimer = nil;
|
|
NSLog(@"[AudioSessionManager] Now Playing timer stopped");
|
|
}
|
|
}
|
|
|
|
- (void)nowPlayingTimerFired {
|
|
if (_isConnected) {
|
|
[self updateNowPlayingInfo];
|
|
} else {
|
|
[self stopNowPlayingTimer];
|
|
}
|
|
}
|
|
|
|
#pragma mark - Background Task Management
|
|
|
|
- (void)startBackgroundTask {
|
|
void (^startTask)(void) = ^{
|
|
[self stopBackgroundTask];
|
|
|
|
self->_bgTask = [[UIApplication sharedApplication] beginBackgroundTaskWithName:@"DroidStarAudio"
|
|
expirationHandler:^{
|
|
NSLog(@"[AudioSessionManager] Background task expiring - renewing");
|
|
[[UIApplication sharedApplication] endBackgroundTask:self->_bgTask];
|
|
self->_bgTask = UIBackgroundTaskInvalid;
|
|
|
|
// Try to renew if still connected
|
|
if (self->_isConnected) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self startBackgroundTask];
|
|
});
|
|
}
|
|
}];
|
|
|
|
if (self->_bgTask != UIBackgroundTaskInvalid) {
|
|
NSTimeInterval remaining = [UIApplication sharedApplication].backgroundTimeRemaining;
|
|
NSLog(@"[AudioSessionManager] Background task started (ID: %lu, remaining: %.1fs)",
|
|
(unsigned long)self->_bgTask, remaining);
|
|
} else {
|
|
NSLog(@"[AudioSessionManager] Failed to start background task");
|
|
}
|
|
};
|
|
|
|
if ([NSThread isMainThread]) {
|
|
startTask();
|
|
} else {
|
|
dispatch_sync(dispatch_get_main_queue(), startTask);
|
|
}
|
|
}
|
|
|
|
- (void)stopBackgroundTask {
|
|
if (_bgTask != UIBackgroundTaskInvalid) {
|
|
[[UIApplication sharedApplication] endBackgroundTask:_bgTask];
|
|
_bgTask = UIBackgroundTaskInvalid;
|
|
NSLog(@"[AudioSessionManager] Background task ended");
|
|
}
|
|
}
|
|
|
|
#pragma mark - Notification Handlers
|
|
|
|
- (void)handleAudioInterruption:(NSNotification *)notification {
|
|
NSUInteger interruptionType = [notification.userInfo[AVAudioSessionInterruptionTypeKey] unsignedIntegerValue];
|
|
|
|
if (interruptionType == AVAudioSessionInterruptionTypeBegan) {
|
|
NSLog(@"[AudioSessionManager] Audio interruption began");
|
|
_isAudioSessionActive = NO;
|
|
[self stopKeepAliveAudio];
|
|
} else if (interruptionType == AVAudioSessionInterruptionTypeEnded) {
|
|
NSLog(@"[AudioSessionManager] Audio interruption ended");
|
|
|
|
NSUInteger options = [notification.userInfo[AVAudioSessionInterruptionOptionKey] unsignedIntegerValue];
|
|
if (options & AVAudioSessionInterruptionOptionShouldResume) {
|
|
NSLog(@"[AudioSessionManager] System indicates we should resume");
|
|
}
|
|
|
|
// Reactivate session
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
NSError *error = nil;
|
|
BOOL success = [session setActive:YES error:&error];
|
|
|
|
if (success) {
|
|
_isAudioSessionActive = YES;
|
|
NSLog(@"[AudioSessionManager] Audio session reactivated after interruption");
|
|
|
|
if (_isConnected) {
|
|
[self startKeepAliveAudio];
|
|
}
|
|
} else {
|
|
NSLog(@"[AudioSessionManager] Failed to reactivate: %@", error.localizedDescription);
|
|
[self setupAVAudioSession];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)handleAudioRouteChange:(NSNotification *)notification {
|
|
if (_isHandlingRouteChange) return;
|
|
|
|
_isHandlingRouteChange = YES;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
AVAudioSessionRouteChangeReason reason = (AVAudioSessionRouteChangeReason)
|
|
[notification.userInfo[AVAudioSessionRouteChangeReasonKey] unsignedIntegerValue];
|
|
|
|
NSLog(@"[AudioSessionManager] Audio route changed, reason: %lu", (unsigned long)reason);
|
|
|
|
if (reason == AVAudioSessionRouteChangeReasonOldDeviceUnavailable ||
|
|
reason == AVAudioSessionRouteChangeReasonNewDeviceAvailable) {
|
|
[self setupAVAudioSession];
|
|
}
|
|
|
|
self->_isHandlingRouteChange = NO;
|
|
});
|
|
}
|
|
|
|
- (void)handleMediaServicesWereReset:(NSNotification *)notification {
|
|
NSLog(@"[AudioSessionManager] Media services reset - reinitializing");
|
|
_isAudioSessionActive = NO;
|
|
[self stopKeepAliveAudio];
|
|
[self setupRemoteCommandCenter];
|
|
[self setupAVAudioSession];
|
|
}
|
|
|
|
- (void)handleAppDidEnterBackground:(NSNotification *)notification {
|
|
NSLog(@"[AudioSessionManager] App entered background");
|
|
[self setupBackgroundAudio];
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
- (void)handleAppWillEnterForeground:(NSNotification *)notification {
|
|
NSLog(@"[AudioSessionManager] App entering foreground");
|
|
[self setupAVAudioSession];
|
|
}
|
|
|
|
- (void)handleAppDidBecomeActive:(NSNotification *)notification {
|
|
NSLog(@"[AudioSessionManager] App became active");
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
|
|
- (void)handleAppWillResignActive:(NSNotification *)notification {
|
|
NSLog(@"[AudioSessionManager] App will resign active");
|
|
// Ensure we're ready for background
|
|
if (_isConnected) {
|
|
[self startKeepAliveAudio];
|
|
[self updateNowPlayingInfo];
|
|
}
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark - C Interface
|
|
|
|
extern "C" void setupAVAudioSession() {
|
|
[[AudioSessionManager sharedManager] setupAVAudioSession];
|
|
}
|
|
|
|
extern "C" void deactivateAVAudioSession() {
|
|
@try {
|
|
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
NSError *error = nil;
|
|
|
|
NSLog(@"[AudioSessionManager] Deactivating audio session...");
|
|
|
|
BOOL success = [session setActive:NO
|
|
withOptions:AVAudioSessionSetActiveOptionNotifyOthersOnDeactivation
|
|
error:&error];
|
|
if (!success || error) {
|
|
NSLog(@"[AudioSessionManager] Error deactivating: %@", error.localizedDescription);
|
|
} else {
|
|
NSLog(@"[AudioSessionManager] Audio session deactivated");
|
|
}
|
|
}
|
|
@catch (NSException *exception) {
|
|
NSLog(@"[AudioSessionManager] Exception deactivating: %@", exception.reason);
|
|
}
|
|
}
|
|
|
|
extern "C" void setupBackgroundAudio() {
|
|
[[AudioSessionManager sharedManager] setupBackgroundAudio];
|
|
}
|
|
|
|
extern "C" void handleAppEnteringForeground() {
|
|
[[AudioSessionManager sharedManager] setupAVAudioSession];
|
|
}
|
|
|
|
extern "C" void clearAudioBuffer() {
|
|
NSLog(@"[AudioSessionManager] Clear audio buffer requested");
|
|
}
|
|
|
|
extern "C" void setPreferredInputDevice() {
|
|
[[AudioSessionManager sharedManager] setPreferredInputDevice];
|
|
}
|
|
|
|
extern "C" bool isAppInBackground() {
|
|
UIApplicationState state = [UIApplication sharedApplication].applicationState;
|
|
return (state == UIApplicationStateBackground);
|
|
}
|
|
|
|
extern "C" void deactivateBackgroundAudio() {
|
|
[[AudioSessionManager sharedManager] stopKeepAliveAudio];
|
|
deactivateAVAudioSession();
|
|
}
|
|
|
|
extern "C" void renewBackgroundTask() {
|
|
[[AudioSessionManager sharedManager] startBackgroundTask];
|
|
}
|
|
|
|
// New functions for state management
|
|
|
|
extern "C" void setAudioConnectionState(bool connected, const char *host, const char *mode) {
|
|
NSString *hostStr = host ? [NSString stringWithUTF8String:host] : @"";
|
|
NSString *modeStr = mode ? [NSString stringWithUTF8String:mode] : @"";
|
|
[[AudioSessionManager sharedManager] setConnectionState:connected host:hostStr mode:modeStr];
|
|
}
|
|
|
|
extern "C" void setAudioRXState(const char *callsign, const char *name, const char *country) {
|
|
NSString *callsignStr = callsign ? [NSString stringWithUTF8String:callsign] : @"";
|
|
NSString *nameStr = name ? [NSString stringWithUTF8String:name] : @"";
|
|
NSString *countryStr = country ? [NSString stringWithUTF8String:country] : @"";
|
|
[[AudioSessionManager sharedManager] setCurrentRX:callsignStr name:nameStr country:countryStr];
|
|
}
|
|
|
|
extern "C" void setAudioTXState(bool transmitting) {
|
|
[[AudioSessionManager sharedManager] setTransmitting:transmitting];
|
|
}
|
|
|
|
extern "C" void clearAudioRXState() {
|
|
[[AudioSessionManager sharedManager] clearCurrentRX];
|
|
}
|
|
|
|
extern "C" void updateNowPlayingInfo() {
|
|
[[AudioSessionManager sharedManager] updateNowPlayingInfo];
|
|
}
|
|
|
|
extern "C" void setPTTCallbacks(void (*pressCallback)(void), void (*releaseCallback)(void)) {
|
|
g_pttPressCallback = pressCallback;
|
|
g_pttReleaseCallback = releaseCallback;
|
|
NSLog(@"[AudioSessionManager] PTT callbacks registered");
|
|
}
|