4

Is there a way that I could reverse and export .m4a audio file? I found a solution to reverse an audio track here, but it only seems to be working on .caf file formats. If the only way is to use a .caf, is there a way to convert the .m4a file to .caf first?

Update: In another post I found out that AVAssetReader can be used to read audio samples from an audio file, but I have no idea how to write the samples back in the reverse order. The below code snippet is an answer directly from the post. Any help would be appreciated. Thanks

+ (void) reverseAudioTrack: (AVAsset *)audioAsset outputURL: (NSURL *)outputURL {
NSError *error;

AVAssetReader* reader = [[AVAssetReader alloc] initWithAsset:audioAsset error:&error];
if (error) {NSLog(@"%@", error.localizedDescription);}

AVAssetTrack* track = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] objectAtIndex:0];

NSMutableDictionary* audioReadSettings = [NSMutableDictionary dictionary];
[audioReadSettings setValue:[NSNumber numberWithInt:kAudioFormatLinearPCM]
                     forKey:AVFormatIDKey];

AVAssetReaderTrackOutput* readerOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:track outputSettings:audioReadSettings];
[reader addOutput:readerOutput];
[reader startReading];

CMSampleBufferRef sample; //= [readerOutput copyNextSampleBuffer];
NSMutableArray *samples = [[NSMutableArray alloc] init];

// Get all samples
while((sample = [readerOutput copyNextSampleBuffer])) {
    [samples addObject:(__bridge id)sample];
    CFRelease(sample);
}

// Process samples in reverse
AudioChannelLayout acl;
bzero(&acl, sizeof(acl));
acl.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;

AVAssetWriter *writer = [[AVAssetWriter alloc] initWithURL:outputURL
                                                   fileType:AVFileTypeAppleM4A
                                                      error:&error];
if (error) {NSLog(@"%@", error.localizedDescription);}
NSDictionary *writerOutputSettings = [ NSDictionary dictionaryWithObjectsAndKeys:
                                      [ NSNumber numberWithInt: kAudioFormatAppleLossless ], AVFormatIDKey,
                                      [ NSNumber numberWithInt: 16 ], AVEncoderBitDepthHintKey,
                                      [ NSNumber numberWithFloat: 44100.0 ], AVSampleRateKey,
                                      [ NSNumber numberWithInt: 1 ], AVNumberOfChannelsKey,
                                      [ NSData dataWithBytes: &acl length: sizeof( acl ) ], AVChannelLayoutKey, nil ];

AVAssetWriterInput *audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:writerOutputSettings];

[writer addInput:audioWriterInput];
[writer startWriting];
[writer startSessionAtSourceTime:CMSampleBufferGetPresentationTimeStamp((__bridge CMSampleBufferRef)samples[0]) ];

// (1) Would it work if I loop in reverse here?
for (NSInteger i = 0; i < samples.count; i++) {
    CMBlockBufferRef buffer = CMSampleBufferGetDataBuffer((__bridge CMSampleBufferRef)samples[i]);

    CMItemCount numSamplesInBuffer = CMSampleBufferGetNumSamples((__bridge CMSampleBufferRef)samples[i]);
    AudioBufferList audioBufferList;
    CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer((__bridge CMSampleBufferRef)samples[i],
                                                            NULL,
                                                            &audioBufferList,
                                                            sizeof(audioBufferList),
                                                            NULL,
                                                            NULL,
                                                            kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment,
                                                            &buffer
                                                            );

    for (int bufferCount = 0; bufferCount < audioBufferList.mNumberBuffers; bufferCount++) {
        SInt16* samples = (SInt16 *)audioBufferList.mBuffers[bufferCount].mData;
        for (int i=0; i < numSamplesInBuffer; i++) {
            // amplitude for the sample is samples[i], assuming you have linear pcm to start with

            // (2) What should I be doing to write the samples into an audio file?
        }
    }
    CFRelease(buffer);
}
Community
  • 1
  • 1
Chan Jing Hong
  • 2,251
  • 4
  • 22
  • 41
  • What if you just revert sample after you create it in `// Get all samples`? – Guig Dec 29 '16 at 20:20
  • @Guig Yeah well the issue is at `// (2) What should I be doing to...` I don't know how to write the samples into a single audio file :/ – Chan Jing Hong Dec 30 '16 at 02:00

2 Answers2

1

Yes, there is a way you can process, then export, any of the audio files for which there is iOS support.

However, most of these formats (mp3 to name one) are lossy and compressed. You must first decompress the data, apply the transformation, and recompress. Most transformation you will apply to the audio information should likely be done at the raw, PCM level.

Combining these two statements, you do this in a few passes:

  1. convert original file to a kAudioFormatLinearPCM compliant audio file, like AIFF
  2. process that temporary file (reverse its content)
  3. convert the temporary file back to the original format

Just like if you were applying a transformation to, say, a compressed jpeg image, there will be degradation in the process. The final audio will have, at best, suffered one more compression cycle.

So the true mathematical answer to this approach is actually no.


Just for reference, here is some starter code in swift 3. It needs further refinement to skip the file headers.

var outAudioFile:AudioFileID?
var pcm = AudioStreamBasicDescription(mSampleRate: 44100.0,
                                      mFormatID: kAudioFormatLinearPCM,
                                      mFormatFlags: kAudioFormatFlagIsBigEndian | kAudioFormatFlagIsSignedInteger,
                                      mBytesPerPacket: 2,
                                      mFramesPerPacket: 1,
                                      mBytesPerFrame: 2,
                                      mChannelsPerFrame: 1,
                                      mBitsPerChannel: 16,
                                      mReserved: 0)

var theErr = AudioFileCreateWithURL(destUrl as CFURL!,
                                    kAudioFileAIFFType,
                                    &pcm,
                                    .eraseFile,
                                    &outAudioFile)
if noErr == theErr, let outAudioFile = outAudioFile {
    var inAudioFile:AudioFileID?
    theErr = AudioFileOpenURL(sourceUrl as! CFURL, .readPermission, 0, &inAudioFile)

    if noErr == theErr, let inAudioFile = inAudioFile {

        var fileDataSize:UInt64 = 0
        var thePropertySize:UInt32 = UInt32(MemoryLayout<UInt64>.stride)
        theErr = AudioFileGetProperty(inAudioFile,
                                      kAudioFilePropertyAudioDataByteCount,
                                      &thePropertySize,
                                      &fileDataSize)

        if( noErr == theErr) {
            let dataSize:Int64 = Int64(fileDataSize)
            let theData = UnsafeMutableRawPointer.allocate(bytes: Int(dataSize),
                                                           alignedTo: MemoryLayout<UInt8>.alignment)

            var readPoint:Int64 = Int64(dataSize)
            var writePoint:Int64 = 0

            while( readPoint > 0 )
            {
                var bytesToRead = UInt32(2)

                AudioFileReadBytes( inAudioFile, false, readPoint, &bytesToRead, theData)
                AudioFileWriteBytes( outAudioFile, false, writePoint, &bytesToRead, theData)

                writePoint += 2
                readPoint -= 2
            }

            theData.deallocate(bytes: Int(dataSize), alignedTo: MemoryLayout<UInt8>.alignment)

            AudioFileClose(inAudioFile);
            AudioFileClose(outAudioFile);
        }
    }
}
SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
  • Thanks for being so helpful! I've been stuck on this for quite some time. I've tried converting the audio file from .m4a to .aiff but for some reason it's not working. I asked [another question](http://stackoverflow.com/questions/41436229/converting-m4a-file-to-aiff-using-audioconverter-swift) can you help me with that? – Chan Jing Hong Jan 03 '17 at 04:02
0

I found a solution on GitHub from this repo: https://github.com/tomisacat/AudioReverse

The function below worked perfectly for me for reversing an m4a file

    func reverse(fromUrl: URL) -> URL? {
    do {
        let inFile: AVAudioFile = try AVAudioFile(forReading: fromUrl)
        let format: AVAudioFormat = inFile.processingFormat
        let frameCount: AVAudioFrameCount = UInt32(inFile.length)
        let outSettings = [AVNumberOfChannelsKey: format.channelCount,
                           AVSampleRateKey: format.sampleRate,
                           AVLinearPCMBitDepthKey: 16,
                           AVFormatIDKey: kAudioFormatMPEG4AAC] as [String : Any]
        let outputPath = NSTemporaryDirectory() + "/" + "reverse.m4a"
        let outputUrl = URL(fileURLWithPath: outputPath)
        let outFile: AVAudioFile = try AVAudioFile(forWriting: outputUrl, settings: outSettings)
        let forwardBuffer: AVAudioPCMBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount)
        let reverseBuffer: AVAudioPCMBuffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: frameCount)
        
        try inFile.read(into: forwardBuffer)
        let frameLength = forwardBuffer.frameLength
        reverseBuffer.frameLength = frameLength
        let audioStride = forwardBuffer.stride
        
        for channelIdx in 0..<forwardBuffer.format.channelCount {
            let forwardChannelData = forwardBuffer.floatChannelData?.advanced(by: Int(channelIdx)).pointee
            let reverseChannelData = reverseBuffer.floatChannelData?.advanced(by: Int(channelIdx)).pointee
            
            var reverseIdx: Int = 0
            for idx in stride(from: frameLength, to: 0, by: -1) {
                memcpy(reverseChannelData?.advanced(by: reverseIdx * audioStride), forwardChannelData?.advanced(by: Int(idx) * audioStride), MemoryLayout<Float>.size)
                reverseIdx += 1
            }
        }
        
        try outFile.write(from: reverseBuffer)
        
        return outputUrl
    } catch let error {
        print(error.localizedDescription)
        
        return nil
    }
}