diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index cfce88e0..ab9eb9b3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -108,6 +108,9 @@ target_sources(tomato PUBLIC ./frame_streams/stream_manager.hpp ./frame_streams/stream_manager.cpp + ./frame_streams/sdl/sdl_audio2_frame_stream2.hpp + ./frame_streams/sdl/sdl_audio2_frame_stream2.cpp + ./stream_manager_ui.hpp ./stream_manager_ui.cpp ) diff --git a/src/frame_streams/sdl/sdl_audio2_frame_stream2.cpp b/src/frame_streams/sdl/sdl_audio2_frame_stream2.cpp new file mode 100644 index 00000000..792f76ba --- /dev/null +++ b/src/frame_streams/sdl/sdl_audio2_frame_stream2.cpp @@ -0,0 +1,271 @@ +#include "./sdl_audio2_frame_stream2.hpp" + +#include +#include +#include + +// "thin" wrapper around sdl audio streams +// we dont needs to get fance, as they already provide everything we need +struct SDLAudio2StreamReader : public AudioFrame2Stream2I { + std::unique_ptr _stream; + + uint32_t _sample_rate {48'000}; + size_t _channels {0}; + + // buffer gets reused! + std::vector _buffer; + + SDLAudio2StreamReader(void) : _stream(nullptr, nullptr) {} + SDLAudio2StreamReader(SDLAudio2StreamReader&& other) : + _stream(std::move(other._stream)), + _sample_rate(other._sample_rate), + _channels(other._channels) + { + const size_t buffer_size {960*_channels}; + _buffer.resize(buffer_size); + } + + ~SDLAudio2StreamReader(void) { + if (_stream) { + SDL_UnbindAudioStream(_stream.get()); + } + } + + int32_t size(void) override { + //assert(_stream); + // returns bytes + //SDL_GetAudioStreamAvailable(_stream.get()); + return -1; + } + + std::optional pop(void) override { + assert(_stream); + if (!_stream) { + return std::nullopt; + } + + const size_t buffer_size {960*_channels}; + _buffer.resize(buffer_size); // noop? + + const auto read_bytes = SDL_GetAudioStreamData( + _stream.get(), + _buffer.data(), + _buffer.size()*sizeof(int16_t) + ); + + // no new frame yet, or error + if (read_bytes <= 0) { + return std::nullopt; + } + + return AudioFrame2 { + _sample_rate, _channels, + Span(_buffer.data(), read_bytes/sizeof(int16_t)), + }; + } + + bool push(const AudioFrame2&) override { + // TODO: make universal sdl stream wrapper (combine with SDLAudioOutputDeviceDefaultInstance) + assert(false && "read only"); + return false; + } +}; + +SDLAudio2InputDevice::SDLAudio2InputDevice(void) : SDLAudio2InputDevice(SDL_AUDIO_DEVICE_DEFAULT_RECORDING) { +} + +SDLAudio2InputDevice::SDLAudio2InputDevice(SDL_AudioDeviceID conf_device_id) : _configured_device_id(conf_device_id) { + if (_configured_device_id == 0) { + // TODO: proper error handling + throw int(1); + } +} + +SDLAudio2InputDevice::~SDLAudio2InputDevice(void) { + _streams.clear(); + + if (_virtual_device_id != 0) { + SDL_CloseAudioDevice(_virtual_device_id); + _virtual_device_id = 0; + } +} + +std::shared_ptr> SDLAudio2InputDevice::subscribe(void) { + if (_virtual_device_id == 0) { + // first stream, open device + // this spec is more like a hint to the hardware + SDL_AudioSpec spec { + SDL_AUDIO_S16, + 1, // TODO: conf + 48'000, + }; + _virtual_device_id = SDL_OpenAudioDevice(_configured_device_id, &spec); + } + + if (_virtual_device_id == 0) { + std::cerr << "SDLAID error: failed opening device " << _configured_device_id << "\n"; + return nullptr; + } + + SDL_AudioSpec spec { + SDL_AUDIO_S16, // required, as AudioFrame2 only supports s16 + 1, // TODO: conf + 48'000, + }; + + SDL_AudioSpec device_spec { + SDL_AUDIO_S16, + 1, // TODO: conf + 48'000, + }; + // TODO: error check + SDL_GetAudioDeviceFormat(_virtual_device_id, &device_spec, nullptr); + + // error check + auto* sdl_stream = SDL_CreateAudioStream(&device_spec, &spec); + + // error check + SDL_BindAudioStream(_virtual_device_id, sdl_stream); + + auto new_stream = std::make_shared(); + // TODO: move to ctr + new_stream->_stream = {sdl_stream, &SDL_DestroyAudioStream}; + new_stream->_sample_rate = spec.freq; + new_stream->_channels = spec.channels; + + _streams.emplace_back(new_stream); + + return new_stream; +} + +bool SDLAudio2InputDevice::unsubscribe(const std::shared_ptr>& sub) { + for (auto it = _streams.cbegin(); it != _streams.cend(); it++) { + if (*it == sub) { + _streams.erase(it); + if (_streams.empty()) { + // last stream, close + // TODO: make sure no shared ptr still exists??? + SDL_CloseAudioDevice(_virtual_device_id); + std::cout << "SDLAID: closing device " << _virtual_device_id << " (" << _configured_device_id << ")\n"; + _virtual_device_id = 0; + } + return true; + } + } + + return false; +} + +// does not need to be visible in the header +struct SDLAudio2OutputDeviceDefaultInstance : public AudioFrame2Stream2I { + std::unique_ptr _stream; + + uint32_t _last_sample_rate {48'000}; + size_t _last_channels {0}; + + // TODO: audio device + SDLAudio2OutputDeviceDefaultInstance(void); + SDLAudio2OutputDeviceDefaultInstance(SDLAudio2OutputDeviceDefaultInstance&& other); + + ~SDLAudio2OutputDeviceDefaultInstance(void); + + int32_t size(void) override; + std::optional pop(void) override; + bool push(const AudioFrame2& value) override; +}; + +SDLAudio2OutputDeviceDefaultInstance::SDLAudio2OutputDeviceDefaultInstance(void) : _stream(nullptr, nullptr) { +} + +SDLAudio2OutputDeviceDefaultInstance::SDLAudio2OutputDeviceDefaultInstance(SDLAudio2OutputDeviceDefaultInstance&& other) : _stream(std::move(other._stream)) { +} + +SDLAudio2OutputDeviceDefaultInstance::~SDLAudio2OutputDeviceDefaultInstance(void) { +} +int32_t SDLAudio2OutputDeviceDefaultInstance::size(void) { + return -1; +} + +std::optional SDLAudio2OutputDeviceDefaultInstance::pop(void) { + assert(false); + // this is an output device, there is no data to pop + return std::nullopt; +} + +bool SDLAudio2OutputDeviceDefaultInstance::push(const AudioFrame2& value) { + if (!static_cast(_stream)) { + return false; + } + + // verify here the fame has the same channel count and sample freq + // if something changed, we need to either use a temporary stream, just for conversion, or reopen _stream with the new params + // because of data temporality, the second options looks like a better candidate + if ( + value.sample_rate != _last_sample_rate || + value.channels != _last_channels + ) { + const SDL_AudioSpec spec = { + static_cast(SDL_AUDIO_S16), + static_cast(value.channels), + static_cast(value.sample_rate) + }; + + SDL_SetAudioStreamFormat(_stream.get(), &spec, nullptr); + + std::cerr << "SDLAOD: audio format changed\n"; + } + + auto data = value.getSpan(); + + if (data.size == 0) { + std::cerr << "empty audio frame??\n"; + } + + if (!SDL_PutAudioStreamData(_stream.get(), data.ptr, data.size * sizeof(int16_t))) { + std::cerr << "put data error\n"; + return false; // return true? + } + + _last_sample_rate = value.sample_rate; + _last_channels = value.channels; + + return true; +} + + +SDLAudio2OutputDeviceDefaultSink::~SDLAudio2OutputDeviceDefaultSink(void) { + // TODO: pause and close device? +} + +std::shared_ptr> SDLAudio2OutputDeviceDefaultSink::subscribe(void) { + auto new_instance = std::make_shared(); + + constexpr SDL_AudioSpec spec = { SDL_AUDIO_S16, 2, 48000 }; + + new_instance->_stream = { + SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, nullptr, nullptr), + &SDL_DestroyAudioStream + }; + new_instance->_last_sample_rate = spec.freq; + new_instance->_last_channels = spec.channels; + + if (!static_cast(new_instance->_stream)) { + std::cerr << "SDL open audio device failed!\n"; + return nullptr; + } + + const auto audio_device_id = SDL_GetAudioStreamDevice(new_instance->_stream.get()); + SDL_ResumeAudioDevice(audio_device_id); + + return new_instance; +} + +bool SDLAudio2OutputDeviceDefaultSink::unsubscribe(const std::shared_ptr>& sub) { + // TODO: i think we should keep track of them + if (!sub) { + return false; + } + + return true; +} + diff --git a/src/frame_streams/sdl/sdl_audio2_frame_stream2.hpp b/src/frame_streams/sdl/sdl_audio2_frame_stream2.hpp new file mode 100644 index 00000000..97a91a19 --- /dev/null +++ b/src/frame_streams/sdl/sdl_audio2_frame_stream2.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "../frame_stream2.hpp" +#include "../audio_stream2.hpp" + +#include + +#include +#include + +// we dont have to multicast ourself, because sdl streams and virtual devices already do this + +// source +// opens device +// creates a sdl audio stream for each subscribed reader stream +struct SDLAudio2InputDevice : public FrameStream2SourceI { + // held by instances + using sdl_stream_type = std::unique_ptr; + + SDL_AudioDeviceID _configured_device_id {0}; + SDL_AudioDeviceID _virtual_device_id {0}; + + std::vector>> _streams; + + SDLAudio2InputDevice(void); + SDLAudio2InputDevice(SDL_AudioDeviceID conf_device_id); + ~SDLAudio2InputDevice(void); + + std::shared_ptr> subscribe(void) override; + bool unsubscribe(const std::shared_ptr>& sub) override; +}; + +// sink +// constructs entirely new streams, since sdl handles sync and mixing for us (or should) +struct SDLAudio2OutputDeviceDefaultSink : public FrameStream2SinkI { + // TODO: pause device? + + ~SDLAudio2OutputDeviceDefaultSink(void); + + std::shared_ptr> subscribe(void) override; + bool unsubscribe(const std::shared_ptr>& sub) override; +}; + diff --git a/src/main.cpp b/src/main.cpp index 49de9669..1e66ce6e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,6 +28,8 @@ int main(int argc, char** argv) { runSysCheck(); + SDL_SetAppMetadata("tomato", "0.0.0-wip", nullptr); + #ifdef __ANDROID__ // change current working dir to internal storage std::filesystem::current_path(SDL_GetAndroidInternalStoragePath()); diff --git a/src/main_screen.cpp b/src/main_screen.cpp index 79902a0f..3253f37d 100644 --- a/src/main_screen.cpp +++ b/src/main_screen.cpp @@ -5,6 +5,8 @@ #include +#include "./frame_streams/sdl/sdl_audio2_frame_stream2.hpp" + #include #include @@ -138,9 +140,44 @@ MainScreen::MainScreen(SimpleConfigModel&& conf_, SDL_Renderer* renderer_, Theme } conf.dump(); + + if (SDL_InitSubSystem(SDL_INIT_AUDIO)) { + // add system audio devices + { // audio in + ObjectHandle asrc {os.registry(), os.registry().create()}; + try { + asrc.emplace>( + std::make_unique() + ); + + asrc.emplace(Components::StreamSource::create("SDL Audio Default Recording Device")); + + os.throwEventConstruct(asrc); + } catch (...) { + os.registry().destroy(asrc); + } + } + { // audio out + ObjectHandle asink {os.registry(), os.registry().create()}; + try { + asink.emplace>( + std::make_unique() + ); + + asink.emplace(Components::StreamSink::create("SDL Audio Default Playback Device")); + + os.throwEventConstruct(asink); + } catch (...) { + os.registry().destroy(asink); + } + } + } else { + std::cerr << "MS warning: no sdl audio: " << SDL_GetError() << "\n"; + } } MainScreen::~MainScreen(void) { + // TODO: quit sdl audio } bool MainScreen::handleEvent(SDL_Event& e) {