Music convert and how to know if writing are completed

436 views Asked by At

i had to convert big file size song from iTunes library to a smaller 8K song file.

As i did the converting async, the bool always return true even though writing to doc folder are not completed. At the moment i'm using a delay of 10sec before i called the function again and it works fine on the interim for iPhone 5s, but i would like to cater on the slower devices.

kindly give me some pointer / recommendation on my code.

-(void)startUploadSongAnalysis
{
     [self updateProgressYForID3NForUpload:NO];

    if ([self.uploadWorkingAray count]>=1)
    {
        Song *songVar = [self.uploadWorkingAray objectAtIndex:0];//core data var
        NSLog(@"songVar %@",songVar.songName);
        NSLog(@"songVar %@",songVar.songURL);
        NSURL *songU = [NSURL URLWithString:songVar.songURL]; //URL of iTunes Lib
       // self.asset = [AVAsset assetWithURL:songU];
       // NSLog(@"asset %@",self.asset);
        NSError *error;
        NSString *subString = [[songVar.songURL componentsSeparatedByString:@"id="] lastObject];
        NSString *savedPath = [self.documentsDir stringByAppendingPathComponent:[NSString stringWithFormat:@"audio%@.m4a",subString]];//save file name of converted 8kb song
        NSString *subStringPath = [NSString stringWithFormat:@"audio%@.m4a",subString];

        if ([self.fileManager fileExistsAtPath:savedPath] == YES)
            [self.fileManager removeItemAtPath:savedPath error:&error];
        NSLog(@"cacheDir %@",savedPath);

        //export low bitrate song to cache
        if ([self exportAudio:[AVAsset assetWithURL:songU] toFilePath:savedPath]) // HERE IS THE PROBLEM, this return true even the writing is not completed cos when i upload to my web server, it will say song file corrupted
        {
           // [self performSelector:@selector(sendSongForUpload:) withObject:subStringPath afterDelay:1];
            [self sendRequest:2 andPath:subStringPath andSongDBItem:songVar];
        }
        else
        {
            NSLog(@"song too short, skipped");
            [self.uploadWorkingAray removeObjectAtIndex:0];
            [self.songNotFoundArray addObject:songVar];
            [self startUploadSongAnalysis];
        }
    }
    else //uploadWorkingAray empty
    {
        NSLog(@"save changes");
        [[VPPCoreData sharedInstance] saveAllChanges];
    }
}





#pragma mark song exporter to doc folder
- (BOOL)exportAudio:(AVAsset *)avAsset toFilePath:(NSString *)filePath
{
    CMTime assetTime = [avAsset duration];
    Float64 duration = CMTimeGetSeconds(assetTime);
    if (duration < 40.0) return NO; // if song too short return no

    // get the first audio track
    NSArray *tracks = [avAsset tracksWithMediaType:AVMediaTypeAudio];
    if ([tracks count] == 0) return NO;

    NSError *readerError = nil;
    AVAssetReader *reader = [[AVAssetReader alloc] initWithAsset:avAsset  error:&readerError];
   //AVAssetReader *reader = [AVAssetReader assetReaderWithAsset:avAsset error:&readerError]; // both works the same ?

    AVAssetReaderOutput *readerOutput = [AVAssetReaderAudioMixOutput
                                         assetReaderAudioMixOutputWithAudioTracks:avAsset.tracks
                                         audioSettings: nil];

    if (! [reader canAddOutput: readerOutput])
    {
        NSLog (@"can't add reader output...!");
        return NO;
    }
    else
    {
        [reader addOutput:readerOutput];
    }

    // writer AVFileTypeCoreAudioFormat AVFileTypeAppleM4A
    NSError *writerError = nil;
    AVAssetWriter *writer = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:filePath]
                                                      fileType:AVFileTypeAppleM4A
                                                         error:&writerError];
    //NSLog(@"writer %@",writer);
    AudioChannelLayout channelLayout;
    memset(&channelLayout, 0, sizeof(AudioChannelLayout));
    channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;

    // use different values to affect the downsampling/compression
    //    NSDictionary *outputSettings = [NSDictionary dictionaryWithObjectsAndKeys:
    //                                    [NSNumber numberWithInt: kAudioFormatMPEG4AAC], AVFormatIDKey,
    //                                    [NSNumber numberWithFloat:16000.0], AVSampleRateKey,
    //                                    [NSNumber numberWithInt:2], AVNumberOfChannelsKey,
    //                                    [NSNumber numberWithInt:128000], AVEncoderBitRateKey,
    //                                    [NSData dataWithBytes:&channelLayout length:sizeof(AudioChannelLayout)], AVChannelLayoutKey,
    //                                    nil];

    NSDictionary *outputSettings = @{AVFormatIDKey: @(kAudioFormatMPEG4AAC),
                                     AVEncoderBitRateKey: @(8000),
                                     AVNumberOfChannelsKey: @(1),
                                     AVSampleRateKey: @(8000)};

    AVAssetWriterInput *writerInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:outputSettings];


    //\Add inputs to Write
    NSParameterAssert(writerInput);
    NSAssert([writer canAddInput:writerInput], @"Cannot write to this type of audio input" );

    if ([writer canAddInput:writerInput])
    {
        [writer addInput:writerInput];
    }
    else
    {
        NSLog (@"can't add asset writer input... die!");
        return NO;
    }
    [writerInput setExpectsMediaDataInRealTime:NO];
    [writer startWriting];
    [writer startSessionAtSourceTime:kCMTimeZero];
    [reader startReading];

    __block UInt64 convertedByteCount = 0;
    __block BOOL returnValue;
    __block CMSampleBufferRef nextBuffer;

    dispatch_queue_t mediaInputQueue = dispatch_queue_create("mediaInputQueue", NULL);

    [writerInput requestMediaDataWhenReadyOnQueue:mediaInputQueue usingBlock:^{

        // NSLog(@"Asset Writer ready : %d", writerInput.readyForMoreMediaData);

        while (writerInput.readyForMoreMediaData)
        {
            nextBuffer = [readerOutput copyNextSampleBuffer];

            if (nextBuffer)
            {
                [writerInput appendSampleBuffer: nextBuffer];
                convertedByteCount += CMSampleBufferGetTotalSampleSize (nextBuffer);
                //NSNumber *convertedByteCountNumber = [NSNumber numberWithLong:convertedByteCount];
                //NSLog (@"writing");
                CFRelease(nextBuffer);
            }
            else
            {
                [writerInput markAsFinished];

                [writer finishWritingWithCompletionHandler:^{

                    if (AVAssetWriterStatusCompleted == writer.status)
                    {
                        NSLog(@"Writer completed");
                        returnValue = YES; //I NEED TO RETURN SOMETHING FROM HERE AFTER WRITING COMPLETED

                        dispatch_async(mediaInputQueue, ^{
                            dispatch_async(dispatch_get_main_queue(), ^{
                                // add this to the main queue as the last item in my serial queue
                                // when I get to this point I know everything in my queue has been run

                                NSDictionary *outputFileAttributes = [[NSFileManager defaultManager]
                                                                      attributesOfItemAtPath:filePath
                                                                      error:nil];
                                NSLog (@"done. file size is %lld",
                                       [outputFileAttributes fileSize]);
                            });
                        });
                    }
                    else if (AVAssetWriterStatusFailed == writer.status)
                    {
                        [writer cancelWriting];
                        [reader cancelReading];
                        NSLog(@"Writer failed");
                        return;
                    }
                    else
                    {
                        NSLog(@"Export Session Status: %d", writer.status);
                    }
                }];
                break;
            }
        }
    }];
    tracks = nil;
    writer = nil;
    writerInput = nil;
    reader = nil;
    readerOutput=nil;
    mediaInputQueue = nil;
      return returnValue;
    //return YES;
}
2

There are 2 answers

0
CouchDeveloper On BEST ANSWER

Your method exportAudio:toFilePath: is actually an asynchronous method and requires a few fixes to become a proper asynchronous method.

First, you should provide a completion handler in order to signal the call-site that the underlying task has been finished:

- (void)exportAudio:(AVAsset *)avAsset 
         toFilePath:(NSString *)filePath 
         completion:(completion_t)completionHandler;

Note, that the result of the method is passed through the completion handler, whose signature might be as follows:

typedef void (^completion_t)(id result);

where parameter result is the eventual result of the method. You should always return an NSError object when anything goes wrong when setting up the various objects within the method - even though, the method could return an immediate result indicating an error.

Next, if you take a look into to documentation you can read:

requestMediaDataWhenReadyOnQueue:usingBlock:

- (void)requestMediaDataWhenReadyOnQueue:(dispatch_queue_t)queue 
                              usingBlock:(void (^)(void))block

Discussion

The block should append media data to the input either until the input’s readyForMoreMediaData property becomes NO or until there is no more media data to supply (at which point it may choose to mark the input as finished using markAsFinished). The block should then exit. After the block exits, if the input has not been marked as finished, once the input has processed the media data it has received and becomes ready for more media data again, it will invoke the block again in order to obtain more.

