mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-01-05 01:55:21 +03:00
Audio: Make basic streaming WAV playback work.
I had to solve a bunch of things simultaneously to make this work. Refactor AWavLoader to be a streaming loader rather than a one-shot one. The constructor parses the header, and if everything looks good, you can repeatedly ask the AWavLoader for sample buffers until it runs out. Also send a message from AudioServer when a buffer has finished playing. That allows us to implement a blocking variant of play(). Use all of this in aplay to play WAV files chunk-at-a-time. This is definitely not perfect and it's a little glitchy and skippy, but I think it's a step in the right direction.
This commit is contained in:
parent
a292d8cd5a
commit
426248098c
Notes:
sideshowbarker
2024-07-19 13:01:38 +09:00
Author: https://github.com/awesomekling Commit: https://github.com/SerenityOS/serenity/commit/426248098c1
@ -53,9 +53,10 @@ struct ASample {
|
||||
class ABuffer : public RefCounted<ABuffer> {
|
||||
public:
|
||||
static RefPtr<ABuffer> from_pcm_data(ByteBuffer& data, int num_channels, int bits_per_sample, int source_rate);
|
||||
ABuffer(Vector<ASample>&& samples)
|
||||
: m_samples(move(samples))
|
||||
{}
|
||||
static NonnullRefPtr<ABuffer> create_with_samples(Vector<ASample>&& samples)
|
||||
{
|
||||
return adopt(*new ABuffer(move(samples)));
|
||||
}
|
||||
|
||||
const Vector<ASample>& samples() const { return m_samples; }
|
||||
Vector<ASample>& samples() { return m_samples; }
|
||||
@ -63,6 +64,10 @@ public:
|
||||
int size_in_bytes() const { return m_samples.size() * sizeof(ASample); }
|
||||
|
||||
private:
|
||||
ABuffer(Vector<ASample>&& samples)
|
||||
: m_samples(move(samples))
|
||||
{
|
||||
}
|
||||
|
||||
Vector<ASample> m_samples;
|
||||
};
|
||||
|
||||
|
@ -17,7 +17,7 @@ void AClientConnection::handshake()
|
||||
set_my_client_id(response.greeting.your_client_id);
|
||||
}
|
||||
|
||||
void AClientConnection::play(const ABuffer& buffer)
|
||||
void AClientConnection::play(const ABuffer& buffer, bool block)
|
||||
{
|
||||
auto shared_buf = SharedBuffer::create_with_size(buffer.size_in_bytes());
|
||||
if (!shared_buf) {
|
||||
@ -31,5 +31,5 @@ void AClientConnection::play(const ABuffer& buffer)
|
||||
ASAPI_ClientMessage request;
|
||||
request.type = ASAPI_ClientMessage::Type::PlayBuffer;
|
||||
request.play_buffer.buffer_id = shared_buf->shared_buffer_id();
|
||||
sync_request(request, ASAPI_ServerMessage::Type::PlayingBuffer);
|
||||
sync_request(request, block ? ASAPI_ServerMessage::Type::FinishedPlayingBuffer : ASAPI_ServerMessage::Type::PlayingBuffer);
|
||||
}
|
||||
|
@ -11,5 +11,5 @@ public:
|
||||
AClientConnection();
|
||||
|
||||
virtual void handshake() override;
|
||||
void play(const ABuffer&);
|
||||
void play(const ABuffer&, bool block);
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ struct ASAPI_ServerMessage {
|
||||
Invalid,
|
||||
Greeting,
|
||||
PlayingBuffer,
|
||||
FinishedPlayingBuffer,
|
||||
};
|
||||
|
||||
Type type { Type::Invalid };
|
||||
|
@ -2,26 +2,32 @@
|
||||
#include <LibAudio/ABuffer.h>
|
||||
#include <LibAudio/AWavLoader.h>
|
||||
#include <LibCore/CFile.h>
|
||||
#include <LibCore/CFileStreamReader.h>
|
||||
#include <limits>
|
||||
|
||||
RefPtr<ABuffer> AWavLoader::load_wav(const StringView& path)
|
||||
AWavLoader::AWavLoader(const StringView& path)
|
||||
: m_file(path)
|
||||
{
|
||||
m_error_string = {};
|
||||
|
||||
CFile wav(path);
|
||||
if (!wav.open(CIODevice::ReadOnly)) {
|
||||
m_error_string = String::format("Can't open file: %s", wav.error_string());
|
||||
return nullptr;
|
||||
if (!m_file.open(CIODevice::ReadOnly)) {
|
||||
m_error_string = String::format("Can't open file: %s", m_file.error_string());
|
||||
return;
|
||||
}
|
||||
|
||||
auto contents = wav.read_all();
|
||||
return parse_wav(contents);
|
||||
parse_header();
|
||||
}
|
||||
|
||||
// TODO: A streaming parser might be better than forcing a ByteBuffer
|
||||
RefPtr<ABuffer> AWavLoader::parse_wav(ByteBuffer& buffer)
|
||||
RefPtr<ABuffer> AWavLoader::get_more_samples()
|
||||
{
|
||||
BufferStream stream(buffer);
|
||||
dbgprintf("Read WAV of format PCM with num_channels %u sample rate %u, bits per sample %u\n", m_num_channels, m_sample_rate, m_bits_per_sample);
|
||||
|
||||
auto raw_samples = m_file.read(128 * KB);
|
||||
auto buffer = ABuffer::from_pcm_data(raw_samples, m_num_channels, m_bits_per_sample, m_sample_rate);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool AWavLoader::parse_header()
|
||||
{
|
||||
CFileStreamReader stream(m_file);
|
||||
|
||||
#define CHECK_OK(msg) \
|
||||
do { \
|
||||
@ -73,13 +79,11 @@ RefPtr<ABuffer> AWavLoader::parse_wav(ByteBuffer& buffer)
|
||||
ASSERT(audio_format == 1);
|
||||
CHECK_OK("Audio format"); // value check
|
||||
|
||||
u16 num_channels;
|
||||
stream >> num_channels;
|
||||
ok = ok && (num_channels == 1 || num_channels == 2);
|
||||
stream >> m_num_channels;
|
||||
ok = ok && (m_num_channels == 1 || m_num_channels == 2);
|
||||
CHECK_OK("Channel count");
|
||||
|
||||
u32 sample_rate;
|
||||
stream >> sample_rate;
|
||||
stream >> m_sample_rate;
|
||||
CHECK_OK("Sample rate");
|
||||
|
||||
u32 byte_rate;
|
||||
@ -90,11 +94,10 @@ RefPtr<ABuffer> AWavLoader::parse_wav(ByteBuffer& buffer)
|
||||
stream >> block_align;
|
||||
CHECK_OK("Block align");
|
||||
|
||||
u16 bits_per_sample;
|
||||
stream >> bits_per_sample;
|
||||
stream >> m_bits_per_sample;
|
||||
CHECK_OK("Bits per sample"); // incomplete read check
|
||||
ok = ok && (bits_per_sample == 8 || bits_per_sample == 16 || bits_per_sample == 24);
|
||||
ASSERT(bits_per_sample == 8 || bits_per_sample == 16 || bits_per_sample == 24);
|
||||
ok = ok && (m_bits_per_sample == 8 || m_bits_per_sample == 16 || m_bits_per_sample == 24);
|
||||
ASSERT(m_bits_per_sample == 8 || m_bits_per_sample == 16 || m_bits_per_sample == 24);
|
||||
CHECK_OK("Bits per sample"); // value check
|
||||
|
||||
// Read chunks until we find DATA
|
||||
@ -116,21 +119,13 @@ RefPtr<ABuffer> AWavLoader::parse_wav(ByteBuffer& buffer)
|
||||
CHECK_OK("Found no data chunk");
|
||||
ASSERT(found_data);
|
||||
|
||||
ok = ok && data_sz < std::numeric_limits<int>::max();
|
||||
ok = ok && data_sz < INT32_MAX;
|
||||
CHECK_OK("Data was too large");
|
||||
|
||||
// ### consider using BufferStream to do this for us
|
||||
ok = ok && int(data_sz) <= (buffer.size() - stream.offset());
|
||||
CHECK_OK("Bad DATA (truncated)");
|
||||
|
||||
// Just make sure we're good before we read the data...
|
||||
ASSERT(!stream.handle_read_failure());
|
||||
|
||||
auto sample_data = buffer.slice_view(stream.offset(), data_sz);
|
||||
|
||||
dbgprintf("Read WAV of format PCM with num_channels %d sample rate %d, bits per sample %d\n", num_channels, sample_rate, bits_per_sample);
|
||||
|
||||
return ABuffer::from_pcm_data(sample_data, num_channels, bits_per_sample, sample_rate);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Small helper to resample from one playback rate to another
|
||||
|
@ -3,6 +3,7 @@
|
||||
#include <AK/AKString.h>
|
||||
#include <AK/RefPtr.h>
|
||||
#include <AK/StringView.h>
|
||||
#include <LibCore/CFile.h>
|
||||
|
||||
class ABuffer;
|
||||
|
||||
@ -13,10 +14,18 @@ class ByteBuffer;
|
||||
// Parses a WAV file and produces an ABuffer instance from it
|
||||
class AWavLoader {
|
||||
public:
|
||||
explicit AWavLoader(const StringView& path);
|
||||
RefPtr<ABuffer> load_wav(const StringView& path);
|
||||
const char* error_string() { return m_error_string.characters(); }
|
||||
|
||||
RefPtr<ABuffer> get_more_samples();
|
||||
|
||||
private:
|
||||
RefPtr<ABuffer> parse_wav(ByteBuffer&);
|
||||
bool parse_header();
|
||||
CFile m_file;
|
||||
String m_error_string;
|
||||
|
||||
u32 m_sample_rate { 0 };
|
||||
u16 m_num_channels { 0 };
|
||||
u16 m_bits_per_sample { 0 };
|
||||
};
|
||||
|
@ -48,11 +48,6 @@ bool ASClientConnection::handle_message(const ASAPI_ClientMessage& message, cons
|
||||
did_misbehave();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (shared_buf->size() / sizeof(ASample) > 441000) {
|
||||
did_misbehave();
|
||||
return false;
|
||||
}
|
||||
samples.resize(shared_buf->size() / sizeof(ASample));
|
||||
memcpy(samples.data(), shared_buf->data(), shared_buf->size());
|
||||
}
|
||||
@ -64,7 +59,7 @@ bool ASClientConnection::handle_message(const ASAPI_ClientMessage& message, cons
|
||||
reply.playing_buffer.buffer_id = message.play_buffer.buffer_id;
|
||||
post_message(reply);
|
||||
|
||||
m_mixer.queue(*this, adopt(*new ABuffer(move(samples))));
|
||||
m_mixer.queue(*this, ABuffer::create_with_samples(move(samples)), message.play_buffer.buffer_id);
|
||||
break;
|
||||
}
|
||||
case ASAPI_ClientMessage::Type::Invalid:
|
||||
|
@ -1,8 +1,9 @@
|
||||
#include <AK/BufferStream.h>
|
||||
#include <AudioServer/ASClientConnection.h>
|
||||
#include <AudioServer/ASMixer.h>
|
||||
#include <LibAudio/ASAPI.h>
|
||||
#include <LibCore/CThread.h>
|
||||
|
||||
#include <limits>
|
||||
#include "ASMixer.h"
|
||||
|
||||
ASMixer::ASMixer()
|
||||
: m_device("/dev/audio")
|
||||
@ -19,11 +20,11 @@ ASMixer::ASMixer()
|
||||
}, this);
|
||||
}
|
||||
|
||||
void ASMixer::queue(ASClientConnection&, const ABuffer& buffer)
|
||||
void ASMixer::queue(ASClientConnection& client, const ABuffer& buffer, int buffer_id)
|
||||
{
|
||||
ASSERT(buffer.size_in_bytes());
|
||||
CLocker lock(m_lock);
|
||||
m_pending_mixing.append(ASMixerBuffer(buffer));
|
||||
m_pending_mixing.append(ASMixerBuffer(buffer, client, buffer_id));
|
||||
}
|
||||
|
||||
void ASMixer::mix()
|
||||
@ -75,8 +76,15 @@ void ASMixer::mix()
|
||||
}
|
||||
|
||||
// clear it later
|
||||
if (buffer.pos == samples.size())
|
||||
if (buffer.pos == samples.size()) {
|
||||
if (buffer.m_buffer_id && buffer.m_client) {
|
||||
ASAPI_ServerMessage reply;
|
||||
reply.type = ASAPI_ServerMessage::Type::FinishedPlayingBuffer;
|
||||
reply.playing_buffer.buffer_id = buffer.m_buffer_id;
|
||||
buffer.m_client->post_message(reply);
|
||||
}
|
||||
buffer.done = true;
|
||||
}
|
||||
}
|
||||
|
||||
// output the mixed stuff to the device
|
||||
@ -108,3 +116,10 @@ void ASMixer::mix()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ASMixer::ASMixerBuffer::ASMixerBuffer(const NonnullRefPtr<ABuffer>& buf, ASClientConnection& client, int buffer_id)
|
||||
: buffer(buf)
|
||||
, m_client(client.make_weak_ptr())
|
||||
, m_buffer_id(buffer_id)
|
||||
{
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/ByteBuffer.h>
|
||||
#include <AK/NonnullRefPtrVector.h>
|
||||
#include <AK/RefCounted.h>
|
||||
#include <AK/WeakPtr.h>
|
||||
#include <LibAudio/ABuffer.h>
|
||||
#include <LibCore/CFile.h>
|
||||
#include <LibCore/CLock.h>
|
||||
#include <LibAudio/ABuffer.h>
|
||||
#include <AK/NonnullRefPtrVector.h>
|
||||
|
||||
class ASClientConnection;
|
||||
|
||||
@ -13,16 +14,16 @@ class ASMixer : public RefCounted<ASMixer> {
|
||||
public:
|
||||
ASMixer();
|
||||
|
||||
void queue(ASClientConnection&, const ABuffer&);
|
||||
void queue(ASClientConnection&, const ABuffer&, int buffer_id);
|
||||
|
||||
private:
|
||||
struct ASMixerBuffer {
|
||||
ASMixerBuffer(const NonnullRefPtr<ABuffer>& buf)
|
||||
: buffer(buf)
|
||||
{}
|
||||
ASMixerBuffer(const NonnullRefPtr<ABuffer>&, ASClientConnection&, int buffer_id = -1);
|
||||
NonnullRefPtr<ABuffer> buffer;
|
||||
int pos { 0 };
|
||||
bool done { false };
|
||||
WeakPtr<ASClientConnection> m_client;
|
||||
int m_buffer_id { -1 };
|
||||
};
|
||||
|
||||
Vector<ASMixerBuffer> m_pending_mixing;
|
||||
|
@ -16,15 +16,18 @@ int main(int argc, char **argv)
|
||||
AClientConnection a_conn;
|
||||
a_conn.handshake();
|
||||
printf("Established connection\n");
|
||||
AWavLoader loader;
|
||||
const auto& buffer = loader.load_wav(argv[1]);
|
||||
if (!buffer) {
|
||||
dbgprintf("Can't parse WAV: %s\n", loader.error_string());
|
||||
return 1;
|
||||
AWavLoader loader(argv[1]);
|
||||
printf("Loaded WAV\n");
|
||||
|
||||
for (;;) {
|
||||
auto samples = loader.get_more_samples();
|
||||
if (!samples) {
|
||||
break;
|
||||
}
|
||||
printf("Playing %d sample(s)\n", samples->samples().size());
|
||||
a_conn.play(*samples, true);
|
||||
}
|
||||
|
||||
printf("Playing WAV\n");
|
||||
a_conn.play(*buffer);
|
||||
printf("Exiting! :)\n");
|
||||
return 0;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user