-
Notifications
You must be signed in to change notification settings - Fork 258
Description
Summary
Currently, the record
package supports streaming microphone audio data (PCM16) on platforms like Android, iOS, macOS, and Windows. However, Linux support for microphone data streaming is not available yet.
I would like to propose adding Linux support for streaming mic data in PCM16 format, and I have already developed a working native solution using GStreamer.
Please review the proposed approach and suggest if a better or more idiomatic implementation would be preferable. I am happy to contribute this feature and iterate based on feedback from the maintainers or the community.
Proposed Native Implementation (GStreamer-based)
The implementation uses a GStreamer pipeline with pulsesrc
and appsink
to capture audio input and stream it in 16-bit PCM format. It hooks into the Flutter method channel to send audio chunks to Dart.
Native Code (Linux)
// Called when new audio sample is available
static void on_new_sample_from_sink(GstElement* sink, gpointer user_data) {
MyApplication* self = static_cast<MyApplication*>(user_data);
GstSample* sample = nullptr;
g_signal_emit_by_name(sink, "pull-sample", &sample);
if (sample) {
GstBuffer* buffer = gst_sample_get_buffer(sample);
GstMapInfo map;
if (gst_buffer_map(buffer, &map, GST_MAP_READ)) {
if (self->channel != nullptr) {
FlValue* args = fl_value_new_uint8_list(map.data, map.size);
fl_method_channel_invoke_method(self->channel, "onMicData", args, nullptr, nullptr, nullptr);
fl_value_unref(args);
}
gst_buffer_unmap(buffer, &map);
}
gst_sample_unref(sample);
}
}
static void start_microphone_streaming(MyApplication* self) {
if (self->is_recording) return;
gst_init(nullptr, nullptr);
const gchar* pipeline_description =
"pulsesrc ! "
"volume volume=3.0 ! "
"audio/x-raw,format=S16LE,channels=1,rate=44100 ! "
"appsink name=appsink emit-signals=true";
self->pipeline = gst_parse_launch(pipeline_description, nullptr);
self->appsink = gst_bin_get_by_name(GST_BIN(self->pipeline), "appsink");
g_signal_connect(self->appsink, "new-sample", G_CALLBACK(on_new_sample_from_sink), self);
gst_element_set_state(self->pipeline, GST_STATE_PLAYING);
self->is_recording = TRUE;
}
static void stop_microphone_streaming(MyApplication* self) {
if (!self->is_recording) return;
gst_element_set_state(self->pipeline, GST_STATE_NULL);
if (self->appsink != nullptr) {
gst_object_unref(self->appsink);
self->appsink = nullptr;
}
if (self->pipeline != nullptr) {
gst_object_unref(self->pipeline);
self->pipeline = nullptr;
}
self->is_recording = FALSE;
}
Method Channel Integration
if (strcmp(fl_method_call_get_name(method_call), "startRecording") == 0) {
start_microphone_streaming(static_cast<MyApplication*>(user_data));
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
} else if (strcmp(fl_method_call_get_name(method_call), "stopRecording") == 0) {
stop_microphone_streaming(static_cast<MyApplication*>(user_data));
response = FL_METHOD_RESPONSE(fl_method_success_response_new(nullptr));
}
Dart Side Example
platform.setMethodCallHandler((call) async {
if (call.method == 'onMicData') {
final Uint8List chunk = call.arguments;
audioBytes.addAll(chunk); // Handle PCM16 bytes
}
});
Wave file conversion
Future<void> _saveAsWavAndPlay() async {
final wavBytes = _convertToWav(Uint8List.fromList(audioBytes));
final dir = await getTemporaryDirectory();
final file = File('${dir.path}/recorded_audio.wav');
await file.writeAsBytes(wavBytes);
final audio = AudioPlayer();
audio.play(DeviceFileSource(file.path));
}
Uint8List _convertToWav(
Uint8List pcmData, {
int sampleRate = 44100,
int channels = 1,
int bitsPerSample = 16,
}) {
final byteRate = sampleRate * channels * bitsPerSample ~/ 8;
final blockAlign = channels * bitsPerSample ~/ 8;
final dataLength = pcmData.length;
final fileSize = 44 + dataLength;
final header = BytesBuilder();
header.add(ascii.encode('RIFF'));
header.add(_intToBytes(fileSize - 8, 4));
header.add(ascii.encode('WAVE'));
header.add(ascii.encode('fmt '));
header.add(_intToBytes(16, 4)); // Subchunk1Size for PCM
header.add(_intToBytes(1, 2)); // AudioFormat = PCM
header.add(_intToBytes(channels, 2));
header.add(_intToBytes(sampleRate, 4));
header.add(_intToBytes(byteRate, 4));
header.add(_intToBytes(blockAlign, 2));
header.add(_intToBytes(bitsPerSample, 2));
header.add(ascii.encode('data'));
header.add(_intToBytes(dataLength, 4));
header.add(pcmData);
return header.toBytes();
}
Uint8List _intToBytes(int value, int byteCount) {
final bytes = ByteData(byteCount);
if (byteCount == 2) {
bytes.setInt16(0, value, Endian.little);
} else if (byteCount == 4) {
bytes.setInt32(0, value, Endian.little);
}
return bytes.buffer.asUint8List();
}
I’m open to aligning the implementation with the package’s architecture and standards. Let me know if a PR draft or further discussion would be preferable before proceeding.
Thanks again for maintaining and supporting this excellent package!