Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion docs/audio-recorder.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class AudioRecorderViewModel

## Configure the recording options

When calling `CreateRecorder` it is possible to provide an optional parameter of type `AudioRecorderOptions`, this parameter makes it possible to customize the recording settings at the platform level. **Note that currently you can only customize options for iOS and macOS**.
When calling `CreateRecorder` it is possible to provide an optional parameter of type `AudioRecorderOptions`, this parameter makes it possible to customize the recording settings at the platform level.

The following example shows how to enable both recording (input) and playback (output) of audio:

Expand All @@ -48,6 +48,36 @@ audioManager.CreateRecorder(
});
```

## Recording from a specific audio device (Android)

By default, the recorder uses the system's default microphone (`AudioSource.Mic`). To record from a specific device such as a Bluetooth headset or USB microphone, use the `PreferredDevice` property.

```csharp
#if ANDROID
using Android.Content;
using Android.Media;

var audioManager = (AudioManager)Android.App.Application.Context.GetSystemService(Context.AudioService)!;
var btDevice = audioManager.GetDevices(GetDevicesTargets.Inputs)
.FirstOrDefault(d => d.Type == AudioDeviceType.BluetoothSco);

var recorder = audioMgr.CreateRecorder(
new AudioRecorderOptions
{
PreferredDevice = btDevice
});
#endif
```

> [!NOTE]
> `SetPreferredDevice` requires Android API 23+ (Marshmallow) for WAV recording and API 28+ (Pie) for AAC recording. On older API levels, the property is ignored and the system default device is used. If the preferred device is unavailable (e.g. disconnected), recording silently falls back to the default device.

> [!IMPORTANT]
> On Android 12+ (API 31+), the `BLUETOOTH_CONNECT` runtime permission may be required to discover and select Bluetooth audio devices. Add it to your `AndroidManifest.xml`:
> ```xml
> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
> ```

## Recording from a Bluetooth microphone (iOS/macCatalyst)

By default, the recorder uses the system's built-in microphone. To enable recording from Bluetooth devices such as AirPods or other headsets, you need to opt in by setting `CategoryOptions` to `AllowBluetooth`. This makes Bluetooth HFP (Hands-Free Profile) devices available as recording inputs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Diagnostics;
using System.Diagnostics;
using static Microsoft.Maui.ApplicationModel.Permissions;
#if ANDROID
using Android.Media;
#endif
#if IOS || MACCATALYST
using AVFoundation;
#endif
Expand All @@ -16,6 +19,9 @@ public class AudioRecorderPageViewModel : BaseViewModel
readonly Stopwatch recordingStopwatch = new Stopwatch();
bool isPlaying;

#if ANDROID
AudioDeviceInfo[] availableInputDevices = [];
#endif
#if IOS || MACCATALYST
AVAudioSessionPortDescription[] availableInputPorts = [];
#endif
Expand Down Expand Up @@ -153,6 +159,31 @@ public bool AllowBluetooth

void LoadInputDevices()
{
#if ANDROID
if (!OperatingSystem.IsAndroidVersionAtLeast(23))
{
InputDevices = ["Default"];
SelectedInputDevice = "Default";
return;
}

var androidAudioManager = (Android.Media.AudioManager?)Android.App.Application.Context.GetSystemService(Android.Content.Context.AudioService);
if (androidAudioManager is null)
{
return;
}

availableInputDevices = androidAudioManager.GetDevices(GetDevicesTargets.Inputs) ?? [];

var devices = new List<string> { "Default" };
foreach (var device in availableInputDevices)
{
devices.Add($"{device.ProductName} ({device.Type})");
}

InputDevices = devices;
SelectedInputDevice = "Default";
#endif
#if IOS || MACCATALYST
var session = AVAudioSession.SharedInstance();

Expand Down Expand Up @@ -217,6 +248,16 @@ async void Start()
options.SampleRate = SelectedSampleRate;
}

#if ANDROID
if (SelectedInputDevice != "Default")
{
var index = InputDevices.IndexOf(SelectedInputDevice) - 1;
if (index >= 0 && index < availableInputDevices.Length)
{
options.PreferredDevice = availableInputDevices[index];
}
}
#endif
#if IOS || MACCATALYST
if (AllowBluetooth)
{
Expand Down Expand Up @@ -286,4 +327,4 @@ internal void OnNavigatedFrom()
{
audioPlayer?.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System.Diagnostics;
using System.Diagnostics;
using Plugin.Maui.Audio.AudioListeners;
#if ANDROID
using Android.Media;
#endif
#if IOS || MACCATALYST
using AVFoundation;
#endif
Expand Down Expand Up @@ -27,6 +30,9 @@ public class AudioStreamerPageViewModel : BaseViewModel
string capturedAudioWavFileLeft;
string capturedAudioWavFileRight;

#if ANDROID
AudioDeviceInfo[] availableInputDevices = [];
#endif
#if IOS || MACCATALYST
AVAudioSessionPortDescription[] availableInputPorts = [];
#endif
Expand Down Expand Up @@ -117,6 +123,31 @@ public bool AllowBluetooth

void LoadInputDevices()
{
#if ANDROID
if (!OperatingSystem.IsAndroidVersionAtLeast(23))
{
InputDevices = ["Default"];
SelectedInputDevice = "Default";
return;
}

var androidAudioManager = (Android.Media.AudioManager?)Android.App.Application.Context.GetSystemService(Android.Content.Context.AudioService);
if (androidAudioManager is null)
{
return;
}

availableInputDevices = androidAudioManager.GetDevices(GetDevicesTargets.Inputs) ?? [];

var devices = new List<string> { "Default" };
foreach (var device in availableInputDevices)
{
devices.Add($"{device.ProductName} ({device.Type})");
}

InputDevices = devices;
SelectedInputDevice = "Default";
#endif
#if IOS || MACCATALYST
var session = AVAudioSession.SharedInstance();

Expand Down Expand Up @@ -314,6 +345,16 @@ async void Start()
audioStreamer.Options.BitDepth = SelectedBitDepth;
audioStreamer.Options.SampleRate = SelectedSampleRate;

#if ANDROID
if (SelectedInputDevice != "Default")
{
var index = InputDevices.IndexOf(SelectedInputDevice) - 1;
if (index >= 0 && index < availableInputDevices.Length)
{
audioStreamer.Options.PreferredDevice = availableInputDevices[index];
}
}
#endif
#if IOS || MACCATALYST
if (AllowBluetooth)
{
Expand Down Expand Up @@ -434,4 +475,4 @@ internal void OnNavigatedFrom()
{
audioPlayer?.Dispose();
}
}
}
85 changes: 83 additions & 2 deletions src/Plugin.Maui.Audio/AudioRecorder/AudioRecorder.android.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ partial class AudioRecorder : IAudioRecorder

static readonly AudioRecorderOptions defaultOptions = new AudioRecorderOptions();

// Bluetooth SCO state
bool bluetoothScoStarted;
Android.Media.AudioManager? androidAudioManager;
Android.Media.Mode previousAudioMode;

AudioRecorderOptions audioRecorderOptions;

// Recording options that are extracted/solved
Expand Down Expand Up @@ -62,6 +67,16 @@ public Task StartAsync(string filePath, AudioRecorderOptions? recordingOptions =

audioFilePath = filePath;

// Determine if the preferred device is Bluetooth and requires SCO
bool isBluetoothDevice = IsBluetoothDevice(audioRecorderOptions.PreferredDevice);
AudioSource audioSource = isBluetoothDevice ? AudioSource.VoiceCommunication : AudioSource.Mic;

// Start Bluetooth SCO if needed (establishes the audio route for BT input)
if (isBluetoothDevice)
{
StartBluetoothSco();
}

// solve some parameters needed for AudioRecord/MediaRecorder
ChannelIn channelIn =
SharedChannelTypesToAndroidChannelTypes(audioRecorderOptions.Channels, audioRecorderOptions.ThrowIfNotSupported);
Expand Down Expand Up @@ -106,7 +121,16 @@ public Task StartAsync(string filePath, AudioRecorderOptions? recordingOptions =
}
}

audioRecord = new AudioRecord(AudioSource.Mic, sampleRate, channelIn, encoding, bufferSize);
audioRecord = new AudioRecord(audioSource, sampleRate, channelIn, encoding, bufferSize);

if (OperatingSystem.IsAndroidVersionAtLeast(23) && audioRecorderOptions.PreferredDevice is not null)
{
if (!audioRecord.SetPreferredDevice(audioRecorderOptions.PreferredDevice))
{
Trace.WriteLine("AudioRecorder: failed to set preferred device on AudioRecord, using default");
}
}

audioRecord.StartRecording();
Task.Run(WriteAudioDataToFile);
}
Expand All @@ -129,13 +153,22 @@ public Task StartAsync(string filePath, AudioRecorderOptions? recordingOptions =
.ApplicationContext); //needs context, obsoleted without context https://stackoverflow.com/questions/73598179/deprecated-mediarecorder-new-mediarecorder#73598440

mediaRecorder.Reset();
mediaRecorder.SetAudioSource(AudioSource.Mic);
mediaRecorder.SetAudioSource(audioSource);
mediaRecorder.SetOutputFormat(outputFormat);
mediaRecorder.SetAudioEncoder(audioEncoder);
mediaRecorder.SetAudioChannels(numChannels);
mediaRecorder.SetAudioSamplingRate(sampleRate);
mediaRecorder.SetAudioEncodingBitRate(bitRate);
mediaRecorder.SetOutputFile(audioFilePath);

if (OperatingSystem.IsAndroidVersionAtLeast(28) && audioRecorderOptions.PreferredDevice is not null)
{
if (!mediaRecorder.SetPreferredDevice(audioRecorderOptions.PreferredDevice))
{
Trace.WriteLine("AudioRecorder: failed to set preferred device on MediaRecorder, using default");
}
}

Comment thread
jfversluis marked this conversation as resolved.
mediaRecorder.Prepare();
mediaRecorder.Start();

Expand Down Expand Up @@ -166,6 +199,9 @@ public Task<IAudioSource> StopAsync()
mediaRecorder?.Stop();
}

// Clean up Bluetooth SCO if it was started
StopBluetoothSco();

if (audioFilePath is null)
{
throw new InvalidOperationException("'audioFilePath' is null, this really should not happen.");
Expand Down Expand Up @@ -349,4 +385,49 @@ static ChannelIn SharedChannelTypesToAndroidChannelTypes(ChannelType type, bool
: SharedChannelTypesToAndroidChannelTypes(defaultOptions.Channels, true)
};
}

static bool IsBluetoothDevice(AudioDeviceInfo? device)
{
if (device is null || !OperatingSystem.IsAndroidVersionAtLeast(23))
{
return false;
}

return device.Type == AudioDeviceType.BluetoothSco
|| (OperatingSystem.IsAndroidVersionAtLeast(31)
&& (device.Type == AudioDeviceType.BleHeadset
|| device.Type == AudioDeviceType.BleSpeaker));
}

void StartBluetoothSco()
{
androidAudioManager = Android.App.Application.Context.GetSystemService(Context.AudioService) as Android.Media.AudioManager;
if (androidAudioManager is null)
{
return;
}

previousAudioMode = androidAudioManager.Mode;
androidAudioManager.Mode = Android.Media.Mode.InCommunication;
androidAudioManager.StartBluetoothSco();
androidAudioManager.BluetoothScoOn = true;
bluetoothScoStarted = true;

Trace.WriteLine("AudioRecorder: Bluetooth SCO started for BT device recording");
}

void StopBluetoothSco()
{
if (!bluetoothScoStarted || androidAudioManager is null)
{
return;
}

androidAudioManager.StopBluetoothSco();
androidAudioManager.BluetoothScoOn = false;
androidAudioManager.Mode = previousAudioMode;
bluetoothScoStarted = false;

Trace.WriteLine("AudioRecorder: Bluetooth SCO stopped");
}
}
Loading
Loading