stream media FROM iphone

我只是一个虾纸丫 提交于 2019-11-29 23:23:40

My work on this topic has been staggering and long. I have finally gotten this to work however hacked it may be. Because of that I will list some warnings prior to posting the answer:

  1. There is still a clicking noise between buffers

  2. I get warnings due to the way I use my obj-c classes in the obj-c++ class, so there is something wrong there (however from my research using a pool does the same as release so I dont believe this matters to much):

    Object 0x13cd20 of class __NSCFString autoreleased with no pool in place - just leaking - break on objc_autoreleaseNoPool() to debug

  3. In order to get this working I had to comment out all AQPlayer references from SpeakHereController (see below) due to errors I couldnt fix any other way. It didnt matter for me however since I am only recording

So the main answer to the above is that there is a bug in AVAssetWriter that stopped it from appending the bytes and writing the audio data. I finally found this out after contacting apple support and have them notify me about this. As far as I know the bug is specific to ulaw and AVAssetWriter though I havnt tried many other formats to verify.
In response to this the only other option is/was to use AudioQueues. Something I had tried before but had brought a bunch of problems. The biggest problem being my lack of knowledge in obj-c++. The class below that got things working is from the speakHere example with slight changes so that the audio is ulaw formatted. The other problems came about trying to get all files to play nicely. However this was easily remedied by changing all filenames in the chain to .mm . The next problem was trying to use the classes in harmony. This is still a WIP, and ties into warning number 2. But my basic solution to this was to use the SpeakHereController (also included in the speakhere example) instead of directly accessing AQRecorder.

Anyways here is the code:

Using the SpeakHereController from an obj-c class

.h

@property(nonatomic,strong) SpeakHereController * recorder;

.mm

[init method]
        //AQRecorder wrapper (SpeakHereController) allocation
        _recorder = [[SpeakHereController alloc]init];
        //AQRecorder wrapper (SpeakHereController) initialization
        //technically this class is a controller and thats why its init method is awakeFromNib
        [_recorder awakeFromNib];

[recording]
     bool buttonState = self.audioRecord.isSelected;
[self.audioRecord setSelected:!buttonState];

if ([self.audioRecord isSelected]) {

    [self.recorder startRecord];
}else {
    [self.recorder stopRecord];
}

SpeakHereController

#import "SpeakHereController.h"

@implementation SpeakHereController

@synthesize player;
@synthesize recorder;

@synthesize btn_record;
@synthesize btn_play;
@synthesize fileDescription;
@synthesize lvlMeter_in;
@synthesize playbackWasInterrupted;

char *OSTypeToStr(char *buf, OSType t)
{
    char *p = buf;
    char str[4], *q = str;
    *(UInt32 *)str = CFSwapInt32(t);
    for (int i = 0; i < 4; ++i) {
        if (isprint(*q) && *q != '\\')
            *p++ = *q++;
        else {
            sprintf(p, "\\x%02x", *q++);
            p += 4;
        }
    }
    *p = '\0';
    return buf;
}

-(void)setFileDescriptionForFormat: (CAStreamBasicDescription)format withName:(NSString*)name
{
    char buf[5];
    const char *dataFormat = OSTypeToStr(buf, format.mFormatID);
    NSString* description = [[NSString alloc] initWithFormat:@"(%d ch. %s @ %g Hz)", format.NumberChannels(), dataFormat, format.mSampleRate, nil];
    fileDescription.text = description;
    [description release];  
}

#pragma mark Playback routines

-(void)stopPlayQueue
{
//  player->StopQueue();
    [lvlMeter_in setAq: nil];
    btn_record.enabled = YES;
}

-(void)pausePlayQueue
{
//  player->PauseQueue();
    playbackWasPaused = YES;
}


-(void)startRecord
{
    //    recorder = new AQRecorder();

    if (recorder->IsRunning()) // If we are currently recording, stop and save the file.
    {
        [self stopRecord];
    }
    else // If we're not recording, start.
    {
        //      btn_play.enabled = NO;  

        // Set the button's state to "stop"
        //      btn_record.title = @"Stop";

        // Start the recorder
        recorder->StartRecord(CFSTR("recordedFile.caf"));

        [self setFileDescriptionForFormat:recorder->DataFormat() withName:@"Recorded File"];

        // Hook the level meter up to the Audio Queue for the recorder
        //      [lvlMeter_in setAq: recorder->Queue()];
    } 
}

