Userspace: Add simple AudioServer and LibAudio
This commit is contained in:
parent
85d195212a
commit
3aea2c007d
|
@ -1,4 +1,5 @@
|
||||||
set(USERSPACE_LIBRARIES
|
set(USERSPACE_LIBRARIES
|
||||||
|
LibAudio
|
||||||
LibC
|
LibC
|
||||||
LibELF
|
LibELF
|
||||||
LibFont
|
LibFont
|
||||||
|
|
|
@ -0,0 +1,136 @@
|
||||||
|
#include <BAN/ScopeGuard.h>
|
||||||
|
|
||||||
|
#include <LibAudio/Audio.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <sys/banan-os.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
|
||||||
|
namespace LibAudio
|
||||||
|
{
|
||||||
|
|
||||||
|
BAN::ErrorOr<Audio> Audio::load(BAN::StringView path)
|
||||||
|
{
|
||||||
|
Audio result(TRY(AudioLoader::load(path)));
|
||||||
|
TRY(result.initialize(256 * 1024));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
BAN::ErrorOr<Audio> Audio::random(uint32_t samples)
|
||||||
|
{
|
||||||
|
Audio result;
|
||||||
|
TRY(result.initialize(samples));
|
||||||
|
|
||||||
|
result.m_audio_buffer->sample_rate = 48000;
|
||||||
|
result.m_audio_buffer->channels = 1;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < samples - 1; i++)
|
||||||
|
result.m_audio_buffer->samples[i] = (rand() - RAND_MAX / 2) / (RAND_MAX / 2.0);
|
||||||
|
result.m_audio_buffer->head = samples - 1;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::clear()
|
||||||
|
{
|
||||||
|
if (m_audio_buffer)
|
||||||
|
munmap(m_audio_buffer, m_smo_size);
|
||||||
|
m_audio_buffer = nullptr;
|
||||||
|
|
||||||
|
if (m_smo_key != -1)
|
||||||
|
smo_delete(m_smo_key);
|
||||||
|
m_smo_key = -1;
|
||||||
|
|
||||||
|
if (m_server_fd != -1)
|
||||||
|
close(m_server_fd);
|
||||||
|
m_server_fd = -1;
|
||||||
|
|
||||||
|
m_audio_loader.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
Audio& Audio::operator=(Audio&& other)
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
|
||||||
|
m_server_fd = other.m_server_fd;
|
||||||
|
m_smo_key = other.m_smo_key;
|
||||||
|
m_smo_size = other.m_smo_size;
|
||||||
|
m_audio_buffer = other.m_audio_buffer;
|
||||||
|
m_audio_loader = BAN::move(other.m_audio_loader);
|
||||||
|
|
||||||
|
other.m_server_fd = -1;
|
||||||
|
other.m_smo_key = -1;
|
||||||
|
other.m_smo_size = 0;
|
||||||
|
other.m_audio_buffer = nullptr;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
m_smo_size = sizeof(AudioBuffer) + total_samples * sizeof(AudioBuffer::sample_t);
|
||||||
|
|
||||||
|
m_smo_key = smo_create(m_smo_size, PROT_READ | PROT_WRITE);
|
||||||
|
if (m_smo_key == -1)
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
|
||||||
|
m_audio_buffer = static_cast<AudioBuffer*>(smo_map(m_smo_key));
|
||||||
|
if (m_audio_buffer == nullptr)
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
new (m_audio_buffer) AudioBuffer();
|
||||||
|
memset(m_audio_buffer->samples, 0, total_samples * sizeof(AudioBuffer::sample_t));
|
||||||
|
|
||||||
|
m_audio_buffer->capacity = total_samples;
|
||||||
|
if (m_audio_loader)
|
||||||
|
{
|
||||||
|
m_audio_buffer->channels = m_audio_loader->channels();
|
||||||
|
m_audio_buffer->sample_rate = m_audio_loader->sample_rate();
|
||||||
|
}
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
sockaddr_un server_addr;
|
||||||
|
server_addr.sun_family = AF_UNIX;
|
||||||
|
strcpy(server_addr.sun_path, s_audio_server_socket.data());
|
||||||
|
|
||||||
|
m_server_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0);
|
||||||
|
if (m_server_fd == -1)
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
|
||||||
|
if (connect(m_server_fd, reinterpret_cast<sockaddr*>(&server_addr), sizeof(server_addr)) == -1)
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void Audio::update()
|
||||||
|
{
|
||||||
|
if (!m_audio_loader)
|
||||||
|
return;
|
||||||
|
|
||||||
|
while (m_audio_loader->samples_remaining())
|
||||||
|
{
|
||||||
|
const uint32_t next_head = (m_audio_buffer->head + 1) % m_audio_buffer->capacity;
|
||||||
|
if (next_head == m_audio_buffer->tail)
|
||||||
|
break;
|
||||||
|
m_audio_buffer->samples[m_audio_buffer->head] = m_audio_loader->get_sample();
|
||||||
|
m_audio_buffer->head = next_head;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
#include <LibAudio/AudioLoader.h>
|
||||||
|
#include <LibAudio/AudioLoaders/WAVLoader.h>
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace LibAudio
|
||||||
|
{
|
||||||
|
|
||||||
|
BAN::ErrorOr<BAN::UniqPtr<AudioLoader>> AudioLoader::load(BAN::StringView path)
|
||||||
|
{
|
||||||
|
if (path.empty())
|
||||||
|
return BAN::Error::from_errno(ENOENT);
|
||||||
|
|
||||||
|
const bool malloced = (path.data()[path.size()] != '\0');
|
||||||
|
|
||||||
|
const char* path_cstr = path.data();
|
||||||
|
if (malloced && (path_cstr = strndup(path.data(), path.size())) == nullptr)
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
const int file_fd = open(path_cstr, O_RDONLY);
|
||||||
|
if (malloced)
|
||||||
|
free(const_cast<char*>(path_cstr));
|
||||||
|
if (file_fd == -1)
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (fstat(file_fd, &st) == -1)
|
||||||
|
{
|
||||||
|
close(file_fd);
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* mmap_addr = static_cast<uint8_t*>(mmap(nullptr, st.st_size, PROT_READ, MAP_PRIVATE, file_fd, 0));
|
||||||
|
close(file_fd);
|
||||||
|
if (mmap_addr == MAP_FAILED)
|
||||||
|
return BAN::Error::from_errno(errno);
|
||||||
|
|
||||||
|
BAN::ErrorOr<BAN::UniqPtr<AudioLoader>> result_or_error { BAN::Error::from_errno(ENOTSUP) };
|
||||||
|
|
||||||
|
auto file_span = BAN::ConstByteSpan(mmap_addr, st.st_size);
|
||||||
|
if (WAVAudioLoader::can_load_from(file_span))
|
||||||
|
result_or_error = WAVAudioLoader::create(file_span);
|
||||||
|
|
||||||
|
if (result_or_error.is_error())
|
||||||
|
{
|
||||||
|
munmap(mmap_addr, st.st_size);
|
||||||
|
return result_or_error.release_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto result = result_or_error.release_value();
|
||||||
|
result->m_mmap_addr = mmap_addr;
|
||||||
|
result->m_mmap_size = st.st_size;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioLoader::~AudioLoader()
|
||||||
|
{
|
||||||
|
if (m_mmap_addr)
|
||||||
|
munmap(m_mmap_addr, m_mmap_size);
|
||||||
|
m_mmap_addr = nullptr;
|
||||||
|
m_mmap_size = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
#include <LibAudio/AudioLoaders/WAVLoader.h>
|
||||||
|
|
||||||
|
namespace LibAudio
|
||||||
|
{
|
||||||
|
|
||||||
|
struct WAVChunk
|
||||||
|
{
|
||||||
|
char chunk_id[4];
|
||||||
|
uint32_t chunk_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RIFFChunk : WAVChunk
|
||||||
|
{
|
||||||
|
char wave_id[4];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FormatChunk : WAVChunk
|
||||||
|
{
|
||||||
|
uint16_t wFormatTag;
|
||||||
|
uint16_t nChannels;
|
||||||
|
uint32_t nSamplePerSec;
|
||||||
|
uint32_t nAvgBytePerSec;
|
||||||
|
uint16_t nBlockAlign;
|
||||||
|
uint16_t wBitsPerSample;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool WAVAudioLoader::can_load_from(BAN::ConstByteSpan data)
|
||||||
|
{
|
||||||
|
if (data.size() < sizeof(RIFFChunk))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const auto riff_chunk = data.as<const RIFFChunk>();
|
||||||
|
if (memcmp(riff_chunk.chunk_id, "RIFF", 4) != 0)
|
||||||
|
return false;
|
||||||
|
if (memcmp(riff_chunk.wave_id, "WAVE", 4) != 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
BAN::ErrorOr<BAN::UniqPtr<AudioLoader>> WAVAudioLoader::create(BAN::ConstByteSpan data)
|
||||||
|
{
|
||||||
|
ASSERT(can_load_from(data));
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto riff_chunk = data.as<const RIFFChunk>();
|
||||||
|
if (sizeof(WAVChunk) + riff_chunk.chunk_size > data.size())
|
||||||
|
return BAN::Error::from_errno(ENOBUFS);
|
||||||
|
data = data.slice(0, sizeof(WAVChunk) + riff_chunk.chunk_size);
|
||||||
|
data = data.slice(sizeof(RIFFChunk));
|
||||||
|
}
|
||||||
|
|
||||||
|
BAN::Optional<FormatChunk> format_chunk;
|
||||||
|
BAN::ConstByteSpan sample_data;
|
||||||
|
|
||||||
|
while (!data.empty())
|
||||||
|
{
|
||||||
|
const auto chunk = data.as<const WAVChunk>();
|
||||||
|
if (data.size() < sizeof(WAVChunk) + chunk.chunk_size)
|
||||||
|
return BAN::Error::from_errno(ENOBUFS);
|
||||||
|
|
||||||
|
if (memcmp(chunk.chunk_id, "fmt ", 4) == 0)
|
||||||
|
format_chunk = data.as<const FormatChunk>();
|
||||||
|
else if (memcmp(chunk.chunk_id, "data", 4) == 0)
|
||||||
|
sample_data = data.slice(sizeof(WAVChunk), chunk.chunk_size);
|
||||||
|
|
||||||
|
data = data.slice(sizeof(WAVChunk) + chunk.chunk_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!format_chunk.has_value() || sample_data.empty())
|
||||||
|
return BAN::Error::from_errno(EINVAL);
|
||||||
|
|
||||||
|
const auto format = static_cast<FormatCode>(format_chunk->wFormatTag);
|
||||||
|
const uint16_t bps = format_chunk->wBitsPerSample;
|
||||||
|
const uint16_t channels = format_chunk->nChannels;
|
||||||
|
|
||||||
|
if (channels == 0)
|
||||||
|
return BAN::Error::from_errno(EINVAL);
|
||||||
|
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case FormatCode::WAVE_FORMAT_PCM:
|
||||||
|
if (bps != 8 && bps != 16 && bps != 32)
|
||||||
|
return BAN::Error::from_errno(ENOTSUP);
|
||||||
|
break;
|
||||||
|
case FormatCode::WAVE_FORMAT_IEEE_FLOAT:
|
||||||
|
if (bps != 32 && bps != 64)
|
||||||
|
return BAN::Error::from_errno(ENOTSUP);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return BAN::Error::from_errno(ENOTSUP);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bps / 8 * channels != format_chunk->nBlockAlign)
|
||||||
|
return BAN::Error::from_errno(EINVAL);
|
||||||
|
|
||||||
|
auto loader = TRY(BAN::UniqPtr<WAVAudioLoader>::create());
|
||||||
|
loader->m_bits_per_sample = bps;
|
||||||
|
loader->m_sample_format = format;
|
||||||
|
loader->m_channels = channels;
|
||||||
|
loader->m_sample_rate = format_chunk->nSamplePerSec;
|
||||||
|
loader->m_sample_data = sample_data;
|
||||||
|
loader->m_total_samples = sample_data.size() / (bps / 8);
|
||||||
|
loader->m_current_sample = 0;
|
||||||
|
return BAN::UniqPtr<AudioLoader>(BAN::move(loader));
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static double read_sample(BAN::ConstByteSpan data)
|
||||||
|
{
|
||||||
|
if constexpr(BAN::is_same_v<float, T> || BAN::is_same_v<double, T>)
|
||||||
|
return data.as<const T>();
|
||||||
|
else if constexpr(BAN::is_signed_v<T>)
|
||||||
|
return data.as<const T>() / static_cast<double>(BAN::numeric_limits<T>::max());
|
||||||
|
else
|
||||||
|
return data.as<const T>() / (BAN::numeric_limits<T>::max() / 2.0) - 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double WAVAudioLoader::get_sample()
|
||||||
|
{
|
||||||
|
ASSERT(samples_remaining() > 0);
|
||||||
|
|
||||||
|
const auto current_sample_data = m_sample_data.slice((m_bits_per_sample / 8) * m_current_sample++);
|
||||||
|
|
||||||
|
switch (m_sample_format)
|
||||||
|
{
|
||||||
|
case FormatCode::WAVE_FORMAT_PCM:
|
||||||
|
switch (m_bits_per_sample)
|
||||||
|
{
|
||||||
|
case 8: return read_sample<uint8_t>(current_sample_data);
|
||||||
|
case 16: return read_sample<int16_t>(current_sample_data);
|
||||||
|
case 32: return read_sample<int32_t>(current_sample_data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case FormatCode::WAVE_FORMAT_IEEE_FLOAT:
|
||||||
|
switch (m_bits_per_sample)
|
||||||
|
{
|
||||||
|
case 32: return read_sample<float>(current_sample_data);
|
||||||
|
case 64: return read_sample<double>(current_sample_data);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSERT_NOT_REACHED();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
set(LIBAUDIO_SOURCES
|
||||||
|
Audio.cpp
|
||||||
|
AudioLoader.cpp
|
||||||
|
AudioLoaders/WAVLoader.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
add_library(libaudio ${LIBAUDIO_SOURCES})
|
||||||
|
banan_link_library(libaudio ban)
|
||||||
|
banan_link_library(libaudio libc)
|
||||||
|
|
||||||
|
banan_install_headers(libaudio)
|
||||||
|
install(TARGETS libaudio OPTIONAL)
|
|
@ -0,0 +1,64 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <BAN/Atomic.h>
|
||||||
|
#include <BAN/StringView.h>
|
||||||
|
#include <BAN/Vector.h>
|
||||||
|
|
||||||
|
#include <LibAudio/AudioLoader.h>
|
||||||
|
|
||||||
|
namespace LibAudio
|
||||||
|
{
|
||||||
|
|
||||||
|
static constexpr BAN::StringView s_audio_server_socket = "/tmp/audio-server.socket"_sv;
|
||||||
|
|
||||||
|
struct AudioBuffer
|
||||||
|
{
|
||||||
|
using sample_t = double;
|
||||||
|
|
||||||
|
uint32_t sample_rate;
|
||||||
|
uint32_t channels;
|
||||||
|
|
||||||
|
uint32_t capacity;
|
||||||
|
BAN::Atomic<uint32_t> tail { 0 };
|
||||||
|
BAN::Atomic<uint32_t> head { 0 };
|
||||||
|
sample_t samples[/* capacity */];
|
||||||
|
};
|
||||||
|
|
||||||
|
class Audio
|
||||||
|
{
|
||||||
|
BAN_NON_COPYABLE(Audio);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static BAN::ErrorOr<Audio> load(BAN::StringView path);
|
||||||
|
static BAN::ErrorOr<Audio> random(uint32_t samples);
|
||||||
|
~Audio() { clear(); }
|
||||||
|
|
||||||
|
Audio(Audio&& other) { *this = BAN::move(other); }
|
||||||
|
Audio& operator=(Audio&& other);
|
||||||
|
|
||||||
|
BAN::ErrorOr<void> start();
|
||||||
|
|
||||||
|
bool is_playing() const { return m_audio_buffer->tail != m_audio_buffer->head; }
|
||||||
|
void update();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Audio() = default;
|
||||||
|
Audio(BAN::UniqPtr<AudioLoader>&& audio_loader)
|
||||||
|
: m_audio_loader(BAN::move(audio_loader))
|
||||||
|
{ }
|
||||||
|
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
BAN::ErrorOr<void> initialize(uint32_t total_samples);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int m_server_fd { -1 };
|
||||||
|
|
||||||
|
BAN::UniqPtr<AudioLoader> m_audio_loader;
|
||||||
|
|
||||||
|
long m_smo_key { -1 };
|
||||||
|
size_t m_smo_size { 0 };
|
||||||
|
AudioBuffer* m_audio_buffer { nullptr };
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <BAN/StringView.h>
|
||||||
|
#include <BAN/UniqPtr.h>
|
||||||
|
|
||||||
|
namespace LibAudio
|
||||||
|
{
|
||||||
|
|
||||||
|
class AudioLoader
|
||||||
|
{
|
||||||
|
BAN_NON_COPYABLE(AudioLoader);
|
||||||
|
BAN_NON_MOVABLE(AudioLoader);
|
||||||
|
|
||||||
|
public:
|
||||||
|
static BAN::ErrorOr<BAN::UniqPtr<AudioLoader>> load(BAN::StringView path);
|
||||||
|
virtual ~AudioLoader();
|
||||||
|
|
||||||
|
virtual uint32_t channels() const = 0;
|
||||||
|
virtual uint32_t sample_rate() const = 0;
|
||||||
|
virtual uint32_t samples_remaining() const = 0;
|
||||||
|
|
||||||
|
virtual double get_sample() = 0;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AudioLoader() = default;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void* m_mmap_addr { nullptr };
|
||||||
|
size_t m_mmap_size { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <LibAudio/AudioLoader.h>
|
||||||
|
|
||||||
|
#include <BAN/ByteSpan.h>
|
||||||
|
|
||||||
|
namespace LibAudio
|
||||||
|
{
|
||||||
|
|
||||||
|
class WAVAudioLoader : public AudioLoader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum FormatCode : uint16_t
|
||||||
|
{
|
||||||
|
WAVE_FORMAT_PCM = 0x01,
|
||||||
|
WAVE_FORMAT_IEEE_FLOAT = 0x03,
|
||||||
|
};
|
||||||
|
|
||||||
|
public:
|
||||||
|
static bool can_load_from(BAN::ConstByteSpan data);
|
||||||
|
static BAN::ErrorOr<BAN::UniqPtr<AudioLoader>> create(BAN::ConstByteSpan data);
|
||||||
|
|
||||||
|
uint32_t channels() const override { return m_channels; }
|
||||||
|
uint32_t sample_rate() const override { return m_sample_rate; }
|
||||||
|
uint32_t samples_remaining() const override { return m_total_samples - m_current_sample; }
|
||||||
|
|
||||||
|
double get_sample() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
uint32_t m_channels { 0 };
|
||||||
|
uint32_t m_sample_rate { 0 };
|
||||||
|
uint32_t m_total_samples { 0 };
|
||||||
|
|
||||||
|
FormatCode m_sample_format;
|
||||||
|
uint16_t m_bits_per_sample { 0 };
|
||||||
|
size_t m_current_sample { 0 };
|
||||||
|
BAN::ConstByteSpan m_sample_data;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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)
|
|
@ -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
|
set(USERSPACE_PROGRAMS
|
||||||
|
AudioServer
|
||||||
bananfetch
|
bananfetch
|
||||||
basename
|
basename
|
||||||
cat
|
cat
|
||||||
|
|
Loading…
Reference in New Issue