LibAudio/AudioServer: Add support for playing real time audio
This commit is contained in:
parent
7ad3f967db
commit
7a5cfe1728
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue