Skip to content

Commit

Permalink
feat(Android): Adjust settings
Browse files Browse the repository at this point in the history
closes #298, closes #307, closes #271
  • Loading branch information
llfbandit committed May 21, 2024
2 parents fc2f599 + e6b0798 commit 9093032
Show file tree
Hide file tree
Showing 18 changed files with 568 additions and 175 deletions.
38 changes: 19 additions & 19 deletions record/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Audio recorder from microphone to a given file path or stream.

No external dependencies:

- On Android, AudioRecord and MediaCodec.
- On Android, AudioRecord and MediaCodec or MediaRecorder.
- On iOS and macOS, AVFoundation.
- On Windows, MediaFoundation.
- On web, well... your browser! (and its underlying platform).
Expand All @@ -17,40 +17,39 @@ External dependencies:
| amplitude(dBFS) | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| permission check | ✔️ | ✔️ | ✔️ | | ✔️ |
| num of channels | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️
| device selection | ✔️ * | (auto BT/mic) | ✔️ | ✔️ | ✔️ | ✔️
| auto gain | ✔️ |(always active?)| ✔️ | | |
| echo cancel | ✔️ | | ✔️ | | |
| noise suppresion | ✔️ | | ✔️ | | |

* min SDK: 23. Bluetooth telephony device link (SCO) is automatically done but there's no phone call management.
| device selection | ✔️ 1 / 2 | (auto BT/mic) | ✔️ | ✔️ | ✔️ | ✔️
| auto gain | ✔️ 2 |(always active?)| ✔️ | | |
| echo cancel | ✔️ 2 | | ✔️ | | |
| noise suppresion | ✔️ 2 | | ✔️ | | |

## File
| Encoder | Android | iOS | web | Windows | macOS | linux
|-----------------|----------------|---------|---------|---------|---------|---------
| aacLc | ✔️ | ✔️ | ? | ✔️ | ✔️ | ✔️
| aacLc | ✔️ | ✔️ | ? | ✔️ | ✔️ | ✔️
| aacEld | ✔️ | ✔️ | ? | | ✔️ |
| aacHe | ✔️ | | ? | | | ✔️
| amrNb | ✔️ | | ? | ✔️ | |
| amrWb | ✔️ | | ? | | |
| opus | ✔️ | | ✔️ | | | ✔️
| wav | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️
| flac | ✔️ | ✔️ | ? | ✔️ | ✔️ | ✔️
| pcm16bits | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| opus | ✔️ | | ✔️ 3 | | | ✔️
| wav | ✔️ 2 | ✔️ | ✔️ | ✔️ | ✔️ | ✔️
| flac | ✔️ 2 | ✔️ | ? | ✔️ | ✔️ | ✔️
| pcm16bits | ✔️ 2 | ✔️ | ✔️ | ✔️ | ✔️ |

\* Question marks (?) in web column mean that the formats are supported by the plugin
* Question marks (?) in web column mean that the formats are supported by the plugin
but are not available in current (and tested) browsers (Chrome / Firefox).

## Stream
| Encoder | Android | iOS | web | Windows | macOS | linux
|-----------------|------------|---------|---------|---------|---------|---------
| aacLc * | ✔️ | | | | |
| aacEld * | ✔️ | | | | |
| aacHe * | ✔️ | | | | |
| pcm16bits | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| aacLc * | ✔️ 2 | | | | |
| aacEld * | ✔️ 2 | | | | |
| aacHe * | ✔️ 2 | | | | |
| pcm16bits | ✔️ 2 | ✔️ | ✔️ | ✔️ | ✔️ |

\* AAC is streamed with raw AAC with ADTS headers, so it's directly readable through a file!

__All audio output is with 16bits depth.__
1. min SDK: 23. Bluetooth telephony device link (SCO) is automatically done but there's no phone call management.
2. Unsupported on legacy Android recorder.
3. Sample rate output is determined by your settings in OS. Bit depth is 32 bits.

## Usage

Expand Down Expand Up @@ -86,6 +85,7 @@ record.dispose(); // As always, don't forget this one.
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
```
- min SDK: 21 (amrNb/amrWb: 26, Opus: 29)
- min SDK: 19 with legacy recorder.

* [Audio formats sample rate hints](https://developer.android.com/guide/topics/media/media-formats#audio-formats)

Expand Down
6 changes: 3 additions & 3 deletions record/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: record
description: Audio recorder from microphone to file or stream with multiple codecs, bit rate and sampling rate options.
version: 5.0.5
version: 5.1.0
homepage: https://github.com/llfbandit/record/tree/master/record

environment:
Expand All @@ -14,11 +14,11 @@ dependencies:
# https://pub.dev/packages/uuid
uuid: ">=3.0.7 <5.0.0"

record_platform_interface: ^1.0.2
record_platform_interface: ^1.1.0
record_web: ^1.0.4
record_windows: ^1.0.2
record_linux: '>=0.5.0 <1.0.0'
record_android: ^1.1.0
record_android: ^1.2.0
record_darwin: ^1.0.1

dev_dependencies:
Expand Down
5 changes: 5 additions & 0 deletions record_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 1.2.0
* feat: Re-introduced native MediaRecorder. Set `RecordConfig.androidConfig.useLegacy` to `true`. This comes with limitations compared to advanced recorder.
* feat: Advanced AudioRecorder will try to adjust given configuration if unsupported or out of range (sample rate, bitrate and channel count).
Those two above should help for older devices or bad vendor implementations.

## 1.1.0
* fix: Properly close container when recording is stopped.
* fix: num channels & sample rate are not applied in AAC format.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import androidx.annotation.Nullable;

import java.io.File;

public class Utils {
private Utils() {}
private Utils() {
}

public static <T> T firstNonNull(@Nullable T first, @Nullable T second) {
return first != null ? first : checkNotNull(second);
Expand All @@ -15,4 +18,15 @@ public static <T> T checkNotNull(T reference) {
}
return reference;
}

public static void deleteFile(String path) {
if (path != null) {
File file = new File(path);

if (file.exists()) {
//noinspection ResultOfMethodCallIgnored
file.delete();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ class MethodCallHandlerImpl(
}

private fun createRecorder(recorderId: String) {
val recorder = RecorderWrapper(recorderId, messenger)
val recorder = RecorderWrapper(appContext, recorderId, messenger)
recorder.setActivity(activity)
recorders[recorderId] = recorder

Expand All @@ -136,22 +136,23 @@ class MethodCallHandlerImpl(
}

private fun getRecordConfig(call: MethodCall): RecordConfig {
val device = if (Build.VERSION.SDK_INT >= 23) {
DeviceUtils.deviceInfoFromMap(appContext, call.argument("device"))
} else {
null
}
val androidConfig = call.argument("androidConfig") as Map<*, *>?

return RecordConfig(
call.argument("path"),
Utils.firstNonNull(call.argument("encoder"), "aacLc"),
Utils.firstNonNull(call.argument("bitRate"), 128000),
Utils.firstNonNull(call.argument("sampleRate"), 44100),
Utils.firstNonNull(call.argument("numChannels"), 2),
device,
if (Build.VERSION.SDK_INT >= 23) {
DeviceUtils.deviceInfoFromMap(appContext, call.argument("device"))
} else {
null
},
Utils.firstNonNull(call.argument("autoGain"), false),
Utils.firstNonNull(call.argument("echoCancel"), false),
Utils.firstNonNull(call.argument("noiseSuppress"), false)
Utils.firstNonNull(call.argument("noiseSuppress"), false),
Utils.firstNonNull(androidConfig?.get("useLegacy") as Boolean?, false),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
package com.llfbandit.record.methodcall

import android.app.Activity
import com.llfbandit.record.record.AudioRecorder
import android.content.Context
import com.llfbandit.record.record.recorder.AudioRecorder
import com.llfbandit.record.record.RecordConfig
import com.llfbandit.record.record.bluetooth.BluetoothScoListener
import com.llfbandit.record.record.recorder.IRecorder
import com.llfbandit.record.record.recorder.MediaRecorder
import com.llfbandit.record.record.stream.RecorderRecordStreamHandler
import com.llfbandit.record.record.stream.RecorderStateStreamHandler
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel

internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger): BluetoothScoListener {
internal class RecorderWrapper(
private val context: Context,
recorderId: String,
messenger: BinaryMessenger
): BluetoothScoListener {
companion object {
const val EVENTS_STATE_CHANNEL = "com.llfbandit.record/events/"
const val EVENTS_RECORD_CHANNEL = "com.llfbandit.record/eventsRecord/"
Expand All @@ -20,7 +27,7 @@ internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger):
private val recorderStateStreamHandler = RecorderStateStreamHandler()
private var eventRecordChannel: EventChannel?
private val recorderRecordStreamHandler = RecorderRecordStreamHandler()
private var recorder: AudioRecorder? = null
private var recorder: IRecorder? = null

init {
eventChannel = EventChannel(messenger, EVENTS_STATE_CHANNEL + recorderId)
Expand All @@ -39,6 +46,9 @@ internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger):
}

fun startRecordingToStream(config: RecordConfig, result: MethodChannel.Result) {
if (config.useLegacy) {
throw Exception("Unsupported feature from legacy recorder.")
}
startRecording(config, result)
}

Expand Down Expand Up @@ -119,7 +129,7 @@ internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger):
private fun startRecording(config: RecordConfig, result: MethodChannel.Result) {
try {
if (recorder == null) {
recorder = createRecorder()
recorder = createRecorder(config)
start(config, result)
} else if (recorder!!.isRecording) {
recorder!!.stop(fun(_) = start(config, result))
Expand All @@ -131,7 +141,11 @@ internal class RecorderWrapper(recorderId: String, messenger: BinaryMessenger):
}
}

private fun createRecorder(): AudioRecorder {
private fun createRecorder(config: RecordConfig): IRecorder {
if (config.useLegacy) {
return MediaRecorder(context, recorderStateStreamHandler)
}

return AudioRecorder(
recorderStateStreamHandler,
recorderRecordStreamHandler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,10 @@ class RecordConfig(
val device: AudioDeviceInfo?,
val autoGain: Boolean = false,
val echoCancel: Boolean = false,
val noiseSuppress: Boolean = false
val noiseSuppress: Boolean = false,
val useLegacy: Boolean = false,
) {
val numChannels: Int

init {
this.numChannels = 2.coerceAtMost(1.coerceAtLeast(numChannels))
}
val numChannels: Int = 2.coerceAtMost(1.coerceAtLeast(numChannels))
}

class AudioEncoder {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
package com.llfbandit.record.record.encoder

import android.annotation.TargetApi
import android.media.MediaCodec
import android.media.MediaCodec.CodecException
import android.media.MediaCodecList
import android.media.MediaFormat
import android.os.Build
import com.llfbandit.record.record.container.IContainerWriter

class MediaCodecEncoder(
encoder: String,
mediaFormat: MediaFormat,
private val listener: EncoderListener,
private val container: IContainerWriter,
) : IEncoder, MediaCodec.Callback() {

private val codec = createCodec(mediaFormat)
private val codec = createCodec(encoder, mediaFormat)
private var trackIndex = -1
private var recordStopped = false
private var recordPaused = false
Expand All @@ -39,36 +37,7 @@ class MediaCodecEncoder(

override fun release() {}

@TargetApi(Build.VERSION_CODES.Q)
private fun findCodecForFormat(format: MediaFormat): String? {
val mime = format.getString(MediaFormat.KEY_MIME)
val codecs = MediaCodecList(MediaCodecList.REGULAR_CODECS)

for (info in codecs.codecInfos.sortedBy { !it.canonicalName.startsWith("c2.android") }) {
if (!info.isEncoder) {
continue
}
try {
val caps = info.getCapabilitiesForType(mime)
if (caps != null && caps.isFormatSupported(format)) {
return info.canonicalName
}
} catch (e: IllegalArgumentException) {
// type is not supported
}
}
return null
}

private fun createCodec(mediaFormat: MediaFormat): MediaCodec {
val encoder = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
MediaCodecList(MediaCodecList.REGULAR_CODECS).findEncoderForFormat(mediaFormat)
} else {
findCodecForFormat(mediaFormat)
}

encoder ?: throw Exception("No encoder found for $mediaFormat")

private fun createCodec(encoder: String, mediaFormat: MediaFormat): MediaCodec {
var mediaCodec: MediaCodec? = null
try {
mediaCodec = MediaCodec.createByCodecName(encoder)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,6 @@ import com.llfbandit.record.record.container.IContainerWriter
import com.llfbandit.record.record.container.MuxerContainer

class AacFormat : Format() {
private val sampleRates = intArrayOf(
96000,
88200,
64000,
48000,
44100,
32000,
24000,
22050,
16000,
12000,
11025,
8000
)

override val mimeTypeAudio: String = MediaFormat.MIMETYPE_AUDIO_AAC
override val passthrough: Boolean = false

Expand All @@ -35,7 +20,7 @@ class AacFormat : Format() {
override fun getMediaFormat(config: RecordConfig): MediaFormat {
val format = MediaFormat().apply {
setString(MediaFormat.KEY_MIME, mimeTypeAudio)
setInteger(MediaFormat.KEY_SAMPLE_RATE, nearestValue(sampleRates, config.sampleRate))
setInteger(MediaFormat.KEY_SAMPLE_RATE, config.sampleRate)
setInteger(MediaFormat.KEY_CHANNEL_COUNT, config.numChannels)
setInteger(MediaFormat.KEY_BIT_RATE, config.bitRate)

Expand Down Expand Up @@ -65,6 +50,16 @@ class AacFormat : Format() {
return format
}

override fun adjustSampleRate(format: MediaFormat, sampleRate: Int) {
super.adjustSampleRate(format, sampleRate)
this.sampleRate = sampleRate
}

override fun adjustNumChannels(format: MediaFormat, numChannels: Int) {
super.adjustNumChannels(format, numChannels)
this.numChannels = numChannels
}

override fun getContainer(path: String?): IContainerWriter {
if (path == null) {
return AdtsContainer(sampleRate, numChannels, aacProfile)
Expand Down
Loading

0 comments on commit 9093032

Please sign in to comment.