skiptools / skiptools/skip-av

AVAudioRecorder on Android diverges from AVFoundation (currentTime sign, metering, record/resume)

Open
#28 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Swift
Stars
14
Forks
12
PR merge metrics
No merged PRs in 30d

Description

While evaluating Skip for an app that records audio, I went through Sources/SkipAV/AVAudioRecorder.swift against the AVFoundation headers and found six places where the Android implementation does not behave the way the iOS API it stands in for does. They are all in the #elseif SKIP branch, so iOS is unaffected.

Quoting AVFAudio.framework/Headers/AVAudioRecorder.h for reference:

- (BOOL)record;    // Start or resume recording to file.
@property(readonly) NSTimeInterval currentTime;
@property(getter=isMeteringEnabled) BOOL meteringEnabled;
- (void)updateMeters;
- (float)peakPowerForChannel:(NSUInteger)channelNumber;
- (float)averagePowerForChannel:(NSUInteger)channelNumber;
1. currentTime returns a negative value
public var currentTime: TimeInterval {
    if let startTime = recordingStartTime {
        return startTime.timeIntervalSinceNow   // negative once recording has started
    }

timeIntervalSinceNow measures from the start date to now, so it is negative for a date in the past. AVFoundation reports a positive elapsed duration. Any elapsed-time label bound to this shows a countdown into negative numbers on Android.

Related: the elapsed time also restarts from zero after pause(), since nothing accumulates the earlier segments.

2. averagePower(forChannel:) returns Double, not Float

The header declares float for both power accessors. Cross-platform code written against the iOS signature (let level: Float = recorder.averagePower(forChannel: 0)) compiles on iOS and then fails to compile once transpiled to Kotlin.

3. Power is reported as a 0...1 linear ratio instead of decibels
return Float(recorder?.maxAmplitude ?? 0) / Float(32767.0)

AVFoundation returns decibels relative to full scale, -160.0 for silence through 0.0 at full scale. A level meter calibrated for the iOS range renders incorrectly on Android — the values are always positive and compress the quiet end of the range.

4. isMeteringEnabled and updateMeters() are missing, so metering cannot be used at all
@available(*, unavailable)
public var meteringEnabled = false

Two problems: the property uses the Objective-C name rather than the name Swift imports (isMeteringEnabled), and it is marked unavailable. There is no updateMeters(). The standard iOS sequence therefore does not compile on Android:

recorder.isMeteringEnabled = true
recorder.updateMeters()
let level = recorder.averagePower(forChannel: 0)

There is a second reason updateMeters() matters here: MediaRecorder.getMaxAmplitude() returns the maximum amplitude sampled since it was last called and resets on every read. Since both accessors read it directly today, calling peak and average in the same frame makes whichever runs second observe silence.

5. record() restarts the recording instead of resuming, and returns Void
public func record() {
    do {
        prepareToRecord()      // builds a new MediaRecorder over the same output file
        recorder?.start()

prepareToRecord() runs on every call, which constructs a fresh MediaRecorder and re-opens the output file. pause() followed by record() therefore discards everything captured before the pause, where the header says record should "start or resume". MediaRecorder.resume() is the counterpart to the pause() already in use.

record() also returns Void rather than BOOL, so there is no way to detect a failed start.

6. AVNumberOfChannelsKey is ignored, and numeric settings only match Int
setAudioChannels(2)                                                   // hardcoded
setAudioSamplingRate(settings["AVSampleRateKey"] as? Int ?? 44100)

Mono cannot be requested. The Showcase app's AudioPlayground already passes AVNumberOfChannelsKey: 1 and records in stereo on Android.

Separately, AVFoundation documents these settings as NSNumber values and iOS code commonly writes AVSampleRateKey: 44100.0. Matching only against Int makes a Double fall through to the default silently.


Related observations, not part of this report
  • MediaRecorder(Context) requires API 31, but the CI workflow runs the emulator at API 28.
  • AVAudioRecorderDelegate's methods are required here; on iOS they are @optional.
  • AVFormatIDKey is accepted but ignored. MediaRecorder has no linear PCM output, so only AAC/MPEG-4 is reachable — worth documenting rather than implementing.
  • record(atTime:) and record(forDuration:) are not implemented.
  • AudioFormat, MediaPlayer, and FileOutputStream are imported but unused.

I have a branch that addresses items 1–6 with a test for the metering conversion and a README update, and will open it as a draft PR shortly. Happy to split it up or drop pieces if you'd prefer a smaller change — items 3 and 5 do change behavior for existing Android users, so I'd understand wanting those handled separately.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Read Sources/SkipAV/AVAudioRecorder.swift, focusing on the #elseif SKIP branch, then inspect the metering-conversion test and README update on the reported branch. Done means Android behavior matches the cited AVFoundation signatures and semantics for items 1–6, with the metering test and documentation update completed.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
audio-video-rtc, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.