Skip to content

Feature Request: Support Streaming Microphone Data on Linux in PCM16 Format #496

@KunjKanani

Description

@KunjKanani

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!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions