ExtAudioFileWrite creates silent m4a

260 views Asked by At

I am making an app that requires a variable amount of audio files to be merged into one. To implement this, I am making use of the multi-channel mixer audio unit. The mixer successfully mixes the tracks, and, if I set the I/O unit in my AUGraph to RemoteIO, it successfully plays the sound.

However, I would like to save the new sound to a file; I figured the best way to do this would be using Generic Output instead of RemoteIO. The file is successfully written and is the correct length (in seconds), but, when played, is silent. Here is how I create the AUGraph (I'll remove as much as possible for brevity)

- (void)createGraph
{
    OSStatus result = NewAUGraph(&graph);

    AudioComponentDescription mixerDescription;
    mixerDescription.componentType = kAudioUnitType_Mixer;
    mixerDescription.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixerDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixerDescription.componentFlags = 0;
    mixerDescription.componentFlagsMask = 0;

    AudioComponentDescription remoteIODescription;
    remoteIODescription.componentType = kAudioUnitType_Output;
    remoteIODescription.componentSubType = kAudioUnitSubType_GenericOutput;
    remoteIODescription.componentManufacturer = kAudioUnitManufacturer_Apple;
    remoteIODescription.componentFlags = 0;
    remoteIODescription.componentFlagsMask = 0;

    AUNode iONode, mixerNode;

    result = AUGraphAddNode(graph, &remoteIODescription, &iONode);
    result = AUGraphAddNode(graph, &mixerDescription, &mixerNode);
    result = AUGraphOpen(graph);
    result = AUGraphNodeInfo(graph, mixerNode, NULL, &mixerUnit);
    result = AUGraphNodeInfo(graph, iONode, NULL, &iOUnit);
    result = AUGraphConnectNodeInput(graph, mixerNode, 0, iONode, 0);

    UInt32 busCount = (UInt32)fileCount;

    result = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &busCount, sizeof(busCount));

    UInt32 maximumFramesPerSlice = 4096;

    result = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, sizeof(maximumFramesPerSlice));

    for (UInt16 busNumber = 0; busNumber < busCount; busNumber++)
    {
        AURenderCallbackStruct renderCallback;
        renderCallback.inputProc = &inputRenderCallback;
        renderCallback.inputProcRefCon = (__bridge void *)self;

        result = AUGraphSetNodeInputCallback(graph, mixerNode, busNumber, &renderCallback);

        //sets certain parameters for the mixer; I don't believe this is the cause of the problem
        AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Pan, kAudioUnitScope_Input, busNumber, fileSettings[busNumber].pan, 0);
        AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, busNumber, fileSettings[busNumber].volume, 0);
        AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Enable, kAudioUnitScope_Input, busNumber, fileSettings[busNumber].enabled, 0);

        if (soundStructs[busNumber].isStereo == YES)
        {
            result = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, busNumber, &stereoDescription, sizeof(stereoDescription));
        }
        else
        {
            result = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, busNumber, &monoDescription, sizeof(monoDescription));
        }
    }

    Float64 sampleRate = SAMPLE_RATE; // 44100.0
    result = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Output, 0, &sampleRate, sizeof(sampleRate));

    CFURLRef url = (__bridge CFURLRef)[NSURL fileURLWithPath:ofn];

    AudioStreamBasicDescription destinationFormat;
    memset(&destinationFormat, 0, sizeof(destinationFormat));
    destinationFormat.mChannelsPerFrame = 2;
    destinationFormat.mFormatID = kAudioFormatMPEG4AAC;
    destinationFormat.mFormatFlags = kMPEG4Object_AAC_Main;
    destinationFormat.mSampleRate = sampleRate;

    UInt32 size = sizeof(destinationFormat);
    result = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &size, &destinationFormat);

    result = ExtAudioFileCreateWithURL(url, kAudioFileM4AType, &destinationFormat, NULL, kAudioFileFlags_EraseFile, &outputFile);

    AudioStreamBasicDescription clientFormat;
    memset(&clientFormat, 0, sizeof(clientFormat));

    size = sizeof(clientFormat);
    result = AudioUnitGetProperty(iOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, & clientFormat, &size);

    UInt32 codec = kAppleHardwareAudioCodecManufacturer;
    ExtAudioFileSetProperty(outputFile, kExtAudioFileProperty_CodecManufacturer, sizeof(codec), &codec);

    ExtAudioFileSetProperty(outputFile, kExtAudioFileProperty_ClientDataFormat, sizeof(clientFormat), &clientFormat);

    result = AUGraphInitialize(graph);
}

And this is how I save the file

- (void)startGraph
{
    AudioUnitRenderActionFlags flags = 0;
    AudioTimeStamp inTimeStamp;
    memset(&inTimeStamp, 0, sizeof(AudioTimeStamp));
    inTimeStamp.mFlags = kAudioTimeStampSampleTimeValid;
    UInt32 busNumber = 0;
    UInt32 numberFrames = 1024;
    inTimeStamp.mSampleTime = 0;
    int channelCount = 2;
    SInt64 totFrms = 0;

    for (int i = 0; i < fileCount; i++) //gets the length for the longest recording in the new track
    {
        SInt64 len = soundStructs[i].totalFrames;
        if (len > totFrms && fileSettings[i].enabled == YES)
            totFrms = len;
    }
    while (totFrms > 0)
    {
        if (totFrms < numberFrames)
            numberFrames = (UInt32)totFrms;
        else
            totFrms -= numberFrames;
        AudioBufferList *bufferList = (AudioBufferList*)malloc(sizeof(AudioBufferList)+sizeof(AudioBuffer)*(channelCount-1));
        bufferList->mNumberBuffers = channelCount;
        for (int j=0; j<channelCount; j++)
        {
            AudioBuffer buffer = {0};
            buffer.mNumberChannels = 1;
            buffer.mDataByteSize = numberFrames*sizeof(SInt32);
            buffer.mData = calloc(numberFrames, sizeof(SInt32));

            bufferList->mBuffers[j] = buffer;
        }
        AudioUnitRender(iOUnit, &flags, &inTimeStamp, busNumber, numberFrames, bufferList);

        OSStatus res = ExtAudioFileWrite(outputFile, numberFrames, bufferList);
        NSAssert(res == noErr, @"Res != noerr");
    }

    ExtAudioFileDispose(outputFile);
}

Because the question is already very long, I won't add the callback function used by the mixer for input or the method used to load the files into memory, as the fact that RemoteIO works leads me to believe there is nothing wrong with these. So, why is the output file created by my graph silent?

1

There are 1 answers

0
dave234 On BEST ANSWER

After you call AudioUnitRender, you need to increment the time stamp's sample time for the next iteration.

AudioUnitRender(iOUnit, &flags, &inTimeStamp, busNumber, numberFrames, bufferList);
inTimeStamp.mSampleTime += numberFrames;