- (void)stopRecord
{
    // Disconnect our level meter from the audio queue
//  [lvlMeter_in setAq: nil];

    recorder->StopRecord();

    // dispose the previous playback queue
//  player->DisposeQueue(true);

    // now create a new queue for the recorded file
    recordFilePath = (CFStringRef)[NSTemporaryDirectory() stringByAppendingPathComponent: @"recordedFile.caf"];
//  player->CreateQueueForFile(recordFilePath);

    // Set the button's state back to "record"
//  btn_record.title = @"Record";
//  btn_play.enabled = YES;
}

- (IBAction)play:(id)sender
{
    if (player->IsRunning())
    {
        if (playbackWasPaused) {
//          OSStatus result = player->StartQueue(true);
//          if (result == noErr)
//              [[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueResumed" object:self];
        }
        else
//          [self stopPlayQueue];
            nil;
    }
    else
    {       
//      OSStatus result = player->StartQueue(false);
//      if (result == noErr)
//          [[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueResumed" object:self];
    }
}

- (IBAction)record:(id)sender
{
    if (recorder->IsRunning()) // If we are currently recording, stop and save the file.
    {
        [self stopRecord];
    }
    else // If we're not recording, start.
    {
//      btn_play.enabled = NO;  
//      
//      // Set the button's state to "stop"
//      btn_record.title = @"Stop";

        // Start the recorder
        recorder->StartRecord(CFSTR("recordedFile.caf"));

        [self setFileDescriptionForFormat:recorder->DataFormat() withName:@"Recorded File"];

        // Hook the level meter up to the Audio Queue for the recorder
        [lvlMeter_in setAq: recorder->Queue()];
    }   
}
#pragma mark AudioSession listeners
void interruptionListener(  void *  inClientData,
                            UInt32  inInterruptionState)
{
    SpeakHereController *THIS = (SpeakHereController*)inClientData;
    if (inInterruptionState == kAudioSessionBeginInterruption)
    {
        if (THIS->recorder->IsRunning()) {
            [THIS stopRecord];
        }
        else if (THIS->player->IsRunning()) {
            //the queue will stop itself on an interruption, we just need to update the UI
            [[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueStopped" object:THIS];
            THIS->playbackWasInterrupted = YES;
        }
    }
    else if ((inInterruptionState == kAudioSessionEndInterruption) && THIS->playbackWasInterrupted)
    {
        // we were playing back when we were interrupted, so reset and resume now
//      THIS->player->StartQueue(true);
        [[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueResumed" object:THIS];
        THIS->playbackWasInterrupted = NO;
    }
}

void propListener(  void *                  inClientData,
                    AudioSessionPropertyID  inID,
                    UInt32                  inDataSize,
                    const void *            inData)
{
    SpeakHereController *THIS = (SpeakHereController*)inClientData;
    if (inID == kAudioSessionProperty_AudioRouteChange)
    {
        CFDictionaryRef routeDictionary = (CFDictionaryRef)inData;          
        //CFShow(routeDictionary);
        CFNumberRef reason = (CFNumberRef)CFDictionaryGetValue(routeDictionary, CFSTR(kAudioSession_AudioRouteChangeKey_Reason));
        SInt32 reasonVal;
        CFNumberGetValue(reason, kCFNumberSInt32Type, &reasonVal);
        if (reasonVal != kAudioSessionRouteChangeReason_CategoryChange)
        {
            /*CFStringRef oldRoute = (CFStringRef)CFDictionaryGetValue(routeDictionary, CFSTR(kAudioSession_AudioRouteChangeKey_OldRoute));
            if (oldRoute)   
            {
                printf("old route:\n");
                CFShow(oldRoute);
            }
            else 
                printf("ERROR GETTING OLD AUDIO ROUTE!\n");

            CFStringRef newRoute;
            UInt32 size; size = sizeof(CFStringRef);
            OSStatus error = AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &size, &newRoute);
            if (error) printf("ERROR GETTING NEW AUDIO ROUTE! %d\n", error);
            else
            {
                printf("new route:\n");
                CFShow(newRoute);
            }*/

            if (reasonVal == kAudioSessionRouteChangeReason_OldDeviceUnavailable)
            {           
                if (THIS->player->IsRunning()) {
                    [THIS pausePlayQueue];
                    [[NSNotificationCenter defaultCenter] postNotificationName:@"playbackQueueStopped" object:THIS];
                }       
            }

            // stop the queue if we had a non-policy route change
            if (THIS->recorder->IsRunning()) {
                [THIS stopRecord];
            }
        }   
    }
    else if (inID == kAudioSessionProperty_AudioInputAvailable)
    {
        if (inDataSize == sizeof(UInt32)) {
            UInt32 isAvailable = *(UInt32*)inData;
            // disable recording if input is not available
            THIS->btn_record.enabled = (isAvailable > 0) ? YES : NO;
        }
    }
}

#pragma mark Initialization routines
- (void)awakeFromNib
{       
    // Allocate our singleton instance for the recorder & player object
    recorder = new AQRecorder();
    player = nil;//new AQPlayer();

    OSStatus error = AudioSessionInitialize(NULL, NULL, interruptionListener, self);
    if (error) printf("ERROR INITIALIZING AUDIO SESSION! %d\n", error);
    else 
    {
        UInt32 category = kAudioSessionCategory_PlayAndRecord;  
        error = AudioSessionSetProperty(kAudioSessionProperty_AudioCategory, sizeof(category), &category);
        if (error) printf("couldn't set audio category!");

        error = AudioSessionAddPropertyListener(kAudioSessionProperty_AudioRouteChange, propListener, self);
        if (error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", error);
        UInt32 inputAvailable = 0;
        UInt32 size = sizeof(inputAvailable);

        // we do not want to allow recording if input is not available
        error = AudioSessionGetProperty(kAudioSessionProperty_AudioInputAvailable, &size, &inputAvailable);
        if (error) printf("ERROR GETTING INPUT AVAILABILITY! %d\n", error);
//      btn_record.enabled = (inputAvailable) ? YES : NO;

        // we also need to listen to see if input availability changes
        error = AudioSessionAddPropertyListener(kAudioSessionProperty_AudioInputAvailable, propListener, self);
        if (error) printf("ERROR ADDING AUDIO SESSION PROP LISTENER! %d\n", error);

        error = AudioSessionSetActive(true); 
        if (error) printf("AudioSessionSetActive (true) failed");
    }

//  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackQueueStopped:) name:@"playbackQueueStopped" object:nil];
//  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(playbackQueueResumed:) name:@"playbackQueueResumed" object:nil];

//  UIColor *bgColor = [[UIColor alloc] initWithRed:.39 green:.44 blue:.57 alpha:.5];
//  [lvlMeter_in setBackgroundColor:bgColor];
//  [lvlMeter_in setBorderColor:bgColor];
//  [bgColor release];

    // disable the play button since we have no recording to play yet
//  btn_play.enabled = NO;
//  playbackWasInterrupted = NO;
//  playbackWasPaused = NO;
}

# pragma mark Notification routines
- (void)playbackQueueStopped:(NSNotification *)note
{
    btn_play.title = @"Play";
    [lvlMeter_in setAq: nil];
    btn_record.enabled = YES;
}

- (void)playbackQueueResumed:(NSNotification *)note
{
    btn_play.title = @"Stop";
    btn_record.enabled = NO;
    [lvlMeter_in setAq: player->Queue()];
}

#pragma mark Cleanup
- (void)dealloc
{
    [btn_record release];
    [btn_play release];
    [fileDescription release];
    [lvlMeter_in release];

//  delete player;
    delete recorder;

    [super dealloc];
}

@end

AQRecorder (.h has 2 lines of importance

#define kNumberRecordBuffers    3
#define kBufferDurationSeconds 5.0

)

#include "AQRecorder.h"
//#include "UploadAudioWrapperInterface.h"
//#include "RestClient.h"

RestClient * restClient;
NSData* data;

// ____________________________________________________________________________________
// Determine the size, in bytes, of a buffer necessary to represent the supplied number
// of seconds of audio data.
int AQRecorder::ComputeRecordBufferSize(const AudioStreamBasicDescription *format, float seconds)
{
    int packets, frames, bytes = 0;
    try {
        frames = (int)ceil(seconds * format->mSampleRate);

        if (format->mBytesPerFrame > 0)
            bytes = frames * format->mBytesPerFrame;
        else {
            UInt32 maxPacketSize;
            if (format->mBytesPerPacket > 0)
                maxPacketSize = format->mBytesPerPacket;    // constant packet size
            else {
                UInt32 propertySize = sizeof(maxPacketSize);
                XThrowIfError(AudioQueueGetProperty(mQueue, kAudioQueueProperty_MaximumOutputPacketSize, &maxPacketSize,
                                                 &propertySize), "couldn't get queue's maximum output packet size");
            }
            if (format->mFramesPerPacket > 0)
                packets = frames / format->mFramesPerPacket;
            else
                packets = frames;   // worst-case scenario: 1 frame in a packet
            if (packets == 0)       // sanity check
                packets = 1;
            bytes = packets * maxPacketSize;
        }
    } catch (CAXException e) {
        char buf[256];
        fprintf(stderr, "Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
        return 0;
    }   
    return bytes;
}

// ____________________________________________________________________________________
// AudioQueue callback function, called when an input buffers has been filled.
void AQRecorder::MyInputBufferHandler(  void *                              inUserData,
                                        AudioQueueRef                       inAQ,
                                        AudioQueueBufferRef                 inBuffer,
                                        const AudioTimeStamp *              inStartTime,
                                        UInt32                              inNumPackets,
                                        const AudioStreamPacketDescription* inPacketDesc)
{
    AQRecorder *aqr = (AQRecorder *)inUserData;


    try {
        if (inNumPackets > 0) {
            // write packets to file
//          XThrowIfError(AudioFileWritePackets(aqr->mRecordFile, FALSE, inBuffer->mAudioDataByteSize,
//                                           inPacketDesc, aqr->mRecordPacket, &inNumPackets, inBuffer->mAudioData),
//                     "AudioFileWritePackets failed");
            aqr->mRecordPacket += inNumPackets;



//            int numBytes = inBuffer->mAudioDataByteSize;       
//            SInt8 *testBuffer = (SInt8*)inBuffer->mAudioData;
//            
//            for (int i=0; i < numBytes; i++)
//            {
//                SInt8 currentData = testBuffer[i];
//                printf("Current data in testbuffer is %d", currentData);
//                
//                NSData * temp = [NSData dataWithBytes:currentData length:sizeof(currentData)];
//            }


            data=[[NSData dataWithBytes:inBuffer->mAudioData length:inBuffer->mAudioDataByteSize]retain];

            [restClient uploadAudioData:data url:nil];

        }


        // if we're not stopping, re-enqueue the buffer so that it gets filled again
        if (aqr->IsRunning())
            XThrowIfError(AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL), "AudioQueueEnqueueBuffer failed");
    } catch (CAXException e) {
        char buf[256];
        fprintf(stderr, "Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
    }

}

AQRecorder::AQRecorder()
{
    mIsRunning = false;
    mRecordPacket = 0;

    data = [[NSData alloc]init];
    restClient = [[RestClient sharedManager]retain];
}

AQRecorder::~AQRecorder()
{
    AudioQueueDispose(mQueue, TRUE);
    AudioFileClose(mRecordFile);

    if (mFileName){
     CFRelease(mFileName);   
    }

    [restClient release];
    [data release];
}

// ____________________________________________________________________________________
// Copy a queue's encoder's magic cookie to an audio file.
void AQRecorder::CopyEncoderCookieToFile()
{
    UInt32 propertySize;
    // get the magic cookie, if any, from the converter     
    OSStatus err = AudioQueueGetPropertySize(mQueue, kAudioQueueProperty_MagicCookie, &propertySize);

    // we can get a noErr result and also a propertySize == 0
    // -- if the file format does support magic cookies, but this file doesn't have one.
    if (err == noErr && propertySize > 0) {
        Byte *magicCookie = new Byte[propertySize];
        UInt32 magicCookieSize;
        XThrowIfError(AudioQueueGetProperty(mQueue, kAudioQueueProperty_MagicCookie, magicCookie, &propertySize), "get audio converter's magic cookie");
        magicCookieSize = propertySize; // the converter lies and tell us the wrong size

        // now set the magic cookie on the output file
        UInt32 willEatTheCookie = false;
        // the converter wants to give us one; will the file take it?
        err = AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie);
        if (err == noErr && willEatTheCookie) {
            err = AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, magicCookieSize, magicCookie);
            XThrowIfError(err, "set audio file's magic cookie");
        }
        delete[] magicCookie;
    }
}

void AQRecorder::SetupAudioFormat(UInt32 inFormatID)
{
    memset(&mRecordFormat, 0, sizeof(mRecordFormat));

    UInt32 size = sizeof(mRecordFormat.mSampleRate);
    XThrowIfError(AudioSessionGetProperty(  kAudioSessionProperty_CurrentHardwareSampleRate,
                                        &size, 
                                        &mRecordFormat.mSampleRate), "couldn't get hardware sample rate");

    //override samplearate to 8k from device sample rate

    mRecordFormat.mSampleRate = 8000.0;

    size = sizeof(mRecordFormat.mChannelsPerFrame);
    XThrowIfError(AudioSessionGetProperty(  kAudioSessionProperty_CurrentHardwareInputNumberChannels, 
                                        &size, 
                                        &mRecordFormat.mChannelsPerFrame), "couldn't get input channel count");


//    mRecordFormat.mChannelsPerFrame = 1;

    mRecordFormat.mFormatID = inFormatID;
    if (inFormatID == kAudioFormatLinearPCM)
    {
        // if we want pcm, default to signed 16-bit little-endian
        mRecordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        mRecordFormat.mBitsPerChannel = 16;
        mRecordFormat.mBytesPerPacket = mRecordFormat.mBytesPerFrame = (mRecordFormat.mBitsPerChannel / 8) * mRecordFormat.mChannelsPerFrame;
        mRecordFormat.mFramesPerPacket = 1;
    }

    if (inFormatID == kAudioFormatULaw) {
//        NSLog(@"is ulaw");
        mRecordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
        mRecordFormat.mSampleRate = 8000.0;
//        mRecordFormat.mFormatFlags = 0;
        mRecordFormat.mFramesPerPacket = 1;
        mRecordFormat.mChannelsPerFrame = 1;
        mRecordFormat.mBitsPerChannel = 16;//was 8
        mRecordFormat.mBytesPerPacket = 1;
        mRecordFormat.mBytesPerFrame = 1;
    }
}

NSString * GetDocumentDirectory(void)
{    
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *basePath = ([paths count] > 0) ? [paths objectAtIndex:0] : nil;
    return basePath;
}


void AQRecorder::StartRecord(CFStringRef inRecordFile)
{
    int i, bufferByteSize;
    UInt32 size;
    CFURLRef url;

    try {       
        mFileName = CFStringCreateCopy(kCFAllocatorDefault, inRecordFile);

        // specify the recording format
        SetupAudioFormat(kAudioFormatULaw /*kAudioFormatLinearPCM*/);

        // create the queue
        XThrowIfError(AudioQueueNewInput(
                                      &mRecordFormat,
                                      MyInputBufferHandler,
                                      this /* userData */,
                                      NULL /* run loop */, NULL /* run loop mode */,
                                      0 /* flags */, &mQueue), "AudioQueueNewInput failed");

        // get the record format back from the queue's audio converter --
        // the file may require a more specific stream description than was necessary to create the encoder.
        mRecordPacket = 0;

        size = sizeof(mRecordFormat);
        XThrowIfError(AudioQueueGetProperty(mQueue, kAudioQueueProperty_StreamDescription,  
                                         &mRecordFormat, &size), "couldn't get queue's format");

        NSString *basePath = GetDocumentDirectory();
        NSString *recordFile = [basePath /*NSTemporaryDirectory()*/ stringByAppendingPathComponent: (NSString*)inRecordFile];   

        url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)recordFile, NULL);

        // create the audio file
        XThrowIfError(AudioFileCreateWithURL(url, kAudioFileCAFType, &mRecordFormat, kAudioFileFlags_EraseFile,
                                          &mRecordFile), "AudioFileCreateWithURL failed");
        CFRelease(url);

        // copy the cookie first to give the file object as much info as we can about the data going in
        // not necessary for pcm, but required for some compressed audio
        CopyEncoderCookieToFile();


        // allocate and enqueue buffers
        bufferByteSize = ComputeRecordBufferSize(&mRecordFormat, kBufferDurationSeconds);   // enough bytes for half a second
        for (i = 0; i < kNumberRecordBuffers; ++i) {
            XThrowIfError(AudioQueueAllocateBuffer(mQueue, bufferByteSize, &mBuffers[i]),
                       "AudioQueueAllocateBuffer failed");
            XThrowIfError(AudioQueueEnqueueBuffer(mQueue, mBuffers[i], 0, NULL),
                       "AudioQueueEnqueueBuffer failed");
        }
        // start the queue
        mIsRunning = true;
        XThrowIfError(AudioQueueStart(mQueue, NULL), "AudioQueueStart failed");
    }
    catch (CAXException &e) {
        char buf[256];
        fprintf(stderr, "Error: %s (%s)\n", e.mOperation, e.FormatError(buf));
    }
    catch (...) {
        fprintf(stderr, "An unknown error occurred\n");
    }   

}

void AQRecorder::StopRecord()
{
    // end recording
    mIsRunning = false;
//    XThrowIfError(AudioQueueReset(mQueue), "AudioQueueStop failed");  
    XThrowIfError(AudioQueueStop(mQueue, true), "AudioQueueStop failed");   
    // a codec may update its cookie at the end of an encoding session, so reapply it to the file now
    CopyEncoderCookieToFile();
    if (mFileName)
    {
        CFRelease(mFileName);
        mFileName = NULL;
    }
    AudioQueueDispose(mQueue, true);
    AudioFileClose(mRecordFile);
}

Please feel free to comment or refine my answer, I will accept it as the answer if its a better solution. Please note this was my first attempt and Im sure it is not the most elegant or proper solution.

You could use the gamekit Framework? Then send the audio over bluetooth. There are examples in the ios developer library

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!