11

I want to waveform display in real-time input from the microphone. I have been implemented using the installTapOnBus:bufferSize:format:block:, This function is called three times in one second. I want to set this function to be called 20 times per second. Where can I set?

AVAudioSession *audioSession = [AVAudioSession sharedInstance];

NSError* error = nil;
if (audioSession.isInputAvailable) [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
if(error){
    return;
}

[audioSession setActive:YES error:&error];
if(error){
    retur;
}

self.engine = [[[AVAudioEngine alloc] init] autorelease];

AVAudioMixerNode* mixer = [self.engine mainMixerNode];
AVAudioInputNode* input = [self.engine inputNode];
[self.engine connect:input to:mixer format:[input inputFormatForBus:0]];

// tap ... 1 call in 16537Frames
// It does not change even if you change the bufferSize
[input installTapOnBus:0 bufferSize:4096 format:[input inputFormatForBus:0] block:^(AVAudioPCMBuffer* buffer, AVAudioTime* when) {

    for (UInt32 i = 0; i < buffer.audioBufferList->mNumberBuffers; i++) {
        Float32 *data = buffer.audioBufferList->mBuffers[i].mData;
        UInt32 frames = buffer.audioBufferList->mBuffers[i].mDataByteSize / sizeof(Float32);

        // create waveform
        ...
    }
}];

[self.engine startAndReturnError:&error];
if (error) {
    return;
}
Melodybox
  • 121
  • 1
  • 6

5 Answers5

18

they say, Apple Support replied no: (on sep 2014)

Yes, currently internally we have a fixed tap buffer size (0.375s), and the client specified buffer size for the tap is not taking effect.

but someone resizes buffer size and gets 40ms https://devforums.apple.com/thread/249510?tstart=0

Can not check it, neen in ObjC :(

UPD it works! just single line:

    [input installTapOnBus:0 bufferSize:1024 format:[mixer outputFormatForBus:0] block:^(AVAudioPCMBuffer *buffer, AVAudioTime *when) {
    buffer.frameLength = 1024; //here
djdance
  • 3,110
  • 27
  • 33
  • 1
    Profiling a built app seems to suggest that the Buffer in question is a ring buffer, meaning that only the audio within the frameLength you specify is "consumed" and freed. It seems there is the short delay (the 0.375s in question?) before buffers start being processed, but afterwards it seems to work as expected. – ephemer Mar 18 '15 at 13:54
  • Please see this link https://stackoverflow.com/questions/45538806/how-to-cancel-or-remove-echo-repeated-sound-with-avaudioengine @djdance – Saurabh Jain Aug 10 '17 at 11:40
  • This is not safe under some circumstances; i.e. if writing the data to a file in a dispatch queue. If there is a delay for some reason, the next call to installTap will have a different buffer object, but the remaining samples will not be there so they will be lost. – Learn OpenGL ES Jan 18 '18 at 19:05
  • If you look in the header file of AVAudioNode, it describes the supported range of buffer duration: ```@param bufferSize the requested size of the incoming buffers in sample frames. Supported range is [100, 400] ms.``` – lahsuk Nov 19 '21 at 09:14
8

The AVAudioNode class reference states that the implementation may choose a buffer size other than the one that you supply, so as far as I know, we are stuck with the very large buffer size. This is unfortunate, because AVAudioEngine is otherwise an excellent Core Audio wrapper. Since I too need to use the input tap for something other than recording, I'm looking into The Amazing Audio Engine, as well as the Core Audio C API (see the iBook Learning Core Audio for excellent tutorials on it), as alternatives.

***Update: It turns out that you can access the AudioUnit of the AVAudioInputNode and install a render callback on it. Via AVAudioSession, you can set your audio session's desired buffer size (not guaranteed, but certainly better than node taps). Thus far, I've gotten buffer sizes as low as 64 samples using this approach. I'll post back here with code once I've had a chance to test this.

Jason McClinsey
  • 426
  • 5
  • 12
  • 1
    Unfortunately, my experiment did not succeed (installing a render callback on the input node). My callback function is called, and I'm getting valid buffers, however all of the sample values are 0. Presumably, there is special behavior for the input node preventing this from working. Sorry to not be able to solve our problem and make AVAudioEngine work for our purposes. I'm going to pursue a C++ implementation with the older API... – Jason McClinsey Oct 30 '14 at 18:15
  • Melodybox, if you wish to take the C/C++ road for your solution, take a look at https://github.com/abbood/Learning-Core-Audio-Book-Code-Sample. The book that goes with it is well worth buying, too. Not sure if you are working on OS X or iOS, but if you are working on iOS, look at the Chapter10_iOSPlayThrough project within said repository. – Jason McClinsey Oct 31 '14 at 00:22
  • https://devforums.apple.com/thread/249510?tstart=0 This code is weird, in that it seems to set the frame size during a buffer ready callback, and it works?! – Tom Andersen Dec 08 '14 at 20:44
  • I think it's a great idea. It was certainly reduce the callback time of tap. It is a worry that has been written about the memory leak. – Melodybox Dec 12 '14 at 18:39
6

As of iOS 13 in 2019, there is AVAudioSinkNode, which may better accomplish what you are looking for. While you could have also created a regular AVAudioUnit / Node and attached it to the input/output, the difference with an AVAudioSinkNode is that there is no output required. That makes it more like a tap and circumvents issues with incomplete chains that might occur when using a regular Audio Unit / Node.

For more information:

The relevant Swift code is on page 10 (with a small error corrected below) of the session's PDF.

// Create Engine
let engine = AVAudioEngine()
// Create and Attach AVAudioSinkNode
let sinkNode = AVAudioSinkNode() { (timeStamp, frames, audioBufferList) ->
OSStatus in
 …
}
engine.attach(sinkNode) 

I imagine that you'll still have to follow the typical real-time audio rules when using this (e.g. no allocating/freeing memory, no ObjC calls, no locking or waiting on locks, etc.). A ring buffer may still be helpful here.

user503821
  • 643
  • 6
  • 12
  • The sad part is that you'd have to process the audio in native device's format (e.g. take care of bit depth, byte endianness, etc) -- but it seems to be the only thing I can think of now... – akuz Mar 24 '20 at 16:12
1

Don't know why or even if this works yet, just trying a few things out. But for sure the NSLogs indicate a 21 ms interval, 1024 samples coming in per buffer...

        AVAudioEngine* sEngine = NULL;
        - (void)applicationDidBecomeActive:(UIApplication *)application 
        {
            /*
             Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
             */

            [glView startAnimation];

            AVAudioSession *audioSession = [AVAudioSession sharedInstance];

            NSError* error = nil;
            if (audioSession.isInputAvailable) [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
            if(error){
                return;
            }

            [audioSession setActive:YES error:&error];
            if(error){
                return;
            }

            sEngine = [[AVAudioEngine alloc] init];

            AVAudioMixerNode* mixer = [sEngine mainMixerNode];
            AVAudioInputNode* input = [sEngine inputNode];
            [sEngine connect:input to:mixer format:[input inputFormatForBus:0]];

            __block NSTimeInterval start = 0.0;

            // tap ... 1 call in 16537Frames
            // It does not change even if you change the bufferSize
            [input installTapOnBus:0 bufferSize:1024 format:[input inputFormatForBus:0] block:^(AVAudioPCMBuffer* buffer, AVAudioTime* when) {

                if (start == 0.0)
                    start = [AVAudioTime secondsForHostTime:[when hostTime]];

                // why does this work? because perhaps the smaller buffer is reused by the audioengine, with the code to dump new data into the block just using the block size as set here?
                // I am not sure that this is supported by apple?
                NSLog(@"buffer frame length %d", (int)buffer.frameLength);
                buffer.frameLength = 1024;
                UInt32 frames = 0;
                for (UInt32 i = 0; i < buffer.audioBufferList->mNumberBuffers; i++) {
                    Float32 *data = buffer.audioBufferList->mBuffers[i].mData;
                    frames = buffer.audioBufferList->mBuffers[i].mDataByteSize / sizeof(Float32);
                    // create waveform
                    ///
                }
                NSLog(@"%d frames are sent at %lf", (int) frames, [AVAudioTime secondsForHostTime:[when hostTime]] - start);
            }];

            [sEngine startAndReturnError:&error];
            if (error) {
                return;
            }

        }
Tom Andersen
  • 7,132
  • 3
  • 38
  • 55
-1

You might be able to use a CADisplayLink to achieve this. A CADisplayLink will give you a callback each time the screen refreshes, which typically will be much more than 20 times per second (so additional logic may be required to throttle or cap the number of times your method is executed in your case).

This is obviously a solution that is quite discrete from your audio work, and to the extent you require a solution that reflects your session, it might not work. But when we need frequent recurring callbacks on iOS, this is often the approach of choice, so it's an idea.

isaac
  • 4,867
  • 1
  • 21
  • 31