Skip to content

Commit 6294861

Browse files
committed
Create audio buffer
1 parent 0892526 commit 6294861

File tree

6 files changed

+194
-0
lines changed

6 files changed

+194
-0
lines changed
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using System;
2+
using LiveKit.Internal;
3+
4+
namespace LiveKit
5+
{
6+
/// <summary>
7+
/// A thread-safe buffer for buffering audio samples.
8+
/// </summary>
9+
internal class AudioBuffer
10+
{
11+
private readonly uint _bufferDurationMs;
12+
private RingBuffer _buffer;
13+
private uint _channels;
14+
private uint _sampleRate;
15+
private object _lock = new object();
16+
17+
/// <summary>
18+
/// Initializes a new audio sample buffer for holding samples for a given duration.
19+
/// </summary>
20+
internal AudioBuffer(uint bufferDurationMs = 200)
21+
{
22+
_bufferDurationMs = bufferDurationMs;
23+
}
24+
25+
/// <summary>
26+
/// Write audio samples.
27+
/// </summary>
28+
/// <remarks>
29+
/// The float data will be converted to short format before being written to the buffer.
30+
/// If the number of channels or sample rate changes, the buffer will be recreated.
31+
/// </remarks>
32+
/// <param name="data">The audio samples to write.</param>
33+
/// <param name="channels">The number of channels in the audio data.</param>
34+
/// <param name="sampleRate">The sample rate of the audio data in Hz.</param>
35+
internal void Write(float[] data, uint channels, uint sampleRate)
36+
{
37+
static short FloatToS16(float v)
38+
{
39+
v *= 32768f;
40+
v = Math.Min(v, 32767f);
41+
v = Math.Max(v, -32768f);
42+
return (short)(v + Math.Sign(v) * 0.5f);
43+
}
44+
45+
var s16Data = new short[data.Length];
46+
for (int i = 0; i < data.Length; i++)
47+
{
48+
s16Data[i] = FloatToS16(data[i]);
49+
}
50+
Capture(s16Data, channels, sampleRate);
51+
}
52+
53+
private void Capture(short[] data, uint channels, uint sampleRate)
54+
{
55+
lock (_lock)
56+
{
57+
if (_buffer == null || channels != _channels || sampleRate != _sampleRate)
58+
{
59+
var size = (int)(channels * sampleRate * (_bufferDurationMs / 1000f));
60+
_buffer?.Dispose();
61+
_buffer = new RingBuffer(size * sizeof(short));
62+
_channels = channels;
63+
_sampleRate = sampleRate;
64+
}
65+
unsafe
66+
{
67+
fixed (short* pData = data)
68+
{
69+
var byteData = new ReadOnlySpan<byte>(pData, data.Length * sizeof(short));
70+
_buffer.Write(byteData);
71+
}
72+
}
73+
}
74+
}
75+
76+
/// <summary>
77+
/// Reads a frame that is the length of the given duration.
78+
/// </summary>
79+
/// <param name="durationMs">The duration of the audio samples to read in milliseconds.</param>
80+
/// <returns>An AudioFrame containing the read audio samples or if there is not enough samples, null.</returns>
81+
internal AudioFrame ReadDuration(uint durationMs)
82+
{
83+
lock (_lock)
84+
{
85+
if (_buffer == null) return null;
86+
87+
var samplesForDuration = (uint)(_sampleRate * (durationMs / 1000f));
88+
var requiredLength = samplesForDuration * _channels * sizeof(short);
89+
if (_buffer.AvailableRead() < requiredLength) return null;
90+
91+
var frame = new AudioFrame(_sampleRate, _channels, samplesForDuration);
92+
unsafe
93+
{
94+
var frameData = new Span<byte>(frame.Data.ToPointer(), frame.Length);
95+
_buffer.Read(frameData);
96+
}
97+
return frame;
98+
}
99+
}
100+
}
101+
}

Runtime/Scripts/Internal/AudioBuffer.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tests/AudioBuffer.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using NUnit.Framework;
3+
4+
namespace LiveKit.Tests
5+
{
6+
public class AudioBufferTest
7+
{
8+
[Test]
9+
[TestCase(24000u, 1u, 10u)]
10+
[TestCase(48000u, 2u, 10u)]
11+
public void TestWriteAndRead(uint sampleRate, uint channels, uint durationMs)
12+
{
13+
var buffer = new AudioBuffer();
14+
15+
Assert.IsNull(buffer.ReadDuration(durationMs), "Should not be able to read from empty buffer");
16+
17+
var samples = TestUtils.GenerateSineWave(channels, sampleRate, durationMs);
18+
buffer.Write(samples, channels, sampleRate);
19+
20+
Assert.IsNull(buffer.ReadDuration(durationMs * 2), "Should not be enough samples for this read");
21+
22+
var frame = buffer.ReadDuration(durationMs);
23+
Assert.IsNotNull(frame);
24+
Assert.AreEqual(sampleRate, frame.SampleRate);
25+
Assert.AreEqual(channels, frame.NumChannels);
26+
Assert.AreEqual(samples.Length / channels, frame.SamplesPerChannel);
27+
28+
Assert.IsNull(buffer.ReadDuration(durationMs), "Should not be able to read again");
29+
}
30+
}
31+
}

Tests/AudioBuffer.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Tests/TestUtils.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System;
2+
3+
namespace LiveKit.Tests
4+
{
5+
internal static class TestUtils
6+
{
7+
/// <summary>
8+
/// Generates a sine wave with the specified parameters.
9+
/// </summary>
10+
/// <param name="channels">Number of audio channels.</param>
11+
/// <param name="sampleRate">Sample rate in Hz.</param>
12+
/// <param name="durationMs">Duration in milliseconds.</param>
13+
/// <param name="frequency">Frequency of the sine wave in Hz.</param>
14+
/// <returns>A float array containing the generated sine wave.</returns>
15+
internal static float[] GenerateSineWave(uint channels, uint sampleRate, uint durationMs, uint frequency = 440)
16+
{
17+
var samplesPerChannel = sampleRate * durationMs / 1000;
18+
var samples = new float[samplesPerChannel * channels];
19+
for (int i = 0; i < samplesPerChannel; i++)
20+
{
21+
float sampleValue = (float)Math.Sin(2 * Math.PI * frequency * i / sampleRate);
22+
for (int channel = 0; channel < channels; channel++)
23+
samples[i * channels + channel] = sampleValue;
24+
}
25+
return samples;
26+
}
27+
}
28+
}
29+

Tests/TestUtils.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)