LibAudio/AudioServer: Add support for playing real time audio

This commit is contained in:
Bananymous 2025-08-06 01:50:41 +03:00
parent 7ad3f967db
commit 7a5cfe1728
5 changed files with 123 additions and 28 deletions

View File

@ -12,6 +12,17 @@
namespace LibAudio namespace LibAudio
{ {
BAN::ErrorOr<Audio> Audio::create(uint32_t channels, uint32_t sample_rate, uint32_t sample_frames)
{
Audio result;
TRY(result.initialize((sample_frames + 10) * channels));
result.m_audio_buffer->sample_rate = sample_rate;
result.m_audio_buffer->channels = channels;
return result;
}
BAN::ErrorOr<Audio> Audio::load(BAN::StringView path) BAN::ErrorOr<Audio> Audio::load(BAN::StringView path)
{ {
Audio result(TRY(AudioLoader::load(path))); Audio result(TRY(AudioLoader::load(path)));
@ -69,18 +80,6 @@ namespace LibAudio
return *this; return *this;
} }
BAN::ErrorOr<void> Audio::start()
{
ASSERT(m_server_fd != -1);
const ssize_t nsend = send(m_server_fd, &m_smo_key, sizeof(m_smo_key), 0);
if (nsend == -1)
return BAN::Error::from_errno(errno);
ASSERT(nsend == sizeof(m_smo_key));
return {};
}
BAN::ErrorOr<void> Audio::initialize(uint32_t total_samples) BAN::ErrorOr<void> Audio::initialize(uint32_t total_samples)
{ {
m_smo_size = sizeof(AudioBuffer) + total_samples * sizeof(AudioBuffer::sample_t); m_smo_size = sizeof(AudioBuffer) + total_samples * sizeof(AudioBuffer::sample_t);
@ -118,11 +117,58 @@ namespace LibAudio
return {}; return {};
} }
BAN::ErrorOr<void> Audio::start()
{
ASSERT(m_server_fd != -1);
const ssize_t nsend = send(m_server_fd, &m_smo_key, sizeof(m_smo_key), 0);
if (nsend == -1)
return BAN::Error::from_errno(errno);
ASSERT(nsend == sizeof(m_smo_key));
return {};
}
void Audio::set_paused(bool paused)
{
ASSERT(m_server_fd != -1);
if (m_audio_buffer->paused == paused)
return;
m_audio_buffer->paused = paused;
long dummy = 0;
send(m_server_fd, &dummy, sizeof(dummy), 0);
}
size_t Audio::queue_samples(BAN::Span<const AudioBuffer::sample_t> samples)
{
size_t samples_queued = 0;
uint32_t head = m_audio_buffer->head;
while (samples_queued < samples.size())
{
const uint32_t next_head = (head + 1) % m_audio_buffer->capacity;
if (next_head == m_audio_buffer->tail)
break;
m_audio_buffer->samples[head] = samples[samples_queued++];
head = next_head;
if (samples_queued % 128 == 0)
m_audio_buffer->head = head;
}
if (samples_queued % 128 != 0)
m_audio_buffer->head = head;
return samples_queued;
}
void Audio::update() void Audio::update()
{ {
if (!m_audio_loader) if (!m_audio_loader)
return; return;
if (!m_audio_loader->samples_remaining() && !is_playing())
return set_paused(true);
while (m_audio_loader->samples_remaining()) while (m_audio_loader->samples_remaining())
{ {
const uint32_t next_head = (m_audio_buffer->head + 1) % m_audio_buffer->capacity; const uint32_t next_head = (m_audio_buffer->head + 1) % m_audio_buffer->capacity;

View File

@ -18,6 +18,8 @@ namespace LibAudio
uint32_t sample_rate; uint32_t sample_rate;
uint32_t channels; uint32_t channels;
BAN::Atomic<bool> paused { false };
uint32_t capacity; uint32_t capacity;
BAN::Atomic<uint32_t> tail { 0 }; BAN::Atomic<uint32_t> tail { 0 };
BAN::Atomic<uint32_t> head { 0 }; BAN::Atomic<uint32_t> head { 0 };
@ -29,6 +31,7 @@ namespace LibAudio
BAN_NON_COPYABLE(Audio); BAN_NON_COPYABLE(Audio);
public: public:
static BAN::ErrorOr<Audio> create(uint32_t channels, uint32_t sample_rate, uint32_t sample_frames);
static BAN::ErrorOr<Audio> load(BAN::StringView path); static BAN::ErrorOr<Audio> load(BAN::StringView path);
static BAN::ErrorOr<Audio> random(uint32_t samples); static BAN::ErrorOr<Audio> random(uint32_t samples);
~Audio() { clear(); } ~Audio() { clear(); }
@ -37,9 +40,13 @@ namespace LibAudio
Audio& operator=(Audio&& other); Audio& operator=(Audio&& other);
BAN::ErrorOr<void> start(); BAN::ErrorOr<void> start();
void update();
void set_paused(bool paused);
bool is_playing() const { return m_audio_buffer->tail != m_audio_buffer->head; } bool is_playing() const { return m_audio_buffer->tail != m_audio_buffer->head; }
void update();
size_t queue_samples(BAN::Span<const AudioBuffer::sample_t> samples);
private: private:
Audio() = default; Audio() = default;

View File

@ -40,6 +40,13 @@ bool AudioServer::on_client_packet(int fd, long smo_key)
{ {
auto& audio_buffer = m_audio_buffers[fd]; auto& audio_buffer = m_audio_buffers[fd];
if (smo_key == 0)
{
if (audio_buffer.buffer)
reset_kernel_buffer();
return true;
}
audio_buffer.buffer = static_cast<LibAudio::AudioBuffer*>(smo_map(smo_key)); audio_buffer.buffer = static_cast<LibAudio::AudioBuffer*>(smo_map(smo_key));
audio_buffer.sample_frames_queued = 0; audio_buffer.sample_frames_queued = 0;
if (audio_buffer.buffer == nullptr) if (audio_buffer.buffer == nullptr)
@ -53,8 +60,11 @@ bool AudioServer::on_client_packet(int fd, long smo_key)
return true; return true;
} }
void AudioServer::update() uint64_t AudioServer::update()
{ {
// FIXME: get this from the kernel
static constexpr uint64_t kernel_buffer_ms = 50;
uint32_t kernel_buffer_size; uint32_t kernel_buffer_size;
if (ioctl(m_audio_device_fd, SND_GET_BUFFERSZ, &kernel_buffer_size) == -1) if (ioctl(m_audio_device_fd, SND_GET_BUFFERSZ, &kernel_buffer_size) == -1)
ASSERT_NOT_REACHED(); ASSERT_NOT_REACHED();
@ -72,14 +82,17 @@ void AudioServer::update()
const size_t max_sample_frames = (m_samples.capacity() - m_samples.size()) / m_channels; const size_t max_sample_frames = (m_samples.capacity() - m_samples.size()) / m_channels;
const size_t queued_samples_end = m_samples.size(); const size_t queued_samples_end = m_samples.size();
if (max_sample_frames == 0) if (max_sample_frames == 0)
return; return kernel_buffer_ms;
size_t max_sample_frames_to_queue = max_sample_frames;
bool anyone_playing = false;
for (auto& [_, buffer] : m_audio_buffers) for (auto& [_, buffer] : m_audio_buffers)
{ {
if (buffer.buffer == nullptr) if (buffer.buffer == nullptr)
continue; continue;
const double sample_ratio = buffer.buffer->sample_rate / static_cast<double>(m_sample_rate); const sample_t sample_ratio = buffer.buffer->sample_rate / static_cast<sample_t>(m_sample_rate);
if (buffer.sample_frames_queued) if (buffer.sample_frames_queued)
{ {
@ -91,13 +104,38 @@ void AudioServer::update()
buffer.sample_frames_queued -= buffer_sample_frames_played; buffer.sample_frames_queued -= buffer_sample_frames_played;
} }
if (buffer.buffer->paused)
continue;
anyone_playing = true;
const uint32_t buffer_total_sample_frames = ((buffer.buffer->capacity + buffer.buffer->head - buffer.buffer->tail) % buffer.buffer->capacity) / buffer.buffer->channels;
const uint32_t buffer_sample_frames_available = (buffer_total_sample_frames - buffer.sample_frames_queued) / sample_ratio;
max_sample_frames_to_queue = BAN::Math::min<size_t>(max_sample_frames_to_queue, buffer_sample_frames_available);
}
if (!anyone_playing)
return 60'000;
// FIXME: this works but if any client stops producing audio samples
// the whole audio server halts
const uint32_t samples_per_10ms = m_sample_rate / 100;
if (max_sample_frames_to_queue < samples_per_10ms)
return 1;
for (auto& [_, buffer] : m_audio_buffers)
{
if (buffer.buffer == nullptr || buffer.buffer->paused)
continue;
const sample_t sample_ratio = buffer.buffer->sample_rate / static_cast<sample_t>(m_sample_rate);
const uint32_t buffer_total_sample_frames = ((buffer.buffer->capacity + buffer.buffer->head - buffer.buffer->tail) % buffer.buffer->capacity) / buffer.buffer->channels; const uint32_t buffer_total_sample_frames = ((buffer.buffer->capacity + buffer.buffer->head - buffer.buffer->tail) % buffer.buffer->capacity) / buffer.buffer->channels;
const uint32_t buffer_sample_frames_available = (buffer_total_sample_frames - buffer.sample_frames_queued) / sample_ratio; const uint32_t buffer_sample_frames_available = (buffer_total_sample_frames - buffer.sample_frames_queued) / sample_ratio;
if (buffer_sample_frames_available == 0) if (buffer_sample_frames_available == 0)
continue; continue;
const size_t sample_frames_to_queue = BAN::Math::min<size_t>(max_sample_frames, buffer_sample_frames_available); const size_t sample_frames_to_queue = BAN::Math::min<size_t>(max_sample_frames_to_queue, buffer_sample_frames_available);
if (sample_frames_to_queue == 0) if (sample_frames_to_queue < samples_per_10ms)
continue; continue;
while (m_samples.size() < queued_samples_end + sample_frames_to_queue * m_channels) while (m_samples.size() < queued_samples_end + sample_frames_to_queue * m_channels)
@ -113,10 +151,17 @@ void AudioServer::update()
m_samples[queued_samples_end + i * m_channels + j] += buffer.buffer->samples[(buffer_tail + buffer_frame * buffer.buffer->channels + j) % buffer.buffer->capacity]; m_samples[queued_samples_end + i * m_channels + j] += buffer.buffer->samples[(buffer_tail + buffer_frame * buffer.buffer->channels + j) % buffer.buffer->capacity];
} }
buffer.sample_frames_queued += sample_frames_to_queue * sample_ratio; buffer.sample_frames_queued += BAN::Math::min<uint32_t>(
buffer_total_sample_frames,
BAN::Math::ceil(sample_frames_to_queue * sample_ratio)
);
} }
send_samples(); send_samples();
const double play_ms = 1000.0 * m_samples_sent / m_channels / m_sample_rate;
const uint64_t wake_ms = BAN::Math::max<uint64_t>(play_ms, kernel_buffer_ms) - kernel_buffer_ms;
return wake_ms;
} }
void AudioServer::reset_kernel_buffer() void AudioServer::reset_kernel_buffer()

View File

@ -19,7 +19,7 @@ public:
void on_client_disconnect(int fd); void on_client_disconnect(int fd);
bool on_client_packet(int fd, long smo_key); bool on_client_packet(int fd, long smo_key);
void update(); uint64_t update();
private: private:
struct ClientInfo struct ClientInfo
@ -28,6 +28,8 @@ private:
size_t sample_frames_queued { 0 }; size_t sample_frames_queued { 0 };
}; };
using sample_t = LibAudio::AudioBuffer::sample_t;
private: private:
enum class AddOrRemove { Add, Remove }; enum class AddOrRemove { Add, Remove };
@ -42,7 +44,7 @@ private:
size_t m_samples_sent { 0 }; size_t m_samples_sent { 0 };
BAN::Array<uint8_t, 1024> m_send_buffer; BAN::Array<uint8_t, 1024> m_send_buffer;
BAN::CircularQueue<double, 64 * 1024> m_samples; BAN::CircularQueue<sample_t, 64 * 1024> m_samples;
BAN::HashMap<int, ClientInfo> m_audio_buffers; BAN::HashMap<int, ClientInfo> m_audio_buffers;
}; };

View File

@ -96,8 +96,7 @@ int main()
dprintln("AudioServer started"); dprintln("AudioServer started");
constexpr uint64_t update_interval_ms = 100; uint64_t next_update_ms = get_current_ms();
uint64_t next_update_ms = get_current_ms() + update_interval_ms;
for (;;) for (;;)
{ {
@ -113,11 +112,7 @@ int main()
} }
const uint64_t current_ms = get_current_ms(); const uint64_t current_ms = get_current_ms();
if (current_ms >= next_update_ms) next_update_ms = current_ms + audio_server->update();
{
audio_server->update();
next_update_ms = current_ms + update_interval_ms;
}
const uint64_t timeout_ms = next_update_ms - current_ms; const uint64_t timeout_ms = next_update_ms - current_ms;