More tests and scoping out the ReMixer API

This commit is contained in:
2024-08-26 14:41:14 -04:00
parent 99542b7a79
commit b0da5f4297
17 changed files with 320 additions and 63 deletions

View File

@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.18)
cmake_minimum_required(VERSION 3.18..3.27)
project(ReMixer
VERSION 1.0
LANGUAGES CXX
@@ -6,7 +6,6 @@ project(ReMixer
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
include(cmake/CPM.cmake)
@@ -38,7 +37,8 @@ endif()
include_directories("include" ${uuid_SOURCE_DIR}/include)
add_library(ReMixer ${SOURCES})
add_library(ReMixer ${SOURCES}
src/linux/PulseSubsystem.cpp)
add_executable(ReMixer-Test main.cpp)
set_target_properties(ReMixer PROPERTIES LINKER_LANGUAGE CXX)

BIN
cmake-build-debug/blm.ogg Normal file

Binary file not shown.

View File

@@ -1,19 +1,206 @@
#include <pulse/pulseaudio.h>
#include <pulse/thread-mainloop.h>
#include <iostream>
#include <pulse/error.h>
#include <pulse/volume.h>
#include <pulse/stream.h>
#include <pulse/introspect.h>
#pragma once
#include <ReMixer/SoundSubsystem.h>
#include <ReMixer/stream.h>
#include <format>
#include <map>
/// A PulseAudio implementation of the SoundSubsystem.
class PulseAudioSubsystem : public SoundSubsystem
{
struct pa_impl
{
pa_threaded_mainloop* mainloop;
pa_context* context;
pa_stream_direction_t* direction;
pa_mainloop_api* api;
const void* read_data;
size_t read_index, read_length;
int operation_success;
};
std::map<std::string, PulseStream> streams;
public:
/// Constructs a new PulseAudio context with the given name.
/// @param name The context name. All streams created with this context will be grouped together by this name.
PulseAudioSubsystem(const std::string& context_name) : SoundSubsystem()
{
const char *server = NULL;
pa = pa_xnew(pa_impl, 1);
int error = PA_ERR_INTERNAL;
if (!(pa->mainloop = pa_threaded_mainloop_new()))
{
throw std::runtime_error("Failed to create a PulseAudio mainloop.");
}
pa->api = pa_threaded_mainloop_get_api(pa->mainloop);
if (!(pa->context = pa_context_new(pa->api, context_name.c_str())))
{
throw std::runtime_error("Failed to create a PulseAudio context.");
}
pa_context_flags flags = pa_context_flags::PA_CONTEXT_NOAUTOSPAWN;
pa_context_set_state_callback(pa->context, OnCtxStateChange, pa);
if (pa_context_connect(pa->context, server, flags, NULL) < 0)
{
error = pa_context_errno(pa->context);
throw std::runtime_error(std::format("Failed to connect to context {}", error));
} else{
ctx_connected = true;
}
pa_threaded_mainloop_lock(pa->mainloop);
if (pa_threaded_mainloop_start(pa->mainloop) < 0) {
throw std::runtime_error("Failed to start thread mainloop.");
}
for (;;) {
pa_context_state_t state;
state = pa_context_get_state(pa->context);
if (state == PA_CONTEXT_READY)
break;
if (!PA_CONTEXT_IS_GOOD(state)) {
error = pa_context_errno(pa->context);
throw std::runtime_error(std::format("Context was not good: {}", error));
}
// Wait until the context is ready.
pa_threaded_mainloop_wait(pa->mainloop);
}
pa_threaded_mainloop_unlock(pa->mainloop);
std::cout << "PulseAudio context is ready." << std::endl;
}
bool Connected()
{
return ctx_connected;
}
PulseStream CreateStream(const std::string& stream_name)
{
// Assumed defaults
// TODO: Expose to the API later
static const pa_sample_spec ss = {
.format = PA_SAMPLE_S16LE,
.rate = 44100,
.channels = 2
};
const pa_cvolume* volume = NULL;
pa_channel_map* map = NULL;
const char* dev = NULL;
pa_buffer_attr* attr = NULL;
PulseStream new_stream = PulseStream();
pa_threaded_mainloop_lock(pa->mainloop);
if (!(new_stream.stream = pa_stream_new(pa->context, stream_name.c_str(), &ss, map)))
{
throw std::runtime_error("Failed to create a new PulseAudio stream.");
}
pa_stream_set_state_callback(new_stream.stream, PulseStream::OnStreamStateChanged, pa->mainloop);
pa_stream_set_read_callback(new_stream.stream, PulseStream::OnStreamRequest, pa->mainloop);
pa_stream_set_write_callback(new_stream.stream, PulseStream::OnStreamRequest, pa->mainloop);
pa_stream_set_latency_update_callback(new_stream.stream, PulseStream::OnLatencyUpdate, pa->mainloop);
pa_stream_flags flags = static_cast<pa_stream_flags>(PA_STREAM_INTERPOLATE_TIMING | PA_STREAM_ADJUST_LATENCY |
PA_STREAM_AUTO_TIMING_UPDATE);
// TODO: Support Playback and Recording streams
int r = pa_stream_connect_playback(new_stream.stream, dev, attr, flags, volume, NULL);
if (r < 0)
{
int error = pa_context_errno(pa->context);
throw std::runtime_error(std::format("Failed to connect the stream: {}", error));
}
for (;;)
{
pa_stream_state_t state;
state = pa_stream_get_state(new_stream.stream);
if (state == PA_STREAM_READY)
break;
if (!PA_STREAM_IS_GOOD(state))
{
int error = pa_context_errno(pa->context);
throw std::runtime_error(std::format("Stream state is bad: {}", error));
}
pa_threaded_mainloop_wait(pa->mainloop);
}
pa_threaded_mainloop_unlock(pa->mainloop);
streams.insert({stream_name, new_stream});
return new_stream;
}
protected:
private:
static void OnCtxStateChange(pa_context* c, void* userdata)
{
std::cout << "context state callback: ";
auto *p = static_cast<pa_impl *>(userdata);
switch(pa_context_get_state(c)) {
case PA_CONTEXT_READY:
std::cout << "Context Ready" << std::endl;
pa_threaded_mainloop_signal(p->mainloop, 0);
break;
case PA_CONTEXT_TERMINATED:
std::cout << "Context Terminated" << std::endl;
pa_threaded_mainloop_signal(p->mainloop, 0);
break;
case PA_CONTEXT_FAILED:
std::cout << "Context Failed" << std::endl;
pa_threaded_mainloop_signal(p->mainloop, 0);
break;
case PA_CONTEXT_UNCONNECTED:
std::cout << "Context Unconnected" << std::endl;
break;
case PA_CONTEXT_CONNECTING:
std::cout << "Context Connecting" << std::endl;
break;
case PA_CONTEXT_AUTHORIZING:
std::cout << "Context Authorizing" << std::endl;
break;
case PA_CONTEXT_SETTING_NAME:
std::cout << "Context Setting Name" << std::endl;
break;
default:
std::cerr << "The fuck?" << std::endl;
break;
}
}
pa_impl* pa;
bool ctx_connected = false;
pa_threaded_mainloop* mainloop;
pa_context* context;
pa_stream_direction_t* direction;
pa_mainloop_api* api;
};

View File

@@ -4,6 +4,7 @@
#include <ReMixer/sound.h>
namespace ReMixer {
uint16_t createStream(const std::string& application_name, const std::string& stream_name, SampleRate sample_rate, StreamMode mode);
SFX* loadSFX(const std::string& file);
}

View File

@@ -3,7 +3,7 @@
#include <Event.h>
/// A managed sound handler object.
class Sound
class NuSound
{
public:
virtual bool IsPlaying() = 0;
@@ -60,6 +60,16 @@ class Decibels
};
enum class SoundApi
{
PulseAudio, /// Currently supported.
ALSA, /// Not yet supported.
PipeWire, /// Not yet supported.
WASAPI, /// Work-in-progress support.
OSS, /// Not yet supported.
CoreAudio /// Support not planned.
};
class AudioDevice {};
class PlaybackDevice : public AudioDevice {};
@@ -84,10 +94,10 @@ public:
void SetMasterVolume(Decibels db);
protected:
private:
};

View File

@@ -6,8 +6,7 @@
#include <ReMixer/stream.h>
class PlaybackDevice {};
class InputDevice {};
class Sound {
private:
@@ -18,7 +17,7 @@ public:
static Sound FromOGGVorbisFile(const std::filesystem::path& file_name)
{
return Sound{};
}
std::vector<char> getAudioData();
@@ -31,3 +30,13 @@ public:
[[nodiscard]] const char* ptr() const { return audio_data.data();}
Sound() = default;
};
/// Class that holds a short sound that can fit in memory and requires no latency. (i.e. footsteps or gun shots).
class SFX {
};
/// Music class is for longer sounds that should be streamed in.
class Music {
};

View File

@@ -12,28 +12,40 @@
#include <string>
#include <vector>
#include <pulse/simple.h>
#include <pulse/stream.h>
#include <pulse/thread-mainloop.h>
enum class StreamMode : bool {
MONO = false,
STEREO = true
};
enum class SampleRate : unsigned int {
STANDARD = 44100,
HIGH = 48000
unsigned operator ""_Hz(unsigned long long int frequency)
{
return frequency;
}
unsigned operator ""_kHz(long double frequency)
{
return frequency * 1000;
}
enum class StreamDirection {
PLAYBACK,
RECORD,
};
class Stream {
private:
std::string application_name;
std::string stream_name;
StreamMode mode;
SampleRate sample_rate;
std::string name;
std::string parent_name;
unsigned int channel_count;
unsigned int sample_rate;
StreamDirection dir;
std::vector<char> buffer;
#ifdef __linux__
pa_simple* stream = nullptr;
pa_stream* stream = nullptr;
#endif
#ifdef _WIN32
@@ -41,13 +53,68 @@ private:
public:
size_t bufferSize;
uint16_t handle;
Stream(const std::string& application_name, const std::string& stream_name, SampleRate sample_rate, StreamMode mode);
//Stream(const std::string& application_name, const std::string& stream_name, SampleRate sample_rate, StreamMode mode);
unsigned int numChannels();
void erase();
void test_play(const void *data, size_t size, int *err);
};
inline std::vector<Stream*> streamList;
class PulseStream : public Stream
{
public:
pa_stream* stream;
static void OnStreamStateChanged(pa_stream* s, void* userdata)
{
std::cout << "stream state callback: ";
auto *p = static_cast<pa_threaded_mainloop *>(userdata);
auto state = pa_stream_get_state(s);
switch(state) {
case PA_STREAM_READY:
std::cout << "Stream Ready" << std::endl;
pa_threaded_mainloop_signal(p, 0);
break;
case PA_STREAM_FAILED:
std::cout << "Stream Failed" << std::endl;
pa_threaded_mainloop_signal(p, 0);
break;
case PA_STREAM_TERMINATED:
std::cout << "Stream Terminated" << std::endl;
pa_threaded_mainloop_signal(p, 0);
break;
case PA_STREAM_UNCONNECTED:
std::cout << "Stream Unconnected" << std::endl;
break;
case PA_STREAM_CREATING:
std::cout << "Stream Creating" << std::endl;
break;
}
}
static void OnStreamRequest(pa_stream* s, size_t length, void* userdata)
{
std::cout << "Received stream request callback: " << length << std::endl;
auto *p = static_cast<pa_threaded_mainloop *>(userdata);
pa_threaded_mainloop_signal(p, 0);
}
static void OnLatencyUpdate(pa_stream* s, void* userdata)
{
std::cout << "Received stream latency update callback" << std::endl;
auto *p = static_cast<pa_threaded_mainloop *>(userdata);
pa_threaded_mainloop_signal(p, 0);
}
protected:
private:
};

View File

@@ -11,32 +11,27 @@
#include <iostream>
#include <ReMixer/stream.h>
#include <ReMixer/sound.h>
#include <ReMixer/PulseSubsystem.h>
#include <vorbis/codec.h>
#include <vorbis/vorbisfile.h>
#include <iostream>
#include <fstream>
#include <thread>
[[noreturn]] int main() {
Stream stream("ReMixer-Test", "Main", SampleRate::STANDARD, StreamMode::STEREO);
FILE* rawData = fopen("output.raw", "rb");
if (!rawData) {
std::cerr << "Error opening input file." << std::endl; return -1;
using namespace std::chrono_literals;
PulseAudioSubsystem test = PulseAudioSubsystem("PulseAudio Test");
PulseStream test_stream = test.CreateStream("Test Stream");
PulseStream test_stream2 = test.CreateStream("Another Test Stream");
while(true)
{
std::this_thread::sleep_for(25ms);
}
signed char raw[65535];
int bytes_read = fread(raw, sizeof(signed char), 65535, rawData);
std::cout << "Read " << bytes_read << " bytes..." << std::endl;
int* returned_err;
stream.test_play(raw, 65535, returned_err);
std::cout << "Something happened!!" << std::endl;
bool keep_runnin = true;
while (keep_runnin) {
}
return 0;
}

View File

@@ -9,7 +9,6 @@
#include <cstring>
#include <thread>
#include <vector>
#include <iostream>
#include <cstdio>
#include <unistd.h>
@@ -105,7 +104,7 @@ static void stream_state_cb(pa_stream* s, void* userdata)
}
static void stream_request_cb(pa_stream* s, size_t length, void* userdata) {
std::cout << "Received stream request callback" << std::endl;
std::cout << "Received stream request callback: " << length << std::endl;
auto *p = static_cast<pa_system *>(userdata);
@@ -197,7 +196,6 @@ pa_system* pa_system_new(
// Wait until the context is ready
pa_threaded_mainloop_wait(p->mainloop);
}
if (!(p->stream = pa_stream_new(p->context, stream_name, ss, map))) {
@@ -219,7 +217,6 @@ pa_system* pa_system_new(
else
r = pa_stream_connect_record(p->stream, dev, attr, my_flags);
if (r < 0) {
error = pa_context_errno(p->context);
// goto unlock_and_fail
@@ -294,7 +291,6 @@ void on_device_sink(pa_context* c, const pa_sink_info* info, int eol, void* user
void on_device_source(pa_context* c, const pa_source_info* info, int eol, void *userdata)
{
if (eol != 0)
{
pa_system* p = static_cast<pa_system *>(userdata);
@@ -551,7 +547,6 @@ struct pcm_s16_data
std::istream_iterator<char>(file),
std::istream_iterator<char>());
}
explicit pcm_s16_data(const std::vector<char>& pcm_buf)
{
@@ -605,7 +600,7 @@ int main(int argc, char* argv[]) {
/* Replace STDIN with the specified file if needed. */
if (argc < 3)
if (argc < 4)
{
std::cerr << "Please provide two input PCM files!" << std::endl;
return -1;
@@ -613,9 +608,9 @@ int main(int argc, char* argv[]) {
pcm_s16_data song = pcm_s16_data(argv[1]);
pcm_s16_data sfx = pcm_s16_data(argv[2]);
pcm_s16_data blm = pcm_s16_data(argv[3]);
pcm_s16_data combined = song + sfx;
pcm_s16_data combined = song + sfx + blm;
/*if (argc > 1) {
int fd;
@@ -641,7 +636,6 @@ int main(int argc, char* argv[]) {
pa_context_get_server_info(s->context, reinterpret_cast<pa_server_info_cb_t>(list_server_info), s);
pa_context_get_sink_info_list(s->context, reinterpret_cast<pa_sink_info_cb_t>(list_sink_info), s);
pa_system_enumerate_devices(s);
if (pa_system_write(s, combined.buffer.data(), combined.buffer.size(), &error) < 0) {
@@ -679,15 +673,6 @@ int main(int argc, char* argv[]) {
}
}*/
using namespace std::chrono_literals;
std::this_thread::sleep_for(1s);
pa_stream_cork(s->stream, 1, success_cb, s);
std::this_thread::sleep_for(1s);
pa_stream_cork(s->stream, 0, success_cb, s);
/* Make sure that every single sample was played */
if (pa_system_drain(s, &error) < 0) {
fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));

Binary file not shown.

View File

@@ -0,0 +1 @@
#include <ReMixer/PulseSubsystem.h>

View File

@@ -7,5 +7,5 @@ uint16_t ReMixer::createStream(const std::string& application_name, const std::s
}
SFX* ReMixer::loadSFX(const std::string& file) {
return new SFX(file);
//return new SFX(file);
}

View File

@@ -27,7 +27,8 @@ void Sound::setSampleRate(SampleRate rate) {
sample_rate = rate;
}
SFX::SFX(const std::string& sfx_file_path) {
/*
* SFX::SFX(const std::string& sfx_file_path) {
OggVorbis_File vf;
FILE* inFile = fopen(sfx_file_path.c_str(), "rb");
@@ -60,4 +61,4 @@ SFX::SFX(const std::string& sfx_file_path) {
ov_clear(&vf);
}
*/

View File

@@ -6,7 +6,7 @@
Stream::Stream(const std::string& name, const std::string& stream_name, SampleRate sample_rate, StreamMode mode) {
/*Stream::Stream(const std::string& name, const std::string& stream_name, SampleRate sample_rate, StreamMode mode) {
this->application_name = name;
this->stream_name = stream_name;
this->sample_rate = sample_rate;
@@ -50,3 +50,4 @@ void Stream::erase() {
if (this == streamList[i])
streamList.erase(streamList.begin() + i);
}
*/