1
0
mirror of https://github.com/danog/Telegram.git synced 2024-12-03 09:57:46 +01:00
Telegram/Telegraph/TGOpusAudioRecorder.m

448 lines
15 KiB
Mathematica
Raw Normal View History

2014-07-10 16:11:09 +02:00
/*
* This is the source code of Telegram for iOS v. 1.1
* It is licensed under GNU GPL v. 2 or later.
* You should have received a copy of the license in this archive (see LICENSE).
*
* Copyright Peter Iakovlev, 2013.
*/
#import "TGOpusAudioRecorder.h"
#import "TGAppDelegate.h"
#import "ASQueue.h"
#import "ActionStage.h"
#import "TGLiveUploadActor.h"
#import <AVFoundation/AVFoundation.h>
#import <AudioUnit/AudioUnit.h>
#import "opus.h"
#import "opusenc.h"
2015-10-01 18:19:52 +02:00
#import "TGDataItem.h"
#import "TGAudioSessionManager.h"
2014-07-10 16:11:09 +02:00
static const int TGOpusAudioRecorderSampleRate = 16000;
typedef struct
{
AudioComponentInstance audioUnit;
bool audioUnitStarted;
bool audioUnitInitialized;
int globalAudioRecorderId;
} TGOpusAudioRecorderContext;
static TGOpusAudioRecorderContext globalRecorderContext = { .audioUnit = NULL, .audioUnitStarted = false, .audioUnitInitialized = false, .globalAudioRecorderId = -1};
static __weak TGOpusAudioRecorder *globalRecorder = nil;
static dispatch_semaphore_t playSoundSemaphore = nil;
@interface TGOpusAudioRecorder () <ASWatcher>
{
2015-10-01 18:19:52 +02:00
TGDataItem *_tempFileItem;
2014-07-10 16:11:09 +02:00
TGOggOpusWriter *_oggWriter;
NSMutableData *_audioBuffer;
NSString *_liveUploadPath;
2015-10-01 18:19:52 +02:00
SMetaDisposable *_currentAudioSession;
2014-07-10 16:11:09 +02:00
}
@property (nonatomic, strong) ASHandle *actionHandle;
@property (nonatomic) int recorderId;
@end
@implementation TGOpusAudioRecorder
- (instancetype)initWithFileEncryption:(bool)fileEncryption
{
self = [super init];
if (self != nil)
{
_actionHandle = [[ASHandle alloc] initWithDelegate:self];
2015-10-01 18:19:52 +02:00
_tempFileItem = [[TGDataItem alloc] initWithTempFile];
_currentAudioSession = [[SMetaDisposable alloc] init];
2014-07-10 16:11:09 +02:00
[[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^
{
static int nextRecorderId = 1;
_recorderId = nextRecorderId++;
globalRecorderContext.globalAudioRecorderId = _recorderId;
globalRecorder = self;
static int nextActionId = 0;
int actionId = nextActionId++;
_liveUploadPath = [[NSString alloc] initWithFormat:@"/tg/liveUpload/(%d)", actionId];
2015-10-01 18:19:52 +02:00
2014-07-10 16:11:09 +02:00
[ActionStageInstance() requestActor:_liveUploadPath options:@{
2015-10-01 18:19:52 +02:00
@"fileItem": _tempFileItem,
2014-07-10 16:11:09 +02:00
@"encryptFile": @(fileEncryption)
} flags:0 watcher:self];
}];
}
return self;
}
- (void)dealloc
{
[ActionStageInstance() removeWatcher:self];
[self cleanup];
}
+ (ASQueue *)processingQueue
{
static ASQueue *queue = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
{
queue = [[ASQueue alloc] initWithName:"org.telegram.opusAudioRecorderQueue"];
});
return queue;
}
- (void)cleanup
{
intptr_t objectId = (intptr_t)self;
int recorderId = _recorderId;
globalRecorder = nil;
_oggWriter = nil;
[[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^
{
if (globalRecorderContext.globalAudioRecorderId == recorderId)
{
globalRecorderContext.globalAudioRecorderId++;
if (globalRecorderContext.audioUnitStarted && globalRecorderContext.audioUnit != NULL)
{
OSStatus status = noErr;
status = AudioOutputUnitStop(globalRecorderContext.audioUnit);
if (status != noErr)
TGLog(@"[TGOpusAudioRecorder#%x AudioOutputUnitStop failed: %d]", objectId, (int)status);
globalRecorderContext.audioUnitStarted = false;
}
if (globalRecorderContext.audioUnit != NULL)
{
OSStatus status = noErr;
status = AudioComponentInstanceDispose(globalRecorderContext.audioUnit);
if (status != noErr)
TGLog(@"[TGOpusAudioRecorder#%x AudioComponentInstanceDispose failed: %d]", objectId, (int)status);
globalRecorderContext.audioUnit = NULL;
}
}
}];
[self _endAudioSession];
}
2015-10-01 18:19:52 +02:00
- (void)_beginAudioSession
2014-07-10 16:11:09 +02:00
{
[[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^
{
2015-10-01 18:19:52 +02:00
__weak TGOpusAudioRecorder *weakSelf = self;
[_currentAudioSession setDisposable:[[TGAudioSessionManager instance] requestSessionWithType:TGAudioSessionTypePlayAndRecord interrupted:^
2014-07-10 16:11:09 +02:00
{
2015-10-01 18:19:52 +02:00
__strong TGOpusAudioRecorder *strongSelf = weakSelf;
if (strongSelf != nil)
{
}
}]];
2014-07-10 16:11:09 +02:00
}];
}
- (void)_endAudioSession
{
2015-10-01 18:19:52 +02:00
id<SDisposable> currentAudioSession = _currentAudioSession;
2014-07-10 16:11:09 +02:00
[[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^
{
2015-10-01 18:19:52 +02:00
[currentAudioSession dispose];
2014-07-10 16:11:09 +02:00
}];
}
- (void)record
{
[[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^
{
2015-10-01 18:19:52 +02:00
[self _beginAudioSession];
2014-07-10 16:11:09 +02:00
AudioComponentDescription desc;
desc.componentType = kAudioUnitType_Output;
desc.componentSubType = kAudioUnitSubType_RemoteIO;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc);
AudioComponentInstanceNew(inputComponent, &globalRecorderContext.audioUnit);
OSStatus status = noErr;
static const UInt32 one = 1;
status = AudioUnitSetProperty(globalRecorderContext.audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &one, sizeof(one));
if (status != noErr)
{
TGLog(@"[TGOpusAudioRecorder#%x AudioUnitSetProperty kAudioOutputUnitProperty_EnableIO failed: %d]", self, (int)status);
[self cleanup];
return;
}
AudioStreamBasicDescription inputAudioFormat;
inputAudioFormat.mSampleRate = TGOpusAudioRecorderSampleRate;
inputAudioFormat.mFormatID = kAudioFormatLinearPCM;
inputAudioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
inputAudioFormat.mFramesPerPacket = 1;
inputAudioFormat.mChannelsPerFrame = 1;
inputAudioFormat.mBitsPerChannel = 16;
inputAudioFormat.mBytesPerPacket = 2;
inputAudioFormat.mBytesPerFrame = 2;
status = AudioUnitSetProperty(globalRecorderContext.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &inputAudioFormat, sizeof(inputAudioFormat));
if (status != noErr)
{
TGLog(@"[TGOpusAudioRecorder#%x AudioUnitSetProperty kAudioUnitProperty_StreamFormat failed: %d]", self, (int)status);
[self cleanup];
return;
}
AudioStreamBasicDescription audioFormat;
audioFormat.mSampleRate = TGOpusAudioRecorderSampleRate;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 1;
audioFormat.mBitsPerChannel = 16;
audioFormat.mBytesPerPacket = 2;
audioFormat.mBytesPerFrame = 2;
status = AudioUnitSetProperty(globalRecorderContext.audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &audioFormat, sizeof(audioFormat));
if (status != noErr)
{
TGLog(@"[TGOpusAudioRecorder#%x AudioUnitSetProperty kAudioUnitProperty_StreamFormat failed: %d]", self, (int)status);
[self cleanup];
return;
}
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = &TGOpusRecordingCallback;
2015-10-01 18:19:52 +02:00
callbackStruct.inputProcRefCon = (void *)(intptr_t)_recorderId;
2014-07-10 16:11:09 +02:00
if (AudioUnitSetProperty(globalRecorderContext.audioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 0, &callbackStruct, sizeof(callbackStruct)) != noErr)
{
TGLog(@"[TGOpusAudioRecorder#%x AudioUnitSetProperty kAudioOutputUnitProperty_SetInputCallback failed]", self);
[self cleanup];
return;
}
static const UInt32 zero = 0;
if (AudioUnitSetProperty(globalRecorderContext.audioUnit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, 0, &zero, sizeof(zero)) != noErr)
{
TGLog(@"[TGOpusAudioRecorder#%x AudioUnitSetProperty kAudioUnitProperty_ShouldAllocateBuffer failed]", self);
[self cleanup];
return;
}
_oggWriter = [[TGOggOpusWriter alloc] init];
2015-10-01 18:19:52 +02:00
if (![_oggWriter beginWithDataItem:_tempFileItem])
2014-07-10 16:11:09 +02:00
{
TGLog(@"[TGOpusAudioRecorder#%x error initializing ogg opus writer]", self);
[self cleanup];
return;
}
CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
status = AudioUnitInitialize(globalRecorderContext.audioUnit);
if (status == noErr)
globalRecorderContext.audioUnitInitialized = true;
else
{
TGLog(@"[TGOpusAudioRecorder#%x AudioUnitInitialize failed: %d]", self, (int)status);
[self cleanup];
return;
}
TGLog(@"[TGOpusAudioRecorder#%x setup time: %f ms]", self, (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0);
//if (playSoundSemaphore != nil)
// dispatch_semaphore_wait(playSoundSemaphore, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)));
status = AudioOutputUnitStart(globalRecorderContext.audioUnit);
if (status == noErr)
{
TGLog(@"[TGOpusAudioRecorder#%x initialization time: %f ms]", self, (CFAbsoluteTimeGetCurrent() - startTime) * 1000.0);
TGLog(@"[TGOpusAudioRecorder#%x started]", self);
globalRecorderContext.audioUnitStarted = true;
}
else
{
TGLog(@"[TGOpusAudioRecorder#%x AudioOutputUnitStart failed: %d]", self, (int)status);
[self cleanup];
}
}];
}
static OSStatus TGOpusRecordingCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, __unused AudioBufferList *ioData)
{
@autoreleasepool
{
if (globalRecorderContext.globalAudioRecorderId != (int)inRefCon)
return noErr;
AudioBuffer buffer;
buffer.mNumberChannels = 1;
buffer.mDataByteSize = inNumberFrames * 2;
buffer.mData = malloc(inNumberFrames * 2);
AudioBufferList bufferList;
bufferList.mNumberBuffers = 1;
bufferList.mBuffers[0] = buffer;
OSStatus status = AudioUnitRender(globalRecorderContext.audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &bufferList);
if (status == noErr)
{
[[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^
{
TGOpusAudioRecorder *recorder = globalRecorder;
2015-10-01 18:19:52 +02:00
if (recorder != nil && recorder.recorderId == (int)(intptr_t)inRefCon)
2014-07-10 16:11:09 +02:00
[recorder _processBuffer:&buffer];
free(buffer.mData);
}];
}
}
return noErr;
}
- (void)_processBuffer:(AudioBuffer const *)buffer
{
@autoreleasepool
{
if (_oggWriter == nil)
return;
static const int millisecondsPerPacket = 60;
static const int encoderPacketSizeInBytes = TGOpusAudioRecorderSampleRate / 1000 * millisecondsPerPacket * 2;
unsigned char currentEncoderPacket[encoderPacketSizeInBytes];
int bufferOffset = 0;
while (true)
{
int currentEncoderPacketSize = 0;
while (currentEncoderPacketSize < encoderPacketSizeInBytes)
{
if (_audioBuffer.length != 0)
{
int takenBytes = MIN((int)_audioBuffer.length, encoderPacketSizeInBytes - currentEncoderPacketSize);
if (takenBytes != 0)
{
memcpy(currentEncoderPacket + currentEncoderPacketSize, _audioBuffer.bytes, takenBytes);
[_audioBuffer replaceBytesInRange:NSMakeRange(0, takenBytes) withBytes:NULL length:0];
currentEncoderPacketSize += takenBytes;
}
}
else if (bufferOffset < (int)buffer->mDataByteSize)
{
int takenBytes = MIN((int)buffer->mDataByteSize - bufferOffset, encoderPacketSizeInBytes - currentEncoderPacketSize);
if (takenBytes != 0)
{
memcpy(currentEncoderPacket + currentEncoderPacketSize, ((const char *)buffer->mData) + bufferOffset, takenBytes);
bufferOffset += takenBytes;
currentEncoderPacketSize += takenBytes;
}
}
else
break;
}
if (currentEncoderPacketSize < encoderPacketSizeInBytes)
{
if (_audioBuffer == nil)
_audioBuffer = [[NSMutableData alloc] initWithCapacity:encoderPacketSizeInBytes];
[_audioBuffer appendBytes:currentEncoderPacket length:currentEncoderPacketSize];
break;
}
else
{
NSUInteger previousBytesWritten = [_oggWriter encodedBytes];
[_oggWriter writeFrame:currentEncoderPacket frameByteCount:(NSUInteger)currentEncoderPacketSize];
NSUInteger currentBytesWritten = [_oggWriter encodedBytes];
if (currentBytesWritten != previousBytesWritten)
{
[ActionStageInstance() dispatchOnStageQueue:^
{
TGLiveUploadActor *actor = (TGLiveUploadActor *)[ActionStageInstance() executingActorWithPath:_liveUploadPath];
[actor updateSize:currentBytesWritten];
}];
}
}
}
}
}
2015-10-01 18:19:52 +02:00
- (TGDataItem *)stopRecording:(NSTimeInterval *)recordedDuration liveData:(__autoreleasing TGLiveUploadActorData **)liveData
2014-07-10 16:11:09 +02:00
{
2015-10-01 18:19:52 +02:00
__block TGDataItem *dataItemResult = nil;
2014-07-10 16:11:09 +02:00
__block NSTimeInterval durationResult = 0.0;
__block NSUInteger totalBytes = 0;
[[TGOpusAudioRecorder processingQueue] dispatchOnQueue:^
{
if (_oggWriter != nil && [_oggWriter writeFrame:NULL frameByteCount:0])
{
2015-10-01 18:19:52 +02:00
dataItemResult = _tempFileItem;
2014-07-10 16:11:09 +02:00
durationResult = [_oggWriter encodedDuration];
totalBytes = [_oggWriter encodedBytes];
}
[self cleanup];
} synchronous:true];
if (recordedDuration != NULL)
*recordedDuration = durationResult;
if (liveData != NULL)
{
dispatch_sync([ActionStageInstance() globalStageDispatchQueue], ^
{
TGLiveUploadActor *actor = (TGLiveUploadActor *)[ActionStageInstance() executingActorWithPath:_liveUploadPath];
*liveData = [actor finishRestOfFile:totalBytes];
});
}
2015-10-01 18:19:52 +02:00
return dataItemResult;
2014-07-10 16:11:09 +02:00
}
- (NSTimeInterval)currentDuration
{
return [_oggWriter encodedDuration];
}
@end