Userspace: Add simple AudioServer and LibAudio
This commit is contained in:
200
userspace/programs/AudioServer/AudioServer.cpp
Normal file
200
userspace/programs/AudioServer/AudioServer.cpp
Normal file
@@ -0,0 +1,200 @@
|
||||
#include "AudioServer.h"
|
||||
|
||||
#include <sys/banan-os.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/mman.h>
|
||||
#include <unistd.h>
|
||||
|
||||
AudioServer::AudioServer(int fd)
|
||||
: m_audio_device_fd(fd)
|
||||
{
|
||||
if (ioctl(m_audio_device_fd, SND_GET_CHANNELS, &m_channels) != 0)
|
||||
ASSERT_NOT_REACHED();
|
||||
if (ioctl(m_audio_device_fd, SND_GET_SAMPLE_RATE, &m_sample_rate) != 0)
|
||||
ASSERT_NOT_REACHED();
|
||||
}
|
||||
|
||||
BAN::ErrorOr<void> AudioServer::on_new_client(int fd)
|
||||
{
|
||||
TRY(m_audio_buffers.emplace(fd, nullptr));
|
||||
return {};
|
||||
}
|
||||
|
||||
void AudioServer::on_client_disconnect(int fd)
|
||||
{
|
||||
auto it = m_audio_buffers.find(fd);
|
||||
ASSERT(it != m_audio_buffers.end());
|
||||
|
||||
if (it->value.buffer != nullptr)
|
||||
{
|
||||
const size_t bytes = sizeof(LibAudio::AudioBuffer) + it->value.buffer->capacity * sizeof(LibAudio::AudioBuffer::sample_t);
|
||||
munmap(it->value.buffer, bytes);
|
||||
}
|
||||
|
||||
m_audio_buffers.remove(it);
|
||||
|
||||
reset_kernel_buffer();
|
||||
}
|
||||
|
||||
bool AudioServer::on_client_packet(int fd, long smo_key)
|
||||
{
|
||||
auto& audio_buffer = m_audio_buffers[fd];
|
||||
|
||||
audio_buffer.buffer = static_cast<LibAudio::AudioBuffer*>(smo_map(smo_key));
|
||||
audio_buffer.sample_frames_queued = 0;
|
||||
if (audio_buffer.buffer == nullptr)
|
||||
{
|
||||
dwarnln("Failed to map audio buffer: {}", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
reset_kernel_buffer();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AudioServer::update()
|
||||
{
|
||||
uint32_t kernel_buffer_size;
|
||||
if (ioctl(m_audio_device_fd, SND_GET_BUFFERSZ, &kernel_buffer_size) == -1)
|
||||
ASSERT_NOT_REACHED();
|
||||
|
||||
const size_t kernel_samples = kernel_buffer_size / sizeof(int16_t);
|
||||
ASSERT(kernel_samples <= m_samples_sent);
|
||||
|
||||
const uint32_t samples_played = m_samples_sent - kernel_samples;
|
||||
ASSERT(samples_played % m_channels == 0);
|
||||
|
||||
for (uint32_t i = 0; i < samples_played; i++)
|
||||
m_samples.pop();
|
||||
m_samples_sent -= samples_played;
|
||||
|
||||
const size_t max_sample_frames = (m_samples.capacity() - m_samples.size()) / m_channels;
|
||||
const size_t queued_samples_end = m_samples.size();
|
||||
if (max_sample_frames == 0)
|
||||
return;
|
||||
|
||||
for (auto& [_, buffer] : m_audio_buffers)
|
||||
{
|
||||
if (buffer.buffer == nullptr)
|
||||
continue;
|
||||
|
||||
const double sample_ratio = buffer.buffer->sample_rate / static_cast<double>(m_sample_rate);
|
||||
|
||||
if (buffer.sample_frames_queued)
|
||||
{
|
||||
const uint32_t buffer_sample_frames_played = BAN::Math::min<size_t>(
|
||||
samples_played * sample_ratio / m_channels,
|
||||
buffer.sample_frames_queued
|
||||
);
|
||||
buffer.buffer->tail = (buffer.buffer->tail + buffer_sample_frames_played * buffer.buffer->channels) % buffer.buffer->capacity;
|
||||
buffer.sample_frames_queued -= buffer_sample_frames_played;
|
||||
}
|
||||
|
||||
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;
|
||||
if (buffer_sample_frames_available == 0)
|
||||
continue;
|
||||
|
||||
const size_t sample_frames_to_queue = BAN::Math::min<size_t>(max_sample_frames, buffer_sample_frames_available);
|
||||
if (sample_frames_to_queue == 0)
|
||||
continue;
|
||||
|
||||
while (m_samples.size() < queued_samples_end + sample_frames_to_queue * m_channels)
|
||||
m_samples.push(0.0);
|
||||
|
||||
const size_t min_channels = BAN::Math::min(m_channels, buffer.buffer->channels);
|
||||
|
||||
const size_t buffer_tail = buffer.buffer->tail + buffer.sample_frames_queued * buffer.buffer->channels;
|
||||
for (size_t i = 0; i < sample_frames_to_queue; i++)
|
||||
{
|
||||
const size_t buffer_frame = i * sample_ratio;
|
||||
for (size_t j = 0; j < min_channels; j++)
|
||||
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;
|
||||
}
|
||||
|
||||
send_samples();
|
||||
}
|
||||
|
||||
void AudioServer::reset_kernel_buffer()
|
||||
{
|
||||
uint32_t kernel_buffer_size;
|
||||
if (ioctl(m_audio_device_fd, SND_RESET_BUFFER, &kernel_buffer_size) != 0)
|
||||
ASSERT_NOT_REACHED();
|
||||
|
||||
const size_t kernel_samples = kernel_buffer_size / sizeof(int16_t);
|
||||
ASSERT(kernel_samples <= m_samples_sent);
|
||||
|
||||
const uint32_t samples_played = m_samples_sent - kernel_samples;
|
||||
ASSERT(samples_played % m_channels == 0);
|
||||
|
||||
m_samples_sent = 0;
|
||||
m_samples.clear();
|
||||
|
||||
for (auto& [_, buffer] : m_audio_buffers)
|
||||
{
|
||||
if (buffer.buffer == nullptr || buffer.sample_frames_queued == 0)
|
||||
continue;
|
||||
const uint32_t buffer_sample_frames_played = BAN::Math::min<size_t>(
|
||||
samples_played / m_channels,
|
||||
buffer.sample_frames_queued
|
||||
);
|
||||
buffer.buffer->tail = (buffer.buffer->tail + buffer_sample_frames_played * buffer.buffer->channels) % buffer.buffer->capacity;
|
||||
buffer.sample_frames_queued = 0;
|
||||
}
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
void AudioServer::send_samples()
|
||||
{
|
||||
// FIXME: don't assume kernel uses 16 bit PCM
|
||||
using kernel_sample_t = int16_t;
|
||||
|
||||
if (m_samples_sent >= m_samples.size())
|
||||
return;
|
||||
|
||||
while (m_samples_sent < m_samples.size())
|
||||
{
|
||||
const size_t samples_to_send = BAN::Math::min(m_send_buffer.size() / sizeof(kernel_sample_t), m_samples.size() - m_samples_sent);
|
||||
|
||||
auto buffer = BAN::ByteSpan(m_send_buffer.data(), samples_to_send * sizeof(kernel_sample_t));
|
||||
|
||||
{
|
||||
auto sample_buffer = buffer.as_span<kernel_sample_t>();
|
||||
for (size_t i = 0; i < samples_to_send; i++)
|
||||
{
|
||||
sample_buffer[i] = BAN::Math::clamp<double>(
|
||||
m_samples[m_samples_sent + i] * BAN::numeric_limits<kernel_sample_t>::max(),
|
||||
BAN::numeric_limits<kernel_sample_t>::min(),
|
||||
BAN::numeric_limits<kernel_sample_t>::max()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
size_t nwritten = 0;
|
||||
while (nwritten < buffer.size())
|
||||
{
|
||||
const ssize_t nwrite = write(
|
||||
m_audio_device_fd,
|
||||
buffer.data() + nwritten,
|
||||
buffer.size() - nwritten
|
||||
);
|
||||
if (nwrite == -1)
|
||||
{
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||
dwarnln("write: {}", strerror(errno));
|
||||
break;
|
||||
}
|
||||
nwritten += nwrite;
|
||||
}
|
||||
|
||||
m_samples_sent += nwritten / sizeof(kernel_sample_t);
|
||||
|
||||
if (nwritten < buffer.size())
|
||||
break;
|
||||
}
|
||||
}
|
||||
48
userspace/programs/AudioServer/AudioServer.h
Normal file
48
userspace/programs/AudioServer/AudioServer.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <BAN/Array.h>
|
||||
#include <BAN/ByteSpan.h>
|
||||
#include <BAN/CircularQueue.h>
|
||||
#include <BAN/HashMap.h>
|
||||
|
||||
#include <LibAudio/Audio.h>
|
||||
|
||||
class AudioServer
|
||||
{
|
||||
BAN_NON_MOVABLE(AudioServer);
|
||||
BAN_NON_COPYABLE(AudioServer);
|
||||
|
||||
public:
|
||||
AudioServer(int audio_device_fd);
|
||||
|
||||
BAN::ErrorOr<void> on_new_client(int fd);
|
||||
void on_client_disconnect(int fd);
|
||||
bool on_client_packet(int fd, long smo_key);
|
||||
|
||||
void update();
|
||||
|
||||
private:
|
||||
struct ClientInfo
|
||||
{
|
||||
LibAudio::AudioBuffer* buffer;
|
||||
size_t sample_frames_queued { 0 };
|
||||
};
|
||||
|
||||
private:
|
||||
enum class AddOrRemove { Add, Remove };
|
||||
|
||||
void reset_kernel_buffer();
|
||||
|
||||
void send_samples();
|
||||
|
||||
private:
|
||||
const int m_audio_device_fd;
|
||||
uint32_t m_sample_rate;
|
||||
uint32_t m_channels;
|
||||
|
||||
size_t m_samples_sent { 0 };
|
||||
BAN::Array<uint8_t, 1024> m_send_buffer;
|
||||
BAN::CircularQueue<double, 64 * 1024> m_samples;
|
||||
|
||||
BAN::HashMap<int, ClientInfo> m_audio_buffers;
|
||||
};
|
||||
11
userspace/programs/AudioServer/CMakeLists.txt
Normal file
11
userspace/programs/AudioServer/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
set(SOURCES
|
||||
main.cpp
|
||||
AudioServer.cpp
|
||||
)
|
||||
|
||||
add_executable(AudioServer ${SOURCES})
|
||||
banan_link_library(AudioServer ban)
|
||||
banan_link_library(AudioServer libc)
|
||||
banan_link_library(AudioServer libaudio)
|
||||
|
||||
install(TARGETS AudioServer OPTIONAL)
|
||||
184
userspace/programs/AudioServer/main.cpp
Normal file
184
userspace/programs/AudioServer/main.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
#include "AudioServer.h"
|
||||
|
||||
#include <LibAudio/Audio.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/select.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int open_server_fd()
|
||||
{
|
||||
struct stat st;
|
||||
if (stat(LibAudio::s_audio_server_socket.data(), &st) != -1)
|
||||
unlink(LibAudio::s_audio_server_socket.data());
|
||||
|
||||
int server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (server_fd == -1)
|
||||
{
|
||||
dwarnln("failed to create server socket: {}", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
sockaddr_un server_addr;
|
||||
server_addr.sun_family = AF_UNIX;
|
||||
strcpy(server_addr.sun_path, LibAudio::s_audio_server_socket.data());
|
||||
if (bind(server_fd, (sockaddr*)&server_addr, sizeof(server_addr)) == -1)
|
||||
{
|
||||
dwarnln("failed to bind server socket: {}", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (chmod(LibAudio::s_audio_server_socket.data(), 0777) == -1)
|
||||
{
|
||||
dwarnln("failed to set server socket permissions: {}", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (listen(server_fd, SOMAXCONN) == -1)
|
||||
{
|
||||
dwarnln("failed to make server socket listening: {}", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
return server_fd;
|
||||
}
|
||||
|
||||
static uint64_t get_current_ms()
|
||||
{
|
||||
timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
constexpr int non_terminating_signals[] {
|
||||
SIGCONT,
|
||||
SIGSTOP,
|
||||
SIGTSTP,
|
||||
SIGTTIN,
|
||||
SIGTTOU,
|
||||
};
|
||||
for (int sig = _SIGMIN; sig <= _SIGMAX; sig++)
|
||||
signal(sig, exit);
|
||||
for (int sig : non_terminating_signals)
|
||||
signal(sig, SIG_DFL);
|
||||
|
||||
const int audio_device_fd = open("/dev/audio0", O_RDWR | O_NONBLOCK);
|
||||
if (audio_device_fd == -1)
|
||||
{
|
||||
dwarnln("failed to open audio device: {}", strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto* audio_server = new AudioServer(audio_device_fd);
|
||||
if (audio_server == nullptr)
|
||||
{
|
||||
dwarnln("Failed to allocate AudioServer: {}", strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int server_fd = open_server_fd();
|
||||
if (server_fd == -1)
|
||||
return 1;
|
||||
|
||||
struct ClientInfo
|
||||
{
|
||||
int fd;
|
||||
};
|
||||
|
||||
BAN::Vector<ClientInfo> clients;
|
||||
|
||||
dprintln("AudioServer started");
|
||||
|
||||
constexpr uint64_t update_interval_ms = 100;
|
||||
uint64_t next_update_ms = get_current_ms() + update_interval_ms;
|
||||
|
||||
for (;;)
|
||||
{
|
||||
fd_set fds;
|
||||
FD_ZERO(&fds);
|
||||
FD_SET(server_fd, &fds);
|
||||
|
||||
int max_fd = server_fd;
|
||||
for (const auto& client : clients)
|
||||
{
|
||||
max_fd = BAN::Math::max(max_fd, client.fd);
|
||||
FD_SET(client.fd, &fds);
|
||||
}
|
||||
|
||||
const uint64_t current_ms = get_current_ms();
|
||||
if (current_ms >= next_update_ms)
|
||||
{
|
||||
audio_server->update();
|
||||
next_update_ms = current_ms + update_interval_ms;
|
||||
}
|
||||
|
||||
const uint64_t timeout_ms = next_update_ms - current_ms;
|
||||
|
||||
timeval timeout {
|
||||
.tv_sec = static_cast<time_t>(timeout_ms / 1000),
|
||||
.tv_usec = static_cast<suseconds_t>((timeout_ms % 1000) * 1000)
|
||||
};
|
||||
if (select(max_fd + 1, &fds, nullptr, nullptr, &timeout) == -1)
|
||||
{
|
||||
dwarnln("select: {}", strerror(errno));
|
||||
break;
|
||||
}
|
||||
|
||||
if (FD_ISSET(server_fd, &fds))
|
||||
{
|
||||
const int client_fd = accept(server_fd, nullptr, nullptr);
|
||||
if (client_fd == -1)
|
||||
{
|
||||
dwarnln("accept: {}", strerror(errno));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto ret = clients.emplace_back(client_fd); ret.is_error())
|
||||
{
|
||||
dwarnln("Failed to add client: {}", ret.error());
|
||||
close(client_fd);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (auto ret = audio_server->on_new_client(client_fd); ret.is_error())
|
||||
{
|
||||
dwarnln("Failed to initialize client: {}", ret.error());
|
||||
clients.pop_back();
|
||||
close(client_fd);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < clients.size(); i++)
|
||||
{
|
||||
auto& client = clients[i];
|
||||
if (!FD_ISSET(client.fd, &fds))
|
||||
continue;
|
||||
|
||||
long smo_key;
|
||||
const ssize_t nrecv = recv(client.fd, &smo_key, sizeof(smo_key), 0);
|
||||
|
||||
if (nrecv < static_cast<ssize_t>(sizeof(smo_key)) || !audio_server->on_client_packet(client.fd, smo_key))
|
||||
{
|
||||
if (nrecv == 0)
|
||||
;
|
||||
else if (nrecv < 0)
|
||||
dwarnln("recv: {}", strerror(errno));
|
||||
else if (nrecv < static_cast<ssize_t>(sizeof(smo_key)))
|
||||
dwarnln("client sent only {} bytes, {} expected", nrecv, sizeof(smo_key));
|
||||
|
||||
audio_server->on_client_disconnect(client.fd);
|
||||
close(client.fd);
|
||||
clients.remove(i--);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
set(USERSPACE_PROGRAMS
|
||||
AudioServer
|
||||
bananfetch
|
||||
basename
|
||||
cat
|
||||
|
||||
Reference in New Issue
Block a user