You should now be quite sure when your task is actually finished. You determine this within the block which is passed to the method requestMediaDataWhenReadyOnQueue:usingBlock:.

When the task is finished you call the completion handler completionHandler provided in method exportAudio:toFilePath:completion:.

Of course, you need to fix your implementation, e.g. having the method ending with

    tracks = nil;
    writer = nil;
    writerInput = nil;
    reader = nil;
    readerOutput=nil;
    mediaInputQueue = nil;
      return returnValue;
    //return YES;
}

makes certainly no sense. Cleaning up and returning a result shall be done when the asynchronous task is actually finished. Unless an error occurs during setup, you need to determine this in the block passed to the method requestMediaDataWhenReadyOnQueue:usingBlock:.

In any case, in order to signal the result to the call-site call the completion handler completionHandler and pass a result object, e.g. if it succeeded the URL where it has been saved, otherwise an NSError object.

Now, since our method startUploadSongAnalysis is calling an asynchronous method, this method inevitable becomes asynchronous as well!

If I understood your original code correctly, you are invoking it recursively in order to process a number of assets. In order to implement this correctly, you need a few fixes shown below. The resulting "construct" is NOT a recursive method though, but instead an iteratively invocation of an asynchronous method ("asynchronous loop").

You may or may not provide a completion handler - same as above. It's up to you - but I would recommend it, it won't hurt to know when all assets have been processed. It may look as follows:

-(void)startUploadSongAnalysisWithCompletion:(completion_t)completionHandler
{
    [self updateProgressYForID3NForUpload:NO];

    // *** check for break condition: ***
    if ([self.uploadWorkingAray count]>=1)
    {
        ... stuff

        //export low bitrate song to cache
        [self exportAudio:[AVAsset assetWithURL:songU] 
               toFilePath:savedPath
               completion:^(id urlOrError)
         {
             if ([urlOrError isKindOfClass[NSError class]]) {

                 // Error occurred:

                 NSLog(@"Error: %@", urlOrError);
                 // There are two alternatives to proceed: 
                 // A) Ignore or remember the error and proceed with the next asset.
                 //    In this case, it would be best to have a result array 
                 //    containing all the results. Then, invoke   
                 //    startUploadSongAnalysisWithCompletion: in order to proceed 
                 //    with the next asset.
                 // 
                 // B) Stop with error.
                 //    Don't call startUploadSongAnalysisWithCompletion: but
                 //    instead invoke the completion handler passing it the error. 

                 // A:
                 // possibly dispatch to a sync queue or the main thread!
                 [self.uploadWorkingAray removeObjectAtIndex:0];
                 [self.songNotFoundArray addObject:songVar];

                 // *** next song: ***
                 [self startUploadSongAnalysisWithCompletion:completionHandler];
             }
             else {
                 // Success:
                 // *** next song: ***
                 NSURL* url = urlOrError;
                 [self startUploadSongAnalysisWithCompletion:completionHandler];
             }
         }];
    }
    else //uploadWorkingAray empty
    {
        NSLog(@"save changes");
        [[VPPCoreData sharedInstance] saveAllChanges];

        // *** signal completion ***
        if (completionHandler) {
            completionHandler(@"OK");
        }
    }
}
0
Shad On

I am not sure, but can not you send a call to a method like following

dispatch_async(mediaInputQueue, ^{
                        dispatch_async(dispatch_get_main_queue(), ^{
                            // add this to the main queue as the last item in my serial queue
                            // when I get to this point I know everything in my queue has been run

                            NSDictionary *outputFileAttributes = [[NSFileManager defaultManager]
                                                                  attributesOfItemAtPath:filePath
                                                                  error:nil];
                            NSLog (@"done. file size is %lld",
                                   [outputFileAttributes fileSize]);

                            //calling the following method after completing the queue
                            [self printMe];

                        });
                    });

-(void)printMe{
NSLog(@"queue complete...");

//Do the next job, may be the following task !!!

if ([self exportAudio:[AVAsset assetWithURL:songU] toFilePath:savedPath]) // HERE IS THE PROBLEM, this return true even the writing is not completed cos when i upload to my web server, it will say song file corrupted
    {
       // [self performSelector:@selector(sendSongForUpload:) withObject:subStringPath afterDelay:1];
        [self sendRequest:2 andPath:subStringPath andSongDBItem:songVar];
    }
    else
    {
        NSLog(@"song too short, skipped");
        [self.uploadWorkingAray removeObjectAtIndex:0];
        [self.songNotFoundArray addObject:songVar];
        [self startUploadSongAnalysis];
    }
}