13

I have a setup as follows, testing on an iPhone 5s with iOS 10.3, both in and out of debug.

  • An AVAudioRecorder fires record(forDuration: 5.0)
  • A CADisplayLink watches the levels of the recorder (the recorder is meteringEnabled), synchronizing an animation, and keeping track of recorder.currentTime (but the problem can be reproduced by minimally keeping track of time in the display link)
  • The reports of recorder.currentTime consistently reach values > 5 (often 5.2 to 5.5). The results are essentially consistent with the values of recorder.deviceTime and CFAbsoluteTimeGetCurrent
  • I initialize an AVAudioPlayer and verify that the sound asset does have a duration of precisely 5.0 seconds.

From my understanding, the recorder's currentTime is measured in seconds, and resets to 0.0 when recorder.isRecording reverts to false, which happens immediately when the recorder stops (this is consistent with what i see in audioRecorderDidFinishRecording)... so observing with a CADisplayLink should produce currentTime values strictly less than 5.0?

The question is: what could be going wrong here? The recorder is stopping as it should after exactly 5 seconds, but internally it thinks it recorded for more than 5 seconds? I'm listening to all my recordings and they sound fine.

I'm not sure if it's relevant, but my AVAudioSession is of type AVAudioSessionCategoryPlayAndRecord, and my audio settings are as follows (all of these excepting sample rate are necessary for later analysis):

audioSettings = [
    AVFormatIDKey: Int(kAudioFormatLinearPCM),
    AVSampleRateKey: 44100,
    AVNumberOfChannelsKey: 2,
    AVLinearPCMIsBigEndianKey: 0,
    AVLinearPCMIsFloatKey: 0,
    AVLinearPCMBitDepthKey: 16,
    AVLinearPCMIsNonInterleaved: 0
]

I already tried fiddling with all of these and didn't see any change behavior.

The CADisplayLink is added from the main thread via

recorderDisplayLink?.add(to: RunLoop.current, forMode: RunLoopMode.commonModes)

The only similar question I can find on stack exchange is this, but the question is underspecified and the answer unhelpful (for me at least).
At first I thought maybe the issue is with overloading the main queue, so that somehow the recorder's sense of time becomes bloated (which I think would still constitute bad behavior), but after disabling the animation (and also experimenting with a Timer instead of a CADisplayLink), the problem persists!!! Perhaps it is still a threading problem, but I don't see why that would be the case. And if i do need to multi-thread, I could use some help both with understanding and implementing :) Any ideas appreciated.

Community
  • 1
  • 1
Badam Baplan
  • 270
  • 2
  • 12
  • So is the question why are you getting between 0.2 and 0.5 extra seconds than the duration requested with `recordForDuration:`? – Rhythmic Fistman Apr 13 '17 at 20:38
  • The question is, how are the following two statements simultaneously compatible: (1) record(forDuration: 5.0) produces a valid recording of exactly 5.0 seconds (2) A timer observing the currentTime property of the recorder in action reports values significantly > 5.0 – Badam Baplan Apr 13 '17 at 21:27
  • TL;DR `AVAudioRecorder` isn't very good, replace it. see my answer for a more nuanced version of this. – Rhythmic Fistman Apr 13 '17 at 21:48
  • You cannot have an iphone 4 with iOS 10.3, the last supported version on this device is iOS 7.1.2. Even if you've meant 4s that would still set the max version to iOS 9.3.5 – Kamil.S Apr 17 '17 at 14:50
  • Haha thank you! You're right. Corrected question. – Badam Baplan Apr 17 '17 at 15:31

2 Answers2

4

Of all the times I've used AVAudioRecorder, I've eventually had to replace it. AVAudioRecorder is a general purpose recording class, so once your requirements get a little specialised, it will disappoint you.

However, it does do metering, which is what attracts many people to it. So maybe the situation can be salvaged.

Possibilities:

a. if currentTime isn't trustworthy, then don't observe it! You've got your 5 second files, so maybe find some other way to mark that passage of time in your app.

b. of the the property currentTime, the header file says:

  only valid while recording

Are you sampling currentTime only while recording? If so that could be the problem. In which case you could use the deviceCurrentTime property, which is always valid, although you will have to subtract off the initial deviceCurrentTime.

If the situation can't be salvaged, you could pretty quickly replace AVAudioRecorder with AVAudioEngine and AVAudioFile, but that's a question for another day.

Rhythmic Fistman
  • 34,352
  • 5
  • 87
  • 159
  • Yea I was thinking about replacing it, but would like not to if possible... and it is the metering that attracted me. Basically I am watching the recorder's average power both to feed an animation and gauge silence. It's the second part for which I need the `currentTime` property. But I need these times to be consistent with `AVAudioPlayer` and `AVReader`'s perception of time. – Badam Baplan Apr 13 '17 at 23:43
  • Oh and also I should clarify: I'm only reporting the values of `currentTime` when `isRecording` is true. That means either that the recorder is still recording when `currentTime` exceeds the actual audio duration, or that `isRecording` does not actually indicate what the recorder is doing. But perhaps the header says it all. "`Only valid while recording (but don't bet on it)`" – Badam Baplan Apr 13 '17 at 23:49
  • I did try to use the `deviceCurrentTime` property of the recorder, but found it to behave just as strangely. Maybe I should use a different measure of device time, outside of the recorder? – Badam Baplan Apr 14 '17 at 00:11
  • Yes, some other way of marking time that isn't `AVAudioRecorder`. – Rhythmic Fistman Apr 14 '17 at 00:45
  • Actually I tried marking time with `CFAbsoluteTimeGetCurrent` and I'm seeing the same problem. The amount of system time that the `isRecording` flag reads true is really longer than the `duration` – Badam Baplan Apr 14 '17 at 15:43
  • With `AVAudioEngine` you can monitor time elapsed by counting samples recorded, I don't think you'll get more accurate than that. Level metering isn't that hard, just sum up the squares and fudge with a scaled log, you'll be fine. – Rhythmic Fistman Apr 14 '17 at 23:09
0

I ran into the same problem for a UILabel that I was updating on a Timer but was able to workaround it by checking if the gap between the timer triggers was too big and remembering the delta as a currentRecordTimeOffset.

     if ( self.recorder.recording ) {
         float adjustedRecordTime = self.recorder.currentTime;
         if(self.currentRecordTimeOffset == 0){
             if(self.recorder.currentTime - self.lastRecordedTime > 1.0){    // current time should be updated every 0.1 so a 1 Sec delta indicates offset needed
                 self.currentRecordTimeOffset = self.recorder.currentTime;
                 adjustedRecordTime = self.recorder.currentTime - self.currentRecordTimeOffset;
             }
         }
         else{
             adjustedRecordTime = self.recorder.currentTime - self.currentRecordTimeOffset;     // get rid of the unusual offset
         }
         self.lastRecordedTime = self.recorder.currentTime;
         self.durationLabel.text = [NSString stringWithFormat:@"%.2f", adjustedRecordTime];