mirror of
https://github.com/zhenyan121/Cubed.git
synced 2026-08-08 17:57:02 +08:00
feat(audio): implement voice chat with Opus codec and streaming audio source
Add AudioStreamSource for continuous PCM playback. Refactor voice send/receive to encode with Opus and stream to a dedicated source instead of per-frame sources. Enable voice transmission over network.
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
#include "Cubed/audio/audio_fade.hpp"
|
#include "Cubed/audio/audio_fade.hpp"
|
||||||
#include "Cubed/audio/audio_recording.hpp"
|
#include "Cubed/audio/audio_recording.hpp"
|
||||||
#include "Cubed/audio/audio_source.hpp"
|
#include "Cubed/audio/audio_source.hpp"
|
||||||
|
#include "Cubed/audio/audio_stream_source.hpp"
|
||||||
#include "Cubed/audio/sound_manager.hpp"
|
#include "Cubed/audio/sound_manager.hpp"
|
||||||
#include "Cubed/audio/source_pool.hpp"
|
#include "Cubed/audio/source_pool.hpp"
|
||||||
#include "Cubed/config.hpp"
|
#include "Cubed/config.hpp"
|
||||||
@@ -70,6 +71,8 @@ private:
|
|||||||
bool m_underwater = false;
|
bool m_underwater = false;
|
||||||
float m_music_volume = 1.0f;
|
float m_music_volume = 1.0f;
|
||||||
float m_sfx_volume = 1.0f;
|
float m_sfx_volume = 1.0f;
|
||||||
|
|
||||||
|
std::unique_ptr<AudioStreamSource> m_voice_source;
|
||||||
std::unique_ptr<AudioFilter> m_low_pass_filter;
|
std::unique_ptr<AudioFilter> m_low_pass_filter;
|
||||||
std::unique_ptr<AudioEffect> m_underwater_effect;
|
std::unique_ptr<AudioEffect> m_underwater_effect;
|
||||||
std::unique_ptr<AudioEffectSlot> m_underwater_slot;
|
std::unique_ptr<AudioEffectSlot> m_underwater_slot;
|
||||||
|
|||||||
31
include/Cubed/audio/audio_stream_source.hpp
Normal file
31
include/Cubed/audio/audio_stream_source.hpp
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <al.h>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
namespace Cubed {
|
||||||
|
class AudioStreamSource {
|
||||||
|
public:
|
||||||
|
static constexpr int NUM_BUFFERS = 4;
|
||||||
|
AudioStreamSource();
|
||||||
|
AudioStreamSource(const AudioStreamSource&) = delete;
|
||||||
|
AudioStreamSource(AudioStreamSource&&) = delete;
|
||||||
|
AudioStreamSource& operator=(const AudioStreamSource&) = delete;
|
||||||
|
AudioStreamSource& operator=(AudioStreamSource&&) = delete;
|
||||||
|
~AudioStreamSource();
|
||||||
|
|
||||||
|
void push_pcm(std::span<const int16_t> pcm, ALsizei sample_rate);
|
||||||
|
|
||||||
|
void stop();
|
||||||
|
bool is_playing() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ALuint m_source = 0;
|
||||||
|
std::array<ALuint, NUM_BUFFERS> m_buffers{};
|
||||||
|
std::vector<ALuint> m_free_buffers;
|
||||||
|
bool m_playing = false;
|
||||||
|
|
||||||
|
void unqueue_processed();
|
||||||
|
};
|
||||||
|
} // namespace Cubed
|
||||||
@@ -87,4 +87,5 @@ target_sources(${PROJECT_NAME}
|
|||||||
tools/system_locate.cpp
|
tools/system_locate.cpp
|
||||||
ui/chat_box.cpp
|
ui/chat_box.cpp
|
||||||
audio/audio_recording.cpp
|
audio/audio_recording.cpp
|
||||||
|
audio/audio_stream_source.cpp
|
||||||
)
|
)
|
||||||
@@ -23,10 +23,11 @@ AudioEngine::~AudioEngine() {
|
|||||||
m_bgm.reset();
|
m_bgm.reset();
|
||||||
m_pool.reset();
|
m_pool.reset();
|
||||||
m_sounds.clear();
|
m_sounds.clear();
|
||||||
|
m_voice_source.reset();
|
||||||
m_low_pass_filter.reset();
|
m_low_pass_filter.reset();
|
||||||
m_underwater_effect.reset();
|
m_underwater_effect.reset();
|
||||||
m_underwater_slot.reset();
|
m_underwater_slot.reset();
|
||||||
|
|
||||||
opus_encoder_destroy(m_encoder);
|
opus_encoder_destroy(m_encoder);
|
||||||
opus_decoder_destroy(m_decoder);
|
opus_decoder_destroy(m_decoder);
|
||||||
alcMakeContextCurrent(nullptr);
|
alcMakeContextCurrent(nullptr);
|
||||||
@@ -118,6 +119,8 @@ void AudioEngine::init() {
|
|||||||
Logger::info("Audio Engine Init Success");
|
Logger::info("Audio Engine Init Success");
|
||||||
|
|
||||||
m_init = true;
|
m_init = true;
|
||||||
|
|
||||||
|
m_voice_source = std::make_unique<AudioStreamSource>();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioEngine::play_bgm() { m_bgm->play(); }
|
void AudioEngine::play_bgm() { m_bgm->play(); }
|
||||||
@@ -254,27 +257,7 @@ void AudioEngine::set_client(std::weak_ptr<NetworkClient> client) {
|
|||||||
|
|
||||||
void AudioEngine::send_voice(
|
void AudioEngine::send_voice(
|
||||||
const std::array<int16_t, AudioRecording::FRAME_SAMPLES>& pcm) {
|
const std::array<int16_t, AudioRecording::FRAME_SAMPLES>& pcm) {
|
||||||
{
|
|
||||||
AudioData data;
|
|
||||||
data.pcm = {pcm.begin(), pcm.end()};
|
|
||||||
data.channels = 1;
|
|
||||||
data.sample_rate = AudioRecording::SAMPLE_RATE;
|
|
||||||
auto* source = m_pool->acquire();
|
|
||||||
source->set_volume(m_sfx_volume);
|
|
||||||
if (!source) {
|
|
||||||
Logger::error("Source is Full");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
std::unique_ptr<AudioBuffer> buffer =
|
|
||||||
std::make_unique<AudioBuffer>(data);
|
|
||||||
|
|
||||||
if (m_efx_supported && m_underwater) {
|
|
||||||
source->set_filter(*m_low_pass_filter);
|
|
||||||
source->set_effect_slot(*m_underwater_slot);
|
|
||||||
}
|
|
||||||
source->play_2d(std::move(buffer));
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
std::array<uint8_t, OPUS_MAX_PACKET_SIZE> opus;
|
std::array<uint8_t, OPUS_MAX_PACKET_SIZE> opus;
|
||||||
int len = opus_encode(m_encoder, pcm.data(), AudioRecording::FRAME_SAMPLES,
|
int len = opus_encode(m_encoder, pcm.data(), AudioRecording::FRAME_SAMPLES,
|
||||||
opus.data(), opus.size());
|
opus.data(), opus.size());
|
||||||
@@ -283,7 +266,6 @@ void AudioEngine::send_voice(
|
|||||||
Logger::error("Opus encode failed: {}", opus_strerror(len));
|
Logger::error("Opus encode failed: {}", opus_strerror(len));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger::info("opus encode len={}", len);
|
|
||||||
if (auto c = m_client.lock()) {
|
if (auto c = m_client.lock()) {
|
||||||
Arena arena;
|
Arena arena;
|
||||||
auto msg = Arena::Create<VoiceMsg>(&arena);
|
auto msg = Arena::Create<VoiceMsg>(&arena);
|
||||||
@@ -296,37 +278,18 @@ void AudioEngine::send_voice(
|
|||||||
pos->set_z(p.z);
|
pos->set_z(p.z);
|
||||||
c->send(make_packet(*msg));
|
c->send(make_packet(*msg));
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
void AudioEngine::receive_voice(std::span<char> opus, const glm::vec3& pos) {
|
void AudioEngine::receive_voice(std::span<char> opus, const glm::vec3& pos) {
|
||||||
AudioData data;
|
std::array<int16_t, AudioRecording::FRAME_SAMPLES> pcm;
|
||||||
data.pcm.resize(AudioRecording::FRAME_SAMPLES);
|
int len =
|
||||||
data.channels = 1;
|
opus_decode(m_decoder, reinterpret_cast<const uint8_t*>(opus.data()),
|
||||||
data.sample_rate = AudioRecording::SAMPLE_RATE;
|
opus.size(), pcm.data(), AudioRecording::FRAME_SAMPLES, 0);
|
||||||
int len = opus_decode(
|
|
||||||
m_decoder, reinterpret_cast<const uint8_t*>(opus.data()), opus.size(),
|
|
||||||
data.pcm.data(), AudioRecording::FRAME_SAMPLES, 0);
|
|
||||||
if (len < 0) {
|
if (len < 0) {
|
||||||
Logger::error("Opus decode failed: {}", opus_strerror(len));
|
Logger::error("Opus decode failed: {}", opus_strerror(len));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Logger::info("decode samples={}", len);
|
m_voice_source->push_pcm(std::span(pcm.data(), len),
|
||||||
Logger::info("Receive Vocie and start play");
|
AudioRecording::SAMPLE_RATE);
|
||||||
auto* source = m_pool->acquire();
|
|
||||||
source->set_volume(m_sfx_volume);
|
|
||||||
if (!source) {
|
|
||||||
Logger::error("Source is Full");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < 10; ++i)
|
|
||||||
Logger::info("recv {}", data.pcm[i]);
|
|
||||||
std::unique_ptr<AudioBuffer> buffer = std::make_unique<AudioBuffer>(data);
|
|
||||||
|
|
||||||
if (m_efx_supported && m_underwater) {
|
|
||||||
source->set_filter(*m_low_pass_filter);
|
|
||||||
source->set_effect_slot(*m_underwater_slot);
|
|
||||||
}
|
|
||||||
source->play_3d(std::move(buffer), pos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioRecording& AudioEngine::audio_recording() { return m_recording; }
|
AudioRecording& AudioEngine::audio_recording() { return m_recording; }
|
||||||
|
|||||||
@@ -60,9 +60,6 @@ void AudioRecording::update() {
|
|||||||
while (available >= FRAME_SAMPLES) {
|
while (available >= FRAME_SAMPLES) {
|
||||||
std::array<int16_t, FRAME_SAMPLES> pcm;
|
std::array<int16_t, FRAME_SAMPLES> pcm;
|
||||||
alcCaptureSamples(m_capture, pcm.data(), FRAME_SAMPLES);
|
alcCaptureSamples(m_capture, pcm.data(), FRAME_SAMPLES);
|
||||||
for (int i = 0; i < 20; i++) {
|
|
||||||
Logger::info("pcm[{}]={}", i, pcm[i]);
|
|
||||||
}
|
|
||||||
send_voice(pcm);
|
send_voice(pcm);
|
||||||
available -= FRAME_SAMPLES;
|
available -= FRAME_SAMPLES;
|
||||||
}
|
}
|
||||||
|
|||||||
71
src/audio/audio_stream_source.cpp
Normal file
71
src/audio/audio_stream_source.cpp
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#include "Cubed/audio/audio_stream_source.hpp"
|
||||||
|
|
||||||
|
#include "Cubed/tools/log.hpp"
|
||||||
|
namespace Cubed {
|
||||||
|
AudioStreamSource::AudioStreamSource() {
|
||||||
|
alGenBuffers(NUM_BUFFERS, m_buffers.data());
|
||||||
|
alGenSources(1, &m_source);
|
||||||
|
|
||||||
|
alSourcei(m_source, AL_SOURCE_RELATIVE, AL_TRUE);
|
||||||
|
alSource3f(m_source, AL_POSITION, 0.0f, 0.0f, 0.0f);
|
||||||
|
m_free_buffers.assign(m_buffers.begin(), m_buffers.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioStreamSource::~AudioStreamSource() {
|
||||||
|
stop();
|
||||||
|
alDeleteSources(1, &m_source);
|
||||||
|
alDeleteBuffers(NUM_BUFFERS, m_buffers.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioStreamSource::push_pcm(std::span<const int16_t> pcm,
|
||||||
|
ALsizei sample_rate) {
|
||||||
|
if (!m_source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
unqueue_processed();
|
||||||
|
|
||||||
|
if (m_free_buffers.empty()) {
|
||||||
|
Logger::warn("Queue is full, dropping frame");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ALuint buf = m_free_buffers.back();
|
||||||
|
m_free_buffers.pop_back();
|
||||||
|
|
||||||
|
alBufferData(buf, AL_FORMAT_MONO16, pcm.data(),
|
||||||
|
static_cast<ALsizei>(pcm.size() * sizeof(int16_t)),
|
||||||
|
sample_rate);
|
||||||
|
alSourceQueueBuffers(m_source, 1, &buf);
|
||||||
|
|
||||||
|
ALint state;
|
||||||
|
alGetSourcei(m_source, AL_SOURCE_STATE, &state);
|
||||||
|
if (state != AL_PLAYING) {
|
||||||
|
alSourcePlay(m_source);
|
||||||
|
m_playing = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioStreamSource::stop() {
|
||||||
|
if (!m_source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alSourceStop(m_source);
|
||||||
|
|
||||||
|
alSourcei(m_source, AL_BUFFER, 0);
|
||||||
|
m_free_buffers.assign(m_buffers.begin(), m_buffers.end());
|
||||||
|
m_playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioStreamSource::unqueue_processed() {
|
||||||
|
ALint processed = 0;
|
||||||
|
alGetSourcei(m_source, AL_BUFFERS_PROCESSED, &processed);
|
||||||
|
|
||||||
|
while (processed-- > 0) {
|
||||||
|
ALuint buf;
|
||||||
|
alSourceUnqueueBuffers(m_source, 1, &buf);
|
||||||
|
m_free_buffers.push_back(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Cubed
|
||||||
Reference in New Issue
Block a user