Skip to content

Commit 002bd8b

Browse files
committed
docs: add AudioRecorder and AudioPlayer documentation
1 parent e01a8a4 commit 002bd8b

File tree

7 files changed

+248
-6
lines changed

7 files changed

+248
-6
lines changed

docs/_sidebar.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
- [Logging](guide/logging.md)
2929
- Utilities
3030
- [Audio Converter](guide/audio_converter.md)
31+
- [Audio Recorder](guide/utilities/audio_recorder.md)
32+
- [Audio Player](guide/utilities/audio_player.md)
3133
- [Video Buffer Converter](guide/utilities/video_buffer_converter.md)
3234
- [Video Capture](guide/video_capturer.md)
3335
- [Screen Capture](guide/screen_capturer.md)

docs/guide/overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ This section provides detailed guides for various features of the webrtc-java li
3838
## Utilities
3939

4040
- [Audio Converter](guide/audio_converter.md) - Resample and remix 10 ms PCM frames between different rates and channel layouts
41+
- [Audio Recorder](guide/utilities/audio_recorder.md) - Capture microphone input and receive 10 ms PCM frames via an AudioSink
42+
- [Audio Player](guide/utilities/audio_player.md) - Play PCM audio to an output device by supplying frames via an AudioSource
4143
- [Video Buffer Converter](guide/utilities/video_buffer_converter.md) - Convert between I420 and common FourCC pixel formats (e.g., RGBA, NV12)
4244
- [Video Capture](guide/video_capturer.md) - Control a camera device, configure capabilities, and deliver frames to a sink
4345
- [Screen Capture](guide/screen_capturer.md) - Enumerate and capture full desktop screens/monitors
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Audio Player
2+
3+
The AudioPlayer is a small helper that plays audio using a selected output device by pulling frames from your implementation of AudioSource. It manages a native AudioDeviceModule internally and provides idempotent start/stop.
4+
5+
API: `dev.onvoid.webrtc.media.audio.AudioPlayer`
6+
7+
## When to use it
8+
- You want to render raw PCM audio (generated or decoded by your app) to an OS output device.
9+
- You need a simple, high‑level start/stop wrapper around WebRTC’s audio playout.
10+
11+
See also: [Audio Device Selection](../audio_devices.md), [Custom Audio Source](../custom_audio_source.md), [Headless Audio](../headless_audio_device_module.md).
12+
13+
## Key concepts
14+
- Device selection: Provide an `AudioDevice` representing the output device (speaker) before starting.
15+
- Data supply: Implement `AudioSource` to provide 10 ms PCM frames on demand.
16+
- Lifecycle: `start()` initializes output and begins pulling; `stop()` halts playout and releases resources.
17+
18+
## Basic usage
19+
20+
```java
21+
import dev.onvoid.webrtc.media.audio.AudioPlayer;
22+
import dev.onvoid.webrtc.media.audio.AudioSource;
23+
import dev.onvoid.webrtc.media.audio.AudioDevice;
24+
import dev.onvoid.webrtc.media.audio.AudioDeviceModule;
25+
26+
import java.nio.ByteBuffer;
27+
28+
public class TonePlayerExample {
29+
30+
public static void main(String[] args) {
31+
// Choose a playout device (speaker). Enumerate via a temporary ADM.
32+
AudioDeviceModule adm = new AudioDeviceModule();
33+
AudioDevice speaker = adm.getPlayoutDevices().stream()
34+
.findFirst()
35+
.orElseThrow(() -> new IllegalStateException("No playout device found"));
36+
adm.dispose();
37+
38+
// Provide 10 ms frames of PCM 16‑bit audio. This example generates a sine tone.
39+
final int sampleRate = 48000;
40+
final int channels = 2;
41+
final int bytesPerSample = channels * 2; // 16‑bit
42+
final double frequency = 440.0; // A4
43+
final double twoPiFDivFs = 2 * Math.PI * frequency / sampleRate;
44+
final int samplesPer10msPerChannel = sampleRate / 100; // 480 samples/channel
45+
final int totalSamplesPer10ms = samplesPer10msPerChannel * channels; // e.g., 960 samples
46+
final double[] phase = new double[] {0.0};
47+
48+
AudioSource source = (audioSamples, nSamples, nBytesPerSample, nChannels, samplesPerSec) -> {
49+
// Ensure caller requested matches our configuration
50+
if (nBytesPerSample != bytesPerSample || nChannels != channels || samplesPerSec != sampleRate) {
51+
// Fill silence if formats mismatch
52+
java.util.Arrays.fill(audioSamples, (byte) 0);
53+
return totalSamplesPer10ms;
54+
}
55+
56+
// Generate interleaved stereo sine wave
57+
int idx = 0;
58+
for (int i = 0; i < samplesPer10msPerChannel; i++) {
59+
short s = (short) (Math.sin(phase[0]) * 32767);
60+
// left
61+
audioSamples[idx++] = (byte) (s & 0xFF);
62+
audioSamples[idx++] = (byte) ((s >> 8) & 0xFF);
63+
// right
64+
audioSamples[idx++] = (byte) (s & 0xFF);
65+
audioSamples[idx++] = (byte) ((s >> 8) & 0xFF);
66+
67+
phase[0] += twoPiFDivFs;
68+
69+
if (phase[0] > Math.PI * 2) {
70+
phase[0] -= Math.PI * 2;
71+
}
72+
}
73+
return totalSamplesPer10ms; // number of samples written across all channels
74+
};
75+
76+
AudioPlayer player = new AudioPlayer();
77+
player.setAudioDevice(speaker);
78+
player.setAudioSource(source);
79+
80+
player.start();
81+
// ... playout running ...
82+
player.stop();
83+
}
84+
}
85+
```
86+
87+
## Data format
88+
- The player requests 10 ms frames as 16‑bit little‑endian PCM via `AudioSource#onPlaybackData`.
89+
- Return value must be the number of samples written across all channels for that 10 ms frame.
90+
91+
## Tips
92+
- If your synthesis/decoder operates at a different rate or channel layout, convert using the [Audio Converter](../audio_converter.md) before writing into the output buffer.
93+
94+
## API reference
95+
- `setAudioDevice(AudioDevice device)` – choose output device
96+
- `setAudioSource(AudioSource source)` – provide playout frames
97+
- `start()` / `stop()` – control the playout lifecycle
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Audio Recorder
2+
3+
The AudioRecorder is a small helper that captures audio from a selected input device and forwards PCM frames to your implementation of AudioSink. It manages a native AudioDeviceModule internally and provides idempotent start/stop.
4+
5+
API: `dev.onvoid.webrtc.media.audio.AudioRecorder`
6+
7+
## When to use it
8+
- You want a simple way to record microphone input without wiring a full PeerConnection.
9+
- You need raw 16‑bit PCM frames (10 ms) delivered to your code for analysis, file writing, or custom processing.
10+
11+
See also: [Audio Device Selection](../audio_devices.md), [Audio Processing](../audio_processing.md), [Custom Audio Source](../custom_audio_source.md).
12+
13+
## Key concepts
14+
- Device selection: Provide an `AudioDevice` representing the input device (microphone) before starting.
15+
- Data delivery: Implement `AudioSink` to receive recorded frames.
16+
- Lifecycle: `start()` initializes and starts capture once; `stop()` halts and releases native resources.
17+
18+
## Basic usage
19+
20+
```java
21+
import dev.onvoid.webrtc.media.audio.AudioRecorder;
22+
import dev.onvoid.webrtc.media.audio.AudioSink;
23+
import dev.onvoid.webrtc.media.audio.AudioDevice;
24+
import dev.onvoid.webrtc.media.audio.AudioDeviceModule;
25+
26+
public class MicRecorderExample {
27+
public static void main(String[] args) {
28+
// Pick a recording device (microphone). You can enumerate via MediaDevices.getAudioCaptureDevices()
29+
AudioDevice mic = ...
30+
31+
// Implement a sink to receive 10 ms PCM frames
32+
AudioSink sink = (audioSamples, nSamples, nBytesPerSample, nChannels, samplesPerSec, totalDelayMS, clockDrift) -> {
33+
// audioSamples: little‑endian 16‑bit PCM
34+
// nSamples: total samples across all channels for this 10 ms frame
35+
// nBytesPerSample: typically 2 for 16‑bit
36+
// nChannels: number of channels (1 = mono, 2 = stereo)
37+
// samplesPerSec: sample rate (e.g., 48000)
38+
// totalDelayMS, clockDrift: diagnostics
39+
// TODO: write to file, analyzer, encoder, etc.
40+
};
41+
42+
AudioRecorder recorder = new AudioRecorder();
43+
recorder.setAudioDevice(mic);
44+
recorder.setAudioSink(sink);
45+
46+
recorder.start();
47+
// ... capture running ...
48+
recorder.stop();
49+
}
50+
}
51+
```
52+
53+
## Data format
54+
- Frames are delivered every 10 ms as 16‑bit little‑endian PCM in a `byte[]`.
55+
- `nSamples` refers to the number of samples across all channels within the 10 ms frame. Example: 48 kHz stereo → (48000/100)×2 = 960×2 = 1920 samples.
56+
57+
## Tips
58+
- Apply echo cancellation, AGC, and noise suppression via the [Audio Processing](../audio_processing.md) guide if needed.
59+
- If you need to resample or remix, use the [Audio Converter](../audio_converter.md).
60+
- Device selection details and best practices are covered in [Audio Device Selection](../audio_devices.md).
61+
62+
## API reference
63+
- `setAudioDevice(AudioDevice device)` – choose input device
64+
- `setAudioSink(AudioSink sink)` – receive captured frames
65+
- `start()` / `stop()` – control the capture lifecycle

webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioPlayer.java

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,68 @@
1818

1919
import java.util.concurrent.atomic.AtomicBoolean;
2020

21+
/**
22+
* High-level wrapper to control audio playout. Provides idempotent start/stop semantics
23+
* around an underlying AudioDeviceModule.
24+
*
25+
* @author Alex Andres
26+
*/
2127
public class AudioPlayer {
2228

29+
/**
30+
* Indicates if the player is currently started. Used atomically to provide
31+
* a thread-safe idempotent start/stop sequence.
32+
*/
2333
private final AtomicBoolean playing;
2434

35+
/**
36+
* Underlying audio device module managing the native playout resources.
37+
* Created lazily on {@link #start()} and disposed on {@link #stop()}.
38+
*/
2539
private AudioDeviceModule module;
2640

41+
/**
42+
* The audio output device to use for playout. Should be set before calling {@link #start()}.
43+
*/
2744
private AudioDevice device;
2845

46+
/**
47+
* The audio source providing audio frames to be rendered by the device.
48+
*/
2949
private AudioSource source;
3050

3151

52+
/**
53+
* Creates a new AudioPlayer instance in a non-playing state.
54+
*/
3255
public AudioPlayer() {
3356
playing = new AtomicBoolean();
3457
}
3558

59+
/**
60+
* Assigns the audio output device.
61+
*
62+
* @param device the target playout device (may be null to clear).
63+
*/
3664
public void setAudioDevice(AudioDevice device) {
3765
this.device = device;
3866
}
3967

68+
/**
69+
* Assigns the audio source supplying audio data.
70+
*
71+
* @param source the audio source (may be null to clear).
72+
*/
4073
public void setAudioSource(AudioSource source) {
4174
this.source = source;
4275
}
4376

77+
/**
78+
* Starts audio playout if not already running. This method is idempotent; subsequent
79+
* calls while already playing have no effect.
80+
* <p>
81+
* Initializes and configures the underlying {@link AudioDeviceModule}.
82+
*/
4483
public void start() {
4584
if (playing.compareAndSet(false, true)) {
4685
module = new AudioDeviceModule();
@@ -51,11 +90,14 @@ public void start() {
5190
}
5291
}
5392

93+
/**
94+
* Stops audio playout if currently running and releases underlying resources.
95+
* This method is idempotent; calling when already stopped has no effect.
96+
*/
5497
public void stop() {
5598
if (playing.compareAndSet(true, false)) {
5699
module.stopPlayout();
57100
module.dispose();
58101
}
59102
}
60-
61103
}

webrtc/src/main/java/dev/onvoid/webrtc/media/audio/AudioRecorder.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,29 +18,57 @@
1818

1919
import java.util.concurrent.atomic.AtomicBoolean;
2020

21+
/**
22+
* High-level recorder that captures audio from a configured {@link AudioDevice}
23+
* and forwards frames to an {@link AudioSink}. Manages the lifecycle of the
24+
* underlying {@link AudioDeviceModule} and offers thread-safe start/stop control.
25+
*
26+
* @author Alex Andres
27+
*/
2128
public class AudioRecorder {
2229

30+
/** Indicates whether the recording is currently active (thread-safe). */
2331
private final AtomicBoolean capturing;
2432

33+
/** Underlying audio device module handling the native capture. */
2534
private AudioDeviceModule module;
2635

36+
/** The audio input device to record from. */
2737
private AudioDevice device;
2838

39+
/** The sink receiving captured audio data. */
2940
private AudioSink sink;
3041

3142

43+
/**
44+
* Creates a new AudioRecorder with an inactive capture state.
45+
*/
3246
public AudioRecorder() {
3347
capturing = new AtomicBoolean();
3448
}
3549

50+
/**
51+
* Set the audio input device to be used for recording.
52+
*
53+
* @param device the recording device; may be null until set.
54+
*/
3655
public void setAudioDevice(AudioDevice device) {
3756
this.device = device;
3857
}
3958

59+
/**
60+
* Set the sink that will receive captured audio frames.
61+
*
62+
* @param sink the audio sink implementation.
63+
*/
4064
public void setAudioSink(AudioSink sink) {
4165
this.sink = sink;
4266
}
4367

68+
/**
69+
* Start recording if not already active. Initializes and starts the underlying
70+
* AudioDeviceModule.
71+
*/
4472
public void start() {
4573
if (capturing.compareAndSet(false, true)) {
4674
module = new AudioDeviceModule();
@@ -51,11 +79,13 @@ public void start() {
5179
}
5280
}
5381

82+
/**
83+
* Stop recording if active and release resources.
84+
*/
5485
public void stop() {
5586
if (capturing.compareAndSet(true, false)) {
5687
module.stopRecording();
5788
module.dispose();
5889
}
5990
}
60-
6191
}

webrtc/src/main/java/dev/onvoid/webrtc/media/video/VideoBufferConverter.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ public final class VideoBufferConverter {
4343
* @throws NullPointerException if {@code src} or {@code dst} is {@code null}.
4444
* @throws Exception if the native conversion fails.
4545
*/
46-
public static void convertFromI420(VideoFrameBuffer src, byte[] dst, FourCC fourCC) throws Exception {
46+
public static void convertFromI420(VideoFrameBuffer src, byte[] dst, FourCC fourCC)
47+
throws Exception {
4748
if (src == null) {
4849
throw new NullPointerException("Source buffer must not be null");
4950
}
@@ -76,7 +77,8 @@ public static void convertFromI420(VideoFrameBuffer src, byte[] dst, FourCC four
7677
* @throws IllegalArgumentException if {@code dst} is read-only.
7778
* @throws Exception if the native conversion fails.
7879
*/
79-
public static void convertFromI420(VideoFrameBuffer src, ByteBuffer dst, FourCC fourCC) throws Exception {
80+
public static void convertFromI420(VideoFrameBuffer src, ByteBuffer dst, FourCC fourCC)
81+
throws Exception {
8082
if (src == null) {
8183
throw new NullPointerException("Source buffer must not be null");
8284
}
@@ -130,7 +132,8 @@ public static void convertFromI420(VideoFrameBuffer src, ByteBuffer dst, FourCC
130132
* @throws NullPointerException if {@code src} or {@code dst} is {@code null}.
131133
* @throws Exception if the native conversion fails.
132134
*/
133-
public static void convertToI420(byte[] src, I420Buffer dst, FourCC fourCC) throws Exception {
135+
public static void convertToI420(byte[] src, I420Buffer dst, FourCC fourCC)
136+
throws Exception {
134137
if (src == null) {
135138
throw new NullPointerException("Source buffer must not be null");
136139
}
@@ -160,7 +163,8 @@ public static void convertToI420(byte[] src, I420Buffer dst, FourCC fourCC) thro
160163
* @throws NullPointerException if {@code src} or {@code dst} is {@code null}.
161164
* @throws Exception if the native conversion fails.
162165
*/
163-
public static void convertToI420(ByteBuffer src, I420Buffer dst, FourCC fourCC) throws Exception {
166+
public static void convertToI420(ByteBuffer src, I420Buffer dst, FourCC fourCC)
167+
throws Exception {
164168
if (src == null) {
165169
throw new NullPointerException("Source buffer must not be null");
166170
}

0 commit comments

Comments
 (0)