diff options
Diffstat (limited to 'src/audio_core')
| -rw-r--r-- | src/audio_core/CMakeLists.txt | 27 | ||||
| -rw-r--r-- | src/audio_core/audio_out.cpp | 58 | ||||
| -rw-r--r-- | src/audio_core/audio_out.h | 43 | ||||
| -rw-r--r-- | src/audio_core/audio_renderer.cpp | 234 | ||||
| -rw-r--r-- | src/audio_core/audio_renderer.h | 206 | ||||
| -rw-r--r-- | src/audio_core/buffer.h | 45 | ||||
| -rw-r--r-- | src/audio_core/codec.cpp | 77 | ||||
| -rw-r--r-- | src/audio_core/codec.h | 44 | ||||
| -rw-r--r-- | src/audio_core/cubeb_sink.cpp | 200 | ||||
| -rw-r--r-- | src/audio_core/cubeb_sink.h | 32 | ||||
| -rw-r--r-- | src/audio_core/null_sink.h | 27 | ||||
| -rw-r--r-- | src/audio_core/sink.h | 31 | ||||
| -rw-r--r-- | src/audio_core/sink_details.cpp | 44 | ||||
| -rw-r--r-- | src/audio_core/sink_details.h | 35 | ||||
| -rw-r--r-- | src/audio_core/sink_stream.h | 32 | ||||
| -rw-r--r-- | src/audio_core/stream.cpp | 127 | ||||
| -rw-r--r-- | src/audio_core/stream.h | 102 |
17 files changed, 1364 insertions, 0 deletions
diff --git a/src/audio_core/CMakeLists.txt b/src/audio_core/CMakeLists.txt new file mode 100644 index 000000000..ec71524a3 --- /dev/null +++ b/src/audio_core/CMakeLists.txt @@ -0,0 +1,27 @@ +add_library(audio_core STATIC + audio_out.cpp + audio_out.h + audio_renderer.cpp + audio_renderer.h + buffer.h + codec.cpp + codec.h + null_sink.h + stream.cpp + stream.h + sink.h + sink_details.cpp + sink_details.h + sink_stream.h + + $<$<BOOL:${ENABLE_CUBEB}>:cubeb_sink.cpp cubeb_sink.h> +) + +create_target_directory_groups(audio_core) + +target_link_libraries(audio_core PUBLIC common core) + +if(ENABLE_CUBEB) + target_link_libraries(audio_core PRIVATE cubeb) + target_compile_definitions(audio_core PRIVATE -DHAVE_CUBEB=1) +endif() diff --git a/src/audio_core/audio_out.cpp b/src/audio_core/audio_out.cpp new file mode 100644 index 000000000..12632a95c --- /dev/null +++ b/src/audio_core/audio_out.cpp @@ -0,0 +1,58 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio_core/audio_out.h" +#include "audio_core/sink.h" +#include "audio_core/sink_details.h" +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/settings.h" + +namespace AudioCore { + +/// Returns the stream format from the specified number of channels +static Stream::Format ChannelsToStreamFormat(u32 num_channels) { + switch (num_channels) { + case 1: + return Stream::Format::Mono16; + case 2: + return Stream::Format::Stereo16; + case 6: + return Stream::Format::Multi51Channel16; + } + + LOG_CRITICAL(Audio, "Unimplemented num_channels={}", num_channels); + UNREACHABLE(); + return {}; +} + +StreamPtr AudioOut::OpenStream(u32 sample_rate, u32 num_channels, std::string&& name, + Stream::ReleaseCallback&& release_callback) { + if (!sink) { + const SinkDetails& sink_details = GetSinkDetails(Settings::values.sink_id); + sink = sink_details.factory(Settings::values.audio_device_id); + } + + return std::make_shared<Stream>( + sample_rate, ChannelsToStreamFormat(num_channels), std::move(release_callback), + sink->AcquireSinkStream(sample_rate, num_channels, name), std::move(name)); +} + +std::vector<Buffer::Tag> AudioOut::GetTagsAndReleaseBuffers(StreamPtr stream, size_t max_count) { + return stream->GetTagsAndReleaseBuffers(max_count); +} + +void AudioOut::StartStream(StreamPtr stream) { + stream->Play(); +} + +void AudioOut::StopStream(StreamPtr stream) { + stream->Stop(); +} + +bool AudioOut::QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector<s16>&& data) { + return stream->QueueBuffer(std::make_shared<Buffer>(tag, std::move(data))); +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_out.h b/src/audio_core/audio_out.h new file mode 100644 index 000000000..39b7e656b --- /dev/null +++ b/src/audio_core/audio_out.h @@ -0,0 +1,43 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <string> +#include <vector> + +#include "audio_core/buffer.h" +#include "audio_core/sink.h" +#include "audio_core/stream.h" +#include "common/common_types.h" + +namespace AudioCore { + +/** + * Represents an audio playback interface, used to open and play audio streams + */ +class AudioOut { +public: + /// Opens a new audio stream + StreamPtr OpenStream(u32 sample_rate, u32 num_channels, std::string&& name, + Stream::ReleaseCallback&& release_callback); + + /// Returns a vector of recently released buffers specified by tag for the specified stream + std::vector<Buffer::Tag> GetTagsAndReleaseBuffers(StreamPtr stream, size_t max_count); + + /// Starts an audio stream for playback + void StartStream(StreamPtr stream); + + /// Stops an audio stream that is currently playing + void StopStream(StreamPtr stream); + + /// Queues a buffer into the specified audio stream, returns true on success + bool QueueBuffer(StreamPtr stream, Buffer::Tag tag, std::vector<s16>&& data); + +private: + SinkPtr sink; +}; + +} // namespace AudioCore diff --git a/src/audio_core/audio_renderer.cpp b/src/audio_core/audio_renderer.cpp new file mode 100644 index 000000000..282f345c5 --- /dev/null +++ b/src/audio_core/audio_renderer.cpp @@ -0,0 +1,234 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include "audio_core/audio_renderer.h" +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/memory.h" + +namespace AudioCore { + +constexpr u32 STREAM_SAMPLE_RATE{48000}; +constexpr u32 STREAM_NUM_CHANNELS{2}; + +AudioRenderer::AudioRenderer(AudioRendererParameter params, + Kernel::SharedPtr<Kernel::Event> buffer_event) + : worker_params{params}, buffer_event{buffer_event}, voices(params.voice_count) { + + audio_core = std::make_unique<AudioCore::AudioOut>(); + stream = audio_core->OpenStream(STREAM_SAMPLE_RATE, STREAM_NUM_CHANNELS, "AudioRenderer", + [=]() { buffer_event->Signal(); }); + audio_core->StartStream(stream); + + QueueMixedBuffer(0); + QueueMixedBuffer(1); + QueueMixedBuffer(2); +} + +std::vector<u8> AudioRenderer::UpdateAudioRenderer(const std::vector<u8>& input_params) { + // Copy UpdateDataHeader struct + UpdateDataHeader config{}; + std::memcpy(&config, input_params.data(), sizeof(UpdateDataHeader)); + u32 memory_pool_count = worker_params.effect_count + (worker_params.voice_count * 4); + + // Copy MemoryPoolInfo structs + std::vector<MemoryPoolInfo> mem_pool_info(memory_pool_count); + std::memcpy(mem_pool_info.data(), + input_params.data() + sizeof(UpdateDataHeader) + config.behavior_size, + memory_pool_count * sizeof(MemoryPoolInfo)); + + // Copy VoiceInfo structs + size_t offset{sizeof(UpdateDataHeader) + config.behavior_size + config.memory_pools_size + + config.voice_resource_size}; + for (auto& voice : voices) { + std::memcpy(&voice.Info(), input_params.data() + offset, sizeof(VoiceInfo)); + offset += sizeof(VoiceInfo); + } + + // Update voices + for (auto& voice : voices) { + voice.UpdateState(); + if (!voice.GetInfo().is_in_use) { + continue; + } + if (voice.GetInfo().is_new) { + voice.SetWaveIndex(voice.GetInfo().wave_buffer_head); + } + } + + // Update memory pool state + std::vector<MemoryPoolEntry> memory_pool(memory_pool_count); + for (size_t index = 0; index < memory_pool.size(); ++index) { + if (mem_pool_info[index].pool_state == MemoryPoolStates::RequestAttach) { + memory_pool[index].state = MemoryPoolStates::Attached; + } else if (mem_pool_info[index].pool_state == MemoryPoolStates::RequestDetach) { + memory_pool[index].state = MemoryPoolStates::Detached; + } + } + + // Release previous buffers and queue next ones for playback + ReleaseAndQueueBuffers(); + + // Copy output header + UpdateDataHeader response_data{worker_params}; + std::vector<u8> output_params(response_data.total_size); + std::memcpy(output_params.data(), &response_data, sizeof(UpdateDataHeader)); + + // Copy output memory pool entries + std::memcpy(output_params.data() + sizeof(UpdateDataHeader), memory_pool.data(), + response_data.memory_pools_size); + + // Copy output voice status + size_t voice_out_status_offset{sizeof(UpdateDataHeader) + response_data.memory_pools_size}; + for (const auto& voice : voices) { + std::memcpy(output_params.data() + voice_out_status_offset, &voice.GetOutStatus(), + sizeof(VoiceOutStatus)); + voice_out_status_offset += sizeof(VoiceOutStatus); + } + + return output_params; +} + +void AudioRenderer::VoiceState::SetWaveIndex(size_t index) { + wave_index = index & 3; + is_refresh_pending = true; +} + +std::vector<s16> AudioRenderer::VoiceState::DequeueSamples(size_t sample_count) { + if (!IsPlaying()) { + return {}; + } + + if (is_refresh_pending) { + RefreshBuffer(); + } + + const size_t max_size{samples.size() - offset}; + const size_t dequeue_offset{offset}; + size_t size{sample_count * STREAM_NUM_CHANNELS}; + if (size > max_size) { + size = max_size; + } + + out_status.played_sample_count += size / STREAM_NUM_CHANNELS; + offset += size; + + const auto& wave_buffer{info.wave_buffer[wave_index]}; + if (offset == samples.size()) { + offset = 0; + + if (!wave_buffer.is_looping) { + SetWaveIndex(wave_index + 1); + } + + out_status.wave_buffer_consumed++; + + if (wave_buffer.end_of_stream) { + info.play_state = PlayState::Paused; + } + } + + return {samples.begin() + dequeue_offset, samples.begin() + dequeue_offset + size}; +} + +void AudioRenderer::VoiceState::UpdateState() { + if (is_in_use && !info.is_in_use) { + // No longer in use, reset state + is_refresh_pending = true; + wave_index = 0; + offset = 0; + out_status = {}; + } + is_in_use = info.is_in_use; +} + +void AudioRenderer::VoiceState::RefreshBuffer() { + std::vector<s16> new_samples(info.wave_buffer[wave_index].buffer_sz / sizeof(s16)); + Memory::ReadBlock(info.wave_buffer[wave_index].buffer_addr, new_samples.data(), + info.wave_buffer[wave_index].buffer_sz); + + switch (static_cast<Codec::PcmFormat>(info.sample_format)) { + case Codec::PcmFormat::Int16: { + // PCM16 is played as-is + break; + } + case Codec::PcmFormat::Adpcm: { + // Decode ADPCM to PCM16 + Codec::ADPCM_Coeff coeffs; + Memory::ReadBlock(info.additional_params_addr, coeffs.data(), sizeof(Codec::ADPCM_Coeff)); + new_samples = Codec::DecodeADPCM(reinterpret_cast<u8*>(new_samples.data()), + new_samples.size() * sizeof(s16), coeffs, adpcm_state); + break; + } + default: + LOG_CRITICAL(Audio, "Unimplemented sample_format={}", info.sample_format); + UNREACHABLE(); + break; + } + + switch (info.channel_count) { + case 1: + // 1 channel is upsampled to 2 channel + samples.resize(new_samples.size() * 2); + for (size_t index = 0; index < new_samples.size(); ++index) { + samples[index * 2] = new_samples[index]; + samples[index * 2 + 1] = new_samples[index]; + } + break; + case 2: { + // 2 channel is played as is + samples = std::move(new_samples); + break; + } + default: + LOG_CRITICAL(Audio, "Unimplemented channel_count={}", info.channel_count); + UNREACHABLE(); + break; + } + + is_refresh_pending = false; +} + +static constexpr s16 ClampToS16(s32 value) { + return static_cast<s16>(std::clamp(value, -32768, 32767)); +} + +void AudioRenderer::QueueMixedBuffer(Buffer::Tag tag) { + constexpr size_t BUFFER_SIZE{512}; + std::vector<s16> buffer(BUFFER_SIZE * stream->GetNumChannels()); + + for (auto& voice : voices) { + if (!voice.IsPlaying()) { + continue; + } + + size_t offset{}; + s64 samples_remaining{BUFFER_SIZE}; + while (samples_remaining > 0) { + const std::vector<s16> samples{voice.DequeueSamples(samples_remaining)}; + + if (samples.empty()) { + break; + } + + samples_remaining -= samples.size(); + + for (const auto& sample : samples) { + const s32 buffer_sample{buffer[offset]}; + buffer[offset++] = + ClampToS16(buffer_sample + static_cast<s32>(sample * voice.GetInfo().volume)); + } + } + } + audio_core->QueueBuffer(stream, tag, std::move(buffer)); +} + +void AudioRenderer::ReleaseAndQueueBuffers() { + const auto released_buffers{audio_core->GetTagsAndReleaseBuffers(stream, 2)}; + for (const auto& tag : released_buffers) { + QueueMixedBuffer(tag); + } +} + +} // namespace AudioCore diff --git a/src/audio_core/audio_renderer.h b/src/audio_core/audio_renderer.h new file mode 100644 index 000000000..6950a4681 --- /dev/null +++ b/src/audio_core/audio_renderer.h @@ -0,0 +1,206 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <memory> +#include <vector> + +#include "audio_core/audio_out.h" +#include "audio_core/codec.h" +#include "audio_core/stream.h" +#include "common/common_types.h" +#include "common/swap.h" +#include "core/hle/kernel/event.h" + +namespace AudioCore { + +enum class PlayState : u8 { + Started = 0, + Stopped = 1, + Paused = 2, +}; + +struct AudioRendererParameter { + u32_le sample_rate; + u32_le sample_count; + u32_le unknown_8; + u32_le unknown_c; + u32_le voice_count; + u32_le sink_count; + u32_le effect_count; + u32_le unknown_1c; + u8 unknown_20; + INSERT_PADDING_BYTES(3); + u32_le splitter_count; + u32_le unknown_2c; + INSERT_PADDING_WORDS(1); + u32_le revision; +}; +static_assert(sizeof(AudioRendererParameter) == 52, "AudioRendererParameter is an invalid size"); + +enum class MemoryPoolStates : u32 { // Should be LE + Invalid = 0x0, + Unknown = 0x1, + RequestDetach = 0x2, + Detached = 0x3, + RequestAttach = 0x4, + Attached = 0x5, + Released = 0x6, +}; + +struct MemoryPoolEntry { + MemoryPoolStates state; + u32_le unknown_4; + u32_le unknown_8; + u32_le unknown_c; +}; +static_assert(sizeof(MemoryPoolEntry) == 0x10, "MemoryPoolEntry has wrong size"); + +struct MemoryPoolInfo { + u64_le pool_address; + u64_le pool_size; + MemoryPoolStates pool_state; + INSERT_PADDING_WORDS(3); // Unknown +}; +static_assert(sizeof(MemoryPoolInfo) == 0x20, "MemoryPoolInfo has wrong size"); +struct BiquadFilter { + u8 enable; + INSERT_PADDING_BYTES(1); + std::array<s16_le, 3> numerator; + std::array<s16_le, 2> denominator; +}; +static_assert(sizeof(BiquadFilter) == 0xc, "BiquadFilter has wrong size"); + +struct WaveBuffer { + u64_le buffer_addr; + u64_le buffer_sz; + s32_le start_sample_offset; + s32_le end_sample_offset; + u8 is_looping; + u8 end_of_stream; + u8 sent_to_server; + INSERT_PADDING_BYTES(5); + u64 context_addr; + u64 context_sz; + INSERT_PADDING_BYTES(8); +}; +static_assert(sizeof(WaveBuffer) == 0x38, "WaveBuffer has wrong size"); + +struct VoiceInfo { + u32_le id; + u32_le node_id; + u8 is_new; + u8 is_in_use; + PlayState play_state; + u8 sample_format; + u32_le sample_rate; + u32_le priority; + u32_le sorting_order; + u32_le channel_count; + float_le pitch; + float_le volume; + std::array<BiquadFilter, 2> biquad_filter; + u32_le wave_buffer_count; + u32_le wave_buffer_head; + INSERT_PADDING_WORDS(1); + u64_le additional_params_addr; + u64_le additional_params_sz; + u32_le mix_id; + u32_le splitter_info_id; + std::array<WaveBuffer, 4> wave_buffer; + std::array<u32_le, 6> voice_channel_resource_ids; + INSERT_PADDING_BYTES(24); +}; +static_assert(sizeof(VoiceInfo) == 0x170, "VoiceInfo is wrong size"); + +struct VoiceOutStatus { + u64_le played_sample_count; + u32_le wave_buffer_consumed; + u32_le voice_drops_count; +}; +static_assert(sizeof(VoiceOutStatus) == 0x10, "VoiceOutStatus has wrong size"); + +struct UpdateDataHeader { + UpdateDataHeader() {} + + explicit UpdateDataHeader(const AudioRendererParameter& config) { + revision = Common::MakeMagic('R', 'E', 'V', '4'); // 5.1.0 Revision + behavior_size = 0xb0; + memory_pools_size = (config.effect_count + (config.voice_count * 4)) * 0x10; + voices_size = config.voice_count * 0x10; + voice_resource_size = 0x0; + effects_size = config.effect_count * 0x10; + mixes_size = 0x0; + sinks_size = config.sink_count * 0x20; + performance_manager_size = 0x10; + total_size = sizeof(UpdateDataHeader) + behavior_size + memory_pools_size + voices_size + + effects_size + sinks_size + performance_manager_size; + } + + u32_le revision; + u32_le behavior_size; + u32_le memory_pools_size; + u32_le voices_size; + u32_le voice_resource_size; + u32_le effects_size; + u32_le mixes_size; + u32_le sinks_size; + u32_le performance_manager_size; + INSERT_PADDING_WORDS(6); + u32_le total_size; +}; +static_assert(sizeof(UpdateDataHeader) == 0x40, "UpdateDataHeader has wrong size"); + +class AudioRenderer { +public: + AudioRenderer(AudioRendererParameter params, Kernel::SharedPtr<Kernel::Event> buffer_event); + std::vector<u8> UpdateAudioRenderer(const std::vector<u8>& input_params); + void QueueMixedBuffer(Buffer::Tag tag); + void ReleaseAndQueueBuffers(); + +private: + class VoiceState { + public: + bool IsPlaying() const { + return is_in_use && info.play_state == PlayState::Started; + } + + const VoiceOutStatus& GetOutStatus() const { + return out_status; + } + + const VoiceInfo& GetInfo() const { + return info; + } + + VoiceInfo& Info() { + return info; + } + + void SetWaveIndex(size_t index); + std::vector<s16> DequeueSamples(size_t sample_count); + void UpdateState(); + void RefreshBuffer(); + + private: + bool is_in_use{}; + bool is_refresh_pending{}; + size_t wave_index{}; + size_t offset{}; + Codec::ADPCMState adpcm_state{}; + std::vector<s16> samples; + VoiceOutStatus out_status{}; + VoiceInfo info{}; + }; + + AudioRendererParameter worker_params; + Kernel::SharedPtr<Kernel::Event> buffer_event; + std::vector<VoiceState> voices; + std::unique_ptr<AudioCore::AudioOut> audio_core; + AudioCore::StreamPtr stream; +}; + +} // namespace AudioCore diff --git a/src/audio_core/buffer.h b/src/audio_core/buffer.h new file mode 100644 index 000000000..a323b23ec --- /dev/null +++ b/src/audio_core/buffer.h @@ -0,0 +1,45 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <vector> + +#include "common/common_types.h" + +namespace AudioCore { + +/** + * Represents a buffer of audio samples to be played in an audio stream + */ +class Buffer { +public: + using Tag = u64; + + Buffer(Tag tag, std::vector<s16>&& samples) : tag{tag}, samples{std::move(samples)} {} + + /// Returns the raw audio data for the buffer + std::vector<s16>& Samples() { + return samples; + } + + /// Returns the raw audio data for the buffer + const std::vector<s16>& GetSamples() const { + return samples; + } + + /// Returns the buffer tag, this is provided by the game to the audout service + Tag GetTag() const { + return tag; + } + +private: + Tag tag; + std::vector<s16> samples; +}; + +using BufferPtr = std::shared_ptr<Buffer>; + +} // namespace AudioCore diff --git a/src/audio_core/codec.cpp b/src/audio_core/codec.cpp new file mode 100644 index 000000000..c3021403f --- /dev/null +++ b/src/audio_core/codec.cpp @@ -0,0 +1,77 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> + +#include "audio_core/codec.h" + +namespace AudioCore::Codec { + +std::vector<s16> DecodeADPCM(const u8* const data, size_t size, const ADPCM_Coeff& coeff, + ADPCMState& state) { + // GC-ADPCM with scale factor and variable coefficients. + // Frames are 8 bytes long containing 14 samples each. + // Samples are 4 bits (one nibble) long. + + constexpr size_t FRAME_LEN = 8; + constexpr size_t SAMPLES_PER_FRAME = 14; + constexpr std::array<int, 16> SIGNED_NIBBLES = { + {0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1}}; + + const size_t sample_count = (size / FRAME_LEN) * SAMPLES_PER_FRAME; + const size_t ret_size = + sample_count % 2 == 0 ? sample_count : sample_count + 1; // Ensure multiple of two. + std::vector<s16> ret(ret_size); + + int yn1 = state.yn1, yn2 = state.yn2; + + const size_t NUM_FRAMES = + (sample_count + (SAMPLES_PER_FRAME - 1)) / SAMPLES_PER_FRAME; // Round up. + for (size_t framei = 0; framei < NUM_FRAMES; framei++) { + const int frame_header = data[framei * FRAME_LEN]; + const int scale = 1 << (frame_header & 0xF); + const int idx = (frame_header >> 4) & 0x7; + + // Coefficients are fixed point with 11 bits fractional part. + const int coef1 = coeff[idx * 2 + 0]; + const int coef2 = coeff[idx * 2 + 1]; + + // Decodes an audio sample. One nibble produces one sample. + const auto decode_sample = [&](const int nibble) -> s16 { + const int xn = nibble * scale; + // We first transform everything into 11 bit fixed point, perform the second order + // digital filter, then transform back. + // 0x400 == 0.5 in 11 bit fixed point. + // Filter: y[n] = x[n] + 0.5 + c1 * y[n-1] + c2 * y[n-2] + int val = ((xn << 11) + 0x400 + coef1 * yn1 + coef2 * yn2) >> 11; + // Clamp to output range. + val = std::clamp<s32>(val, -32768, 32767); + // Advance output feedback. + yn2 = yn1; + yn1 = val; + return static_cast<s16>(val); + }; + + size_t outputi = framei * SAMPLES_PER_FRAME; + size_t datai = framei * FRAME_LEN + 1; + for (size_t i = 0; i < SAMPLES_PER_FRAME && outputi < sample_count; i += 2) { + const s16 sample1 = decode_sample(SIGNED_NIBBLES[data[datai] >> 4]); + ret[outputi] = sample1; + outputi++; + + const s16 sample2 = decode_sample(SIGNED_NIBBLES[data[datai] & 0xF]); + ret[outputi] = sample2; + outputi++; + + datai++; + } + } + + state.yn1 = yn1; + state.yn2 = yn2; + + return ret; +} + +} // namespace AudioCore::Codec diff --git a/src/audio_core/codec.h b/src/audio_core/codec.h new file mode 100644 index 000000000..3f845c42c --- /dev/null +++ b/src/audio_core/codec.h @@ -0,0 +1,44 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <array> +#include <vector> + +#include "common/common_types.h" + +namespace AudioCore::Codec { + +enum class PcmFormat : u32 { + Invalid = 0, + Int8 = 1, + Int16 = 2, + Int24 = 3, + Int32 = 4, + PcmFloat = 5, + Adpcm = 6, +}; + +/// See: Codec::DecodeADPCM +struct ADPCMState { + // Two historical samples from previous processed buffer, + // required for ADPCM decoding + s16 yn1; ///< y[n-1] + s16 yn2; ///< y[n-2] +}; + +using ADPCM_Coeff = std::array<s16, 16>; + +/** + * @param data Pointer to buffer that contains ADPCM data to decode + * @param size Size of buffer in bytes + * @param coeff ADPCM coefficients + * @param state ADPCM state, this is updated with new state + * @return Decoded stereo signed PCM16 data, sample_count in length + */ +std::vector<s16> DecodeADPCM(const u8* const data, size_t size, const ADPCM_Coeff& coeff, + ADPCMState& state); + +}; // namespace AudioCore::Codec diff --git a/src/audio_core/cubeb_sink.cpp b/src/audio_core/cubeb_sink.cpp new file mode 100644 index 000000000..1501ef1f4 --- /dev/null +++ b/src/audio_core/cubeb_sink.cpp @@ -0,0 +1,200 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <cstring> + +#include "audio_core/cubeb_sink.h" +#include "audio_core/stream.h" +#include "common/logging/log.h" + +namespace AudioCore { + +class SinkStreamImpl final : public SinkStream { +public: + SinkStreamImpl(cubeb* ctx, u32 sample_rate, u32 num_channels_, cubeb_devid output_device, + const std::string& name) + : ctx{ctx}, num_channels{num_channels_} { + + if (num_channels == 6) { + // 6-channel audio does not seem to work with cubeb + SDL, so we downsample this to 2 + // channel for now + is_6_channel = true; + num_channels = 2; + } + + cubeb_stream_params params{}; + params.rate = sample_rate; + params.channels = num_channels; + params.format = CUBEB_SAMPLE_S16NE; + params.layout = num_channels == 1 ? CUBEB_LAYOUT_MONO : CUBEB_LAYOUT_STEREO; + + u32 minimum_latency{}; + if (cubeb_get_min_latency(ctx, ¶ms, &minimum_latency) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error getting minimum latency"); + } + + if (cubeb_stream_init(ctx, &stream_backend, name.c_str(), nullptr, nullptr, output_device, + ¶ms, std::max(512u, minimum_latency), + &SinkStreamImpl::DataCallback, &SinkStreamImpl::StateCallback, + this) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error initializing cubeb stream"); + return; + } + + if (cubeb_stream_start(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error starting cubeb stream"); + return; + } + } + + ~SinkStreamImpl() { + if (!ctx) { + return; + } + + if (cubeb_stream_stop(stream_backend) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "Error stopping cubeb stream"); + } + + cubeb_stream_destroy(stream_backend); + } + + void EnqueueSamples(u32 num_channels, const std::vector<s16>& samples) override { + if (!ctx) { + return; + } + + queue.reserve(queue.size() + samples.size() * GetNumChannels()); + + if (is_6_channel) { + // Downsample 6 channels to 2 + const size_t sample_count_copy_size = samples.size() * 2; + queue.reserve(sample_count_copy_size); + for (size_t i = 0; i < samples.size(); i += num_channels) { + queue.push_back(samples[i]); + queue.push_back(samples[i + 1]); + } + } else { + // Copy as-is + std::copy(samples.begin(), samples.end(), std::back_inserter(queue)); + } + } + + u32 GetNumChannels() const { + return num_channels; + } + +private: + std::vector<std::string> device_list; + + cubeb* ctx{}; + cubeb_stream* stream_backend{}; + u32 num_channels{}; + bool is_6_channel{}; + + std::vector<s16> queue; + + static long DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* output_buffer, long num_frames); + static void StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state); +}; + +CubebSink::CubebSink(std::string target_device_name) { + if (cubeb_init(&ctx, "yuzu", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return; + } + + if (target_device_name != auto_device_name && !target_device_name.empty()) { + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + const auto collection_end{collection.device + collection.count}; + const auto device{std::find_if(collection.device, collection_end, + [&](const cubeb_device_info& device) { + return target_device_name == device.friendly_name; + })}; + if (device != collection_end) { + output_device = device->devid; + } + cubeb_device_collection_destroy(ctx, &collection); + } + } +} + +CubebSink::~CubebSink() { + if (!ctx) { + return; + } + + for (auto& sink_stream : sink_streams) { + sink_stream.reset(); + } + + cubeb_destroy(ctx); +} + +SinkStream& CubebSink::AcquireSinkStream(u32 sample_rate, u32 num_channels, + const std::string& name) { + sink_streams.push_back( + std::make_unique<SinkStreamImpl>(ctx, sample_rate, num_channels, output_device, name)); + return *sink_streams.back(); +} + +long SinkStreamImpl::DataCallback(cubeb_stream* stream, void* user_data, const void* input_buffer, + void* output_buffer, long num_frames) { + SinkStreamImpl* impl = static_cast<SinkStreamImpl*>(user_data); + u8* buffer = reinterpret_cast<u8*>(output_buffer); + + if (!impl) { + return {}; + } + + const size_t frames_to_write{ + std::min(impl->queue.size() / impl->GetNumChannels(), static_cast<size_t>(num_frames))}; + + memcpy(buffer, impl->queue.data(), frames_to_write * sizeof(s16) * impl->GetNumChannels()); + impl->queue.erase(impl->queue.begin(), + impl->queue.begin() + frames_to_write * impl->GetNumChannels()); + + if (frames_to_write < num_frames) { + // Fill the rest of the frames with silence + memset(buffer + frames_to_write * sizeof(s16) * impl->GetNumChannels(), 0, + (num_frames - frames_to_write) * sizeof(s16) * impl->GetNumChannels()); + } + + return num_frames; +} + +void SinkStreamImpl::StateCallback(cubeb_stream* stream, void* user_data, cubeb_state state) {} + +std::vector<std::string> ListCubebSinkDevices() { + std::vector<std::string> device_list; + cubeb* ctx; + + if (cubeb_init(&ctx, "Citra Device Enumerator", nullptr) != CUBEB_OK) { + LOG_CRITICAL(Audio_Sink, "cubeb_init failed"); + return {}; + } + + cubeb_device_collection collection; + if (cubeb_enumerate_devices(ctx, CUBEB_DEVICE_TYPE_OUTPUT, &collection) != CUBEB_OK) { + LOG_WARNING(Audio_Sink, "Audio output device enumeration not supported"); + } else { + for (size_t i = 0; i < collection.count; i++) { + const cubeb_device_info& device = collection.device[i]; + if (device.friendly_name) { + device_list.emplace_back(device.friendly_name); + } + } + cubeb_device_collection_destroy(ctx, &collection); + } + + cubeb_destroy(ctx); + return device_list; +} + +} // namespace AudioCore diff --git a/src/audio_core/cubeb_sink.h b/src/audio_core/cubeb_sink.h new file mode 100644 index 000000000..59cbf05e9 --- /dev/null +++ b/src/audio_core/cubeb_sink.h @@ -0,0 +1,32 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <string> +#include <vector> + +#include <cubeb/cubeb.h> + +#include "audio_core/sink.h" + +namespace AudioCore { + +class CubebSink final : public Sink { +public: + explicit CubebSink(std::string device_id); + ~CubebSink() override; + + SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels, + const std::string& name) override; + +private: + cubeb* ctx{}; + cubeb_devid output_device{}; + std::vector<SinkStreamPtr> sink_streams; +}; + +std::vector<std::string> ListCubebSinkDevices(); + +} // namespace AudioCore diff --git a/src/audio_core/null_sink.h b/src/audio_core/null_sink.h new file mode 100644 index 000000000..f235d93e5 --- /dev/null +++ b/src/audio_core/null_sink.h @@ -0,0 +1,27 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include "audio_core/sink.h" + +namespace AudioCore { + +class NullSink final : public Sink { +public: + explicit NullSink(std::string){}; + ~NullSink() override = default; + + SinkStream& AcquireSinkStream(u32 /*sample_rate*/, u32 /*num_channels*/, + const std::string& /*name*/) override { + return null_sink_stream; + } + +private: + struct NullSinkStreamImpl final : SinkStream { + void EnqueueSamples(u32 /*num_channels*/, const std::vector<s16>& /*samples*/) override {} + } null_sink_stream; +}; + +} // namespace AudioCore diff --git a/src/audio_core/sink.h b/src/audio_core/sink.h new file mode 100644 index 000000000..95c7b2b6e --- /dev/null +++ b/src/audio_core/sink.h @@ -0,0 +1,31 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <string> + +#include "audio_core/sink_stream.h" +#include "common/common_types.h" + +namespace AudioCore { + +constexpr char auto_device_name[] = "auto"; + +/** + * This class is an interface for an audio sink. An audio sink accepts samples in stereo signed + * PCM16 format to be output. Sinks *do not* handle resampling and expect the correct sample rate. + * They are dumb outputs. + */ +class Sink { +public: + virtual ~Sink() = default; + virtual SinkStream& AcquireSinkStream(u32 sample_rate, u32 num_channels, + const std::string& name) = 0; +}; + +using SinkPtr = std::unique_ptr<Sink>; + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.cpp b/src/audio_core/sink_details.cpp new file mode 100644 index 000000000..955ba20fb --- /dev/null +++ b/src/audio_core/sink_details.cpp @@ -0,0 +1,44 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <memory> +#include <string> +#include <vector> +#include "audio_core/null_sink.h" +#include "audio_core/sink_details.h" +#ifdef HAVE_CUBEB +#include "audio_core/cubeb_sink.h" +#endif +#include "common/logging/log.h" + +namespace AudioCore { + +// g_sink_details is ordered in terms of desirability, with the best choice at the top. +const std::vector<SinkDetails> g_sink_details = { +#ifdef HAVE_CUBEB + SinkDetails{"cubeb", &std::make_unique<CubebSink, std::string>, &ListCubebSinkDevices}, +#endif + SinkDetails{"null", &std::make_unique<NullSink, std::string>, + [] { return std::vector<std::string>{"null"}; }}, +}; + +const SinkDetails& GetSinkDetails(std::string sink_id) { + auto iter = + std::find_if(g_sink_details.begin(), g_sink_details.end(), + [sink_id](const auto& sink_detail) { return sink_detail.id == sink_id; }); + + if (sink_id == "auto" || iter == g_sink_details.end()) { + if (sink_id != "auto") { + LOG_ERROR(Audio, "AudioCore::SelectSink given invalid sink_id {}", sink_id); + } + // Auto-select. + // g_sink_details is ordered in terms of desirability, with the best choice at the front. + iter = g_sink_details.begin(); + } + + return *iter; +} + +} // namespace AudioCore diff --git a/src/audio_core/sink_details.h b/src/audio_core/sink_details.h new file mode 100644 index 000000000..ea666c554 --- /dev/null +++ b/src/audio_core/sink_details.h @@ -0,0 +1,35 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <functional> +#include <memory> +#include <utility> +#include <vector> + +namespace AudioCore { + +class Sink; + +struct SinkDetails { + using FactoryFn = std::function<std::unique_ptr<Sink>(std::string)>; + using ListDevicesFn = std::function<std::vector<std::string>()>; + + SinkDetails(const char* id_, FactoryFn factory_, ListDevicesFn list_devices_) + : id(id_), factory(std::move(factory_)), list_devices(std::move(list_devices_)) {} + + /// Name for this sink. + const char* id; + /// A method to call to construct an instance of this type of sink. + FactoryFn factory; + /// A method to call to list available devices. + ListDevicesFn list_devices; +}; + +extern const std::vector<SinkDetails> g_sink_details; + +const SinkDetails& GetSinkDetails(std::string sink_id); + +} // namespace AudioCore diff --git a/src/audio_core/sink_stream.h b/src/audio_core/sink_stream.h new file mode 100644 index 000000000..41b6736d8 --- /dev/null +++ b/src/audio_core/sink_stream.h @@ -0,0 +1,32 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <memory> +#include <vector> + +#include "common/common_types.h" + +namespace AudioCore { + +/** + * Accepts samples in stereo signed PCM16 format to be output. Sinks *do not* handle resampling and + * expect the correct sample rate. They are dumb outputs. + */ +class SinkStream { +public: + virtual ~SinkStream() = default; + + /** + * Feed stereo samples to sink. + * @param num_channels Number of channels used. + * @param samples Samples in interleaved stereo PCM16 format. + */ + virtual void EnqueueSamples(u32 num_channels, const std::vector<s16>& samples) = 0; +}; + +using SinkStreamPtr = std::unique_ptr<SinkStream>; + +} // namespace AudioCore diff --git a/src/audio_core/stream.cpp b/src/audio_core/stream.cpp new file mode 100644 index 000000000..ad9e2915c --- /dev/null +++ b/src/audio_core/stream.cpp @@ -0,0 +1,127 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <algorithm> +#include <cmath> + +#include "audio_core/sink.h" +#include "audio_core/sink_details.h" +#include "audio_core/stream.h" +#include "common/assert.h" +#include "common/logging/log.h" +#include "core/core_timing.h" +#include "core/core_timing_util.h" +#include "core/settings.h" + +namespace AudioCore { + +constexpr size_t MaxAudioBufferCount{32}; + +u32 Stream::GetNumChannels() const { + switch (format) { + case Format::Mono16: + return 1; + case Format::Stereo16: + return 2; + case Format::Multi51Channel16: + return 6; + } + LOG_CRITICAL(Audio, "Unimplemented format={}", static_cast<u32>(format)); + UNREACHABLE(); + return {}; +} + +Stream::Stream(u32 sample_rate, Format format, ReleaseCallback&& release_callback, + SinkStream& sink_stream, std::string&& name_) + : sample_rate{sample_rate}, format{format}, release_callback{std::move(release_callback)}, + sink_stream{sink_stream}, name{std::move(name_)} { + + release_event = CoreTiming::RegisterEvent( + name, [this](u64 userdata, int cycles_late) { ReleaseActiveBuffer(); }); +} + +void Stream::Play() { + state = State::Playing; + PlayNextBuffer(); +} + +void Stream::Stop() { + ASSERT_MSG(false, "Unimplemented"); +} + +s64 Stream::GetBufferReleaseCycles(const Buffer& buffer) const { + const size_t num_samples{buffer.GetSamples().size() / GetNumChannels()}; + return CoreTiming::usToCycles((static_cast<u64>(num_samples) * 1000000) / sample_rate); +} + +static void VolumeAdjustSamples(std::vector<s16>& samples) { + const float volume{std::clamp(Settings::values.volume, 0.0f, 1.0f)}; + + if (volume == 1.0f) { + return; + } + + // Implementation of a volume slider with a dynamic range of 60 dB + const float volume_scale_factor{std::exp(6.90775f * volume) * 0.001f}; + for (auto& sample : samples) { + sample = static_cast<s16>(sample * volume_scale_factor); + } +} + +void Stream::PlayNextBuffer() { + if (!IsPlaying()) { + // Ensure we are in playing state before playing the next buffer + return; + } + + if (active_buffer) { + // Do not queue a new buffer if we are already playing a buffer + return; + } + + if (queued_buffers.empty()) { + // No queued buffers - we are effectively paused + return; + } + + active_buffer = queued_buffers.front(); + queued_buffers.pop(); + + VolumeAdjustSamples(active_buffer->Samples()); + sink_stream.EnqueueSamples(GetNumChannels(), active_buffer->GetSamples()); + + CoreTiming::ScheduleEventThreadsafe(GetBufferReleaseCycles(*active_buffer), release_event, {}); +} + +void Stream::ReleaseActiveBuffer() { + ASSERT(active_buffer); + released_buffers.push(std::move(active_buffer)); + release_callback(); + PlayNextBuffer(); +} + +bool Stream::QueueBuffer(BufferPtr&& buffer) { + if (queued_buffers.size() < MaxAudioBufferCount) { + queued_buffers.push(std::move(buffer)); + PlayNextBuffer(); + return true; + } + return false; +} + +bool Stream::ContainsBuffer(Buffer::Tag tag) const { + ASSERT_MSG(false, "Unimplemented"); + return {}; +} + +std::vector<Buffer::Tag> Stream::GetTagsAndReleaseBuffers(size_t max_count) { + std::vector<Buffer::Tag> tags; + for (size_t count = 0; count < max_count && !released_buffers.empty(); ++count) { + tags.push_back(released_buffers.front()->GetTag()); + released_buffers.pop(); + } + return tags; +} + +} // namespace AudioCore diff --git a/src/audio_core/stream.h b/src/audio_core/stream.h new file mode 100644 index 000000000..049b92ca9 --- /dev/null +++ b/src/audio_core/stream.h @@ -0,0 +1,102 @@ +// Copyright 2018 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <functional> +#include <memory> +#include <string> +#include <vector> +#include <queue> + +#include "audio_core/buffer.h" +#include "audio_core/sink_stream.h" +#include "common/assert.h" +#include "common/common_types.h" +#include "core/core_timing.h" + +namespace AudioCore { + +/** + * Represents an audio stream, which is a sequence of queued buffers, to be outputed by AudioOut + */ +class Stream { +public: + /// Audio format of the stream + enum class Format { + Mono16, + Stereo16, + Multi51Channel16, + }; + + /// Callback function type, used to change guest state on a buffer being released + using ReleaseCallback = std::function<void()>; + + Stream(u32 sample_rate, Format format, ReleaseCallback&& release_callback, + SinkStream& sink_stream, std::string&& name_); + + /// Plays the audio stream + void Play(); + + /// Stops the audio stream + void Stop(); + + /// Queues a buffer into the audio stream, returns true on success + bool QueueBuffer(BufferPtr&& buffer); + + /// Returns true if the audio stream contains a buffer with the specified tag + bool ContainsBuffer(Buffer::Tag tag) const; + + /// Returns a vector of recently released buffers specified by tag + std::vector<Buffer::Tag> GetTagsAndReleaseBuffers(size_t max_count); + + /// Returns true if the stream is currently playing + bool IsPlaying() const { + return state == State::Playing; + } + + /// Returns the number of queued buffers + size_t GetQueueSize() const { + return queued_buffers.size(); + } + + /// Gets the sample rate + u32 GetSampleRate() const { + return sample_rate; + } + + /// Gets the number of channels + u32 GetNumChannels() const; + +private: + /// Current state of the stream + enum class State { + Stopped, + Playing, + }; + + /// Plays the next queued buffer in the audio stream, starting playback if necessary + void PlayNextBuffer(); + + /// Releases the actively playing buffer, signalling that it has been completed + void ReleaseActiveBuffer(); + + /// Gets the number of core cycles when the specified buffer will be released + s64 GetBufferReleaseCycles(const Buffer& buffer) const; + + u32 sample_rate; ///< Sample rate of the stream + Format format; ///< Format of the stream + ReleaseCallback release_callback; ///< Buffer release callback for the stream + State state{State::Stopped}; ///< Playback state of the stream + CoreTiming::EventType* release_event{}; ///< Core timing release event for the stream + BufferPtr active_buffer; ///< Actively playing buffer in the stream + std::queue<BufferPtr> queued_buffers; ///< Buffers queued to be played in the stream + std::queue<BufferPtr> released_buffers; ///< Buffers recently released from the stream + SinkStream& sink_stream; ///< Output sink for the stream + std::string name; ///< Name of the stream, must be unique +}; + +using StreamPtr = std::shared_ptr<Stream>; + +} // namespace AudioCore |
