You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
There is a considerable performance discrepancy between iOS and Android when using the getAmplitude() method.
To Reproduce
Run the minimal example app on iOS and Android
Start recording
See the discrepancy in sample rate
Here is a minimal example app:
Example App
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'flutter_record Example',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(title: 'flutter_record Example'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
/// Audio Recorder
final _recorder = AudioRecorder();
/// File path to store the recorded audio
String? _filePath;
/// Whether the audio is currently being recorded
bool _isRecording = false;
/// The list to store the volume data recorded throughout the exercise.
final List<String> _volumeData = [];
/// Timer to update the volume level
Timer? _updateVolumeTimer;
/// Stopwatch
Stopwatch? _stopwatch;
/// Time of the stopwatch
String get stopwatchTime {
if (_stopwatch == null || !_stopwatch!.isRunning) {
return "00:00";
} else {
final seconds = _stopwatch!.elapsed.inSeconds.toString().padLeft(2, '0');
final milliseconds = (_stopwatch!.elapsed.inMilliseconds % 1000 ~/ 10)
.toString()
.padLeft(2, '0');
return "$seconds:$milliseconds";
}
}
/// Start recording audio
Future<void> startRecording() async {
final bool hasPermission = await _recorder.hasPermission();
if (!hasPermission) return;
final directory = await getApplicationDocumentsDirectory();
/// Generate a file name
final String fileName =
'recording_${DateTime.now().millisecondsSinceEpoch}.m4a';
_filePath = '${directory.path}/$fileName';
/// Start recording
await _recorder.start(const RecordConfig(), path: _filePath!);
_isRecording = true;
setState(() {});
/// Start the stopwatch
_stopwatch = Stopwatch();
_stopwatch?.start();
/// Take a snapshot of the volume every 10 milliseconds
_updateVolumeTimer = Timer.periodic(
const Duration(milliseconds: 10),
(timer) {
updateVolume();
},
);
/// Record for 3 seconds
await Future.delayed(const Duration(seconds: 3));
/// Stop recording volume data
_stopwatch?.stop();
_updateVolumeTimer?.cancel();
/// Stop recording
await stopRecording();
print('RECORDING = ${_recorder.isRecording}');
}
/// Stop recording
Future<void> stopRecording() async {
final path = await _recorder.stop();
final audioFile = File(path!);
_isRecording = false;
print('RECORDED AUDIO AT: $audioFile');
setState(() {});
}
/// The volume level, at current
double _volume = 0.0;
/// Update the volume level
Future<void> updateVolume() async {
/// Start time measurement
final startTime = DateTime.now();
/// Get the current amplitude
final Amplitude ampl = await _recorder.getAmplitude();
/// End time measurement
final endTime = DateTime.now();
/// Calculate the duration
final duration = endTime.difference(startTime).inMilliseconds;
/// Add the amplitude data to the list
_volume = ampl.current;
_volumeData.add('${duration}ms ($_volume)');
print('GET AMPLITUDE TIME: ${duration}ms ($_volume)');
setState(() {});
}
/// AudioPlayer to playback the recording
final _audioPlayer = AudioPlayer();
/// Play the recorded audio
void playRecording() {
_audioPlayer.setFilePath(_filePath!);
_audioPlayer.play();
print('PLAYING AUDIO');
setState(() {});
}
/// The current platform
final String platform = Platform.isAndroid ? 'ANDROID' : 'iOS';
/// The number of unique items in the volume data
int countUniqueItems() {
Set<String> uniqueItems = _volumeData.toSet();
return uniqueItems.length;
}
/// How many samples were recorded out of the target of 300 (a sample every 10ms for 3 seconds)
double calculateSampleCompleteness() {
return (_volumeData.length / 300) * 100;
}
/// How diverse the samples are
double calculateSampleDiversity() {
return (countUniqueItems() / _volumeData.length) * 100;
}
/// Calculate the sample rate
double calculateEffectiveSampleRate() {
if (_volumeData.isEmpty) return 0;
final totalDuration = _stopwatch?.elapsed.inMilliseconds ?? 3000;
return _volumeData.length / (totalDuration / 1000);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Center(
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 32),
children: <Widget>[
const SizedBox(height: 40),
/// Start recording button
ElevatedButton(
onPressed: () {
_isRecording ? null : startRecording();
},
child: Text(_isRecording ? 'Recording' : 'Start Recording')),
const SizedBox(height: 24),
if (_isRecording)
Text(
textAlign: TextAlign.center,
'$stopwatchTime s',
),
/// Playback recorded audio button
if (_filePath != null)
ElevatedButton(
onPressed: () {
playRecording();
},
child: const Text('Play Recorded Audio')),
const SizedBox(height: 40),
/// Display the volume data
if (_volumeData.isNotEmpty) ...[
Text('PLATFORM: $platform'),
const SizedBox(height: 16),
Text(
'Recording length: ${_stopwatch?.elapsed.inMilliseconds} ms'),
const SizedBox(height: 16),
Text('Sample Count: ${_volumeData.length} (Target: 300)'),
const SizedBox(height: 16),
Text(
'Sample Rate Score: ${calculateSampleCompleteness().toStringAsFixed(2)}%'),
const SizedBox(height: 16),
Text(
'Sample Diversity: ${calculateSampleDiversity().toStringAsFixed(2)}% - (${countUniqueItems()} unique samples)'),
const SizedBox(height: 16),
Text(
'Sample Rate: ${calculateEffectiveSampleRate().toStringAsFixed(2)} Hz (Target: 100 Hz)'),
const SizedBox(height: 16),
Text('$platform VOLUME DATA: ${_volumeData.join('\n')}'),
]
])));
}
}
Here are screenshots which highlight the problem:
Note the recording length is in milliseconds, excuse the typo in the screenshots.
As you can see, when calling getAmplitude() every 10 milliseconds across a 3 second recording, iOS collects close to 300 samples with effectively no delay.
Android only collects in the range of 140-160 samples, often with significant delays between collecting samples whilst also getting lots of duplicate readings, too.
You can also see that whilst iOS starts getting volume data immediately, Android can take between 250-500ms before it starts actually picking up accurate data.
I didn’t notice this issue until updating my build.gradle from minSdkVersion: 23 to 24.
The text was updated successfully, but these errors were encountered:
Same issue here. getAmplitude() on Android returns the same exact value for upwards of 500ms, whereas every sample on iOS (down to every frame at 60fps) returns a slightly different value (as expected).
FYI, you should use onAmplitudeChanged stream to achieve the same.
Also, onStateChanged stream is the way to go to ensure that the recording is really started.
I made small improvements in this area but it seems it is just slower to reach native side on Android.
Also, this does not solve "duplicated" values.
This is because on iOS we must call explicitly an SDK method to get the values.
On Android and other platforms, this is done each time PCM reader reads audio from hardware and I don't implement some kind of cumulated values to compare with previous call.
With my device and the given setup on top of example project on debug mode:
10ms interval => 280 with 100+ unique values. 10- values at -160.0 with onAmplitudeChanged stream.
20ms interval => 140+ with 95+ unique values. 5- values at -160.0 with onAmplitudeChanged stream.
I will consider this ok as 10ms is a very low interval.
Package version
record: ^5.1.2
Environment
Describe the bug
There is a considerable performance discrepancy between iOS and Android when using the
getAmplitude()
method.To Reproduce
Here is a minimal example app:
Example App
Here are screenshots which highlight the problem:
Note the recording length is in milliseconds, excuse the typo in the screenshots.
As you can see, when calling
getAmplitude()
every 10 milliseconds across a 3 second recording, iOS collects close to 300 samples with effectively no delay.Android only collects in the range of 140-160 samples, often with significant delays between collecting samples whilst also getting lots of duplicate readings, too.
You can also see that whilst iOS starts getting volume data immediately, Android can take between 250-500ms before it starts actually picking up accurate data.
I didn’t notice this issue until updating my
build.gradle
fromminSdkVersion: 23
to24
.The text was updated successfully, but these errors were encountered: