Skip to content

Commit

Permalink
🚚 Move code from RtAudioWrapper to here
Browse files Browse the repository at this point in the history
  • Loading branch information
JulesFouchy committed Nov 5, 2023
1 parent 0bad2c9 commit 34db1ac
Show file tree
Hide file tree
Showing 11 changed files with 544 additions and 56 deletions.
3 changes: 2 additions & 1 deletion include/Audio/Audio.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "../../src/InputStream.hpp"
#include "../../src/Player.hpp"
#include "../../src/compute_volume.hpp"
#include "../../src/load_audio_file.hpp"
#include "RtAudioWrapper/RtAudioWrapper.hpp"
#include "dj_fft.h"
2 changes: 1 addition & 1 deletion lib/RtAudioWrapper
99 changes: 99 additions & 0 deletions src/InputStream.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#include "InputStream.hpp"
#include <mutex>
#include <span>

namespace Audio {

InputStream::InputStream(RtAudioErrorCallback error_callback)
{
_backend.setErrorCallback(std::move(error_callback));
set_device(_backend.getDefaultInputDevice());
}

auto InputStream::device_ids() const -> std::vector<unsigned int>
{
auto ids = _backend.getDeviceIds();
// Keep only the input devices
std::erase_if(ids, [&](unsigned int id) {
auto const info = _backend.getDeviceInfo(id);
return info.inputChannels == 0;
});
return ids;
}

auto InputStream::device_info(unsigned int device_id) const -> RtAudio::DeviceInfo
{
return _backend.getDeviceInfo(device_id);
}

void InputStream::shrink_samples_to_fit()
{
while (_samples.size() > _nb_of_retained_samples && !_samples.empty())
_samples.pop_front();
}

void InputStream::set_nb_of_retained_samples(size_t samples_count)
{
_nb_of_retained_samples = samples_count;
// shrink_samples_to_fit(); // Don't shrink here, this will be done during `for_each_sample()`. This avoids locking too often.
}

void InputStream::for_each_sample(int64_t samples_count, std::function<void(float)> const& callback)
{
auto const samples = [&]() {
std::lock_guard const lock{_samples_mutex}; // Lock while we copy
shrink_samples_to_fit(); // Might not be fit, if set_nb_of_retained_samples() has been called.
return _samples;
}();
for ( // Take the `samples_count` last elements of `samples`.
int64_t i = static_cast<int64_t>(samples.size()) - samples_count;
i < static_cast<int64_t>(samples.size());
++i
)
{
if (i < 0) // If `samples` has less than `samples_count` elements this will happen.
callback(0.f);
else
callback(samples[static_cast<size_t>(i)]);
}
}

auto audio_input_callback(void* /* output_buffer */, void* input_buffer, unsigned int frames_count, double /* stream_time */, RtAudioStreamStatus /* status */, void* user_data) -> int
{
auto const input = std::span{static_cast<float*>(input_buffer), frames_count};
auto& This = *static_cast<InputStream*>(user_data);

{
std::lock_guard const lock{This._samples_mutex};
for (float const sample : input)
{
This._samples.push_back(sample);
This.shrink_samples_to_fit();
}
}
return 0;
}

void InputStream::set_device(unsigned int device_id)
{
if (_backend.isStreamOpen())
_backend.closeStream();

{ // Clear the samples, they do not correspond to the new device. (Shouldn't really matter, but I guess this is technically more correct)
std::lock_guard const lock{_samples_mutex};
_samples.clear();
}

auto const info = _backend.getDeviceInfo(device_id);
RtAudio::StreamParameters params;
params.deviceId = device_id;
params.nChannels = 1;
unsigned int nb_frames{512}; // 512 is a decent value that seems to work well.
auto const sample_rate = info.preferredSampleRate; // TODO(Audio-Philippe) Should we use preferredSampleRate or currentSampleRate?
_backend.openStream(nullptr, &params, RTAUDIO_FLOAT32, sample_rate, &nb_frames, &audio_input_callback, this);
_backend.startStream();
_current_input_device_name = info.name;
_current_input_device_sample_rate = sample_rate;
}

} // namespace Audio
52 changes: 52 additions & 0 deletions src/InputStream.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#pragma once
#include <deque>
#include <mutex>
#include "rtaudio/RtAudio.h"

namespace Audio {

class InputStream {
public:
explicit InputStream(RtAudioErrorCallback);
~InputStream() = default;
InputStream(InputStream const&) = delete; //
auto operator=(InputStream const&) -> InputStream& = delete; // Can't copy nor move
InputStream(InputStream&&) noexcept = delete; // because we pass the address of this object to the audio callback.
auto operator=(InputStream&&) noexcept -> InputStream& = delete; //

/// Calls the callback for each of the `samples_count` latest samples received through the device.
/// This data is always mono-channel, 1 sample == 1 frame.
void for_each_sample(int64_t samples_count, std::function<void(float)> const& callback);
/// You MUST call this function at least once at the beginning to tell us the maximum numbers of samples you will query with `for_each_sample`.
/// If that max number changes over time, you can call this function again to update it.
void set_nb_of_retained_samples(size_t samples_count);

/// Returns the list of all the ids of input devices.
auto device_ids() const -> std::vector<unsigned int>;
/// Returns all the info about a given device.
auto device_info(unsigned int device_id) const -> RtAudio::DeviceInfo;
///
auto current_device_name() const -> std::string const& { return _current_input_device_name; }
/// Returns the sample rate of the currently used device.
auto sample_rate() const -> unsigned int { return _current_input_device_sample_rate; }
/// Sets the device to use.
/// By default, when an InputStream is created it uses the default input device selected by the OS.
void set_device(unsigned int device_id);

private:
friend auto audio_input_callback(void* output_buffer, void* input_buffer, unsigned int frames_count, double stream_time, RtAudioStreamStatus status, void* user_data) -> int;

/// /!\ YOU MUST LOCK `_samples_mutex` before using this function
void shrink_samples_to_fit();

private:
std::deque<float> _samples{};
size_t _nb_of_retained_samples{256};
std::mutex _samples_mutex{};

mutable RtAudio _backend{};
std::string _current_input_device_name{};
unsigned int _current_input_device_sample_rate{};
};

} // namespace Audio
208 changes: 208 additions & 0 deletions src/Player.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
#include "Player.hpp"
#include <cassert>

namespace Audio {

static constexpr int64_t output_channels_count = 2;

static auto backend() -> RtAudio&
{
static RtAudio instance{};
return instance;
}

#ifndef NDEBUG // Only used by the assert, so unused in Release, which would cause a warning.
static auto is_API_available() -> bool
{
std::vector<RtAudio::Api> apis;
RtAudio::getCompiledApi(apis);
return apis[0] != RtAudio::Api::RTAUDIO_DUMMY;
}
#endif

Player::Player()
{
assert(is_API_available());
update_device_if_necessary();
}

void Player::update_device_if_necessary()
{
auto const id = backend().getDefaultOutputDevice();
if (id == _current_output_device_id)
return;

_current_output_device_id = id;
recreate_stream_adapted_to_current_audio_data();
}

auto Player::has_audio_data() const -> bool
{
return !_data.samples.empty();
}

auto Player::has_device() const -> bool
{
return _current_output_device_id != 0;
}

auto audio_callback(void* output_buffer, void* /* input_buffer */, unsigned int frames_count, double /* stream_time */, RtAudioStreamStatus /* status */, void* user_data) -> int
{
auto* out_buffer = static_cast<float*>(output_buffer);
auto& player = *static_cast<Player*>(user_data);

for (int64_t frame_idx = 0; frame_idx < frames_count; frame_idx++)
{
for (int64_t channel_idx = 0; channel_idx < output_channels_count; ++channel_idx)
{
out_buffer[frame_idx * output_channels_count + channel_idx] = // NOLINT(*pointer-arithmetic)
player._is_playing
? player.sample(player._next_frame_to_play, channel_idx)
: 0.f;
}
if (player._is_playing)
++player._next_frame_to_play;
}

return 0;
}

void Player::recreate_stream_adapted_to_current_audio_data()
{
if (backend().isStreamOpen())
backend().closeStream();

if (!has_audio_data()
|| !has_device())
return;

RtAudio::StreamParameters _parameters;
_parameters.deviceId = _current_output_device_id;
_parameters.firstChannel = 0;
_parameters.nChannels = output_channels_count;
unsigned int nb_frames_per_callback{128};

backend().openStream(
&_parameters,
nullptr, // No input stream needed
RTAUDIO_FLOAT32,
_data.sample_rate, // TODO(Audio-Philippe) Resample the audio data to make it match the preferredSampleRate of the device. The current strategy works, unless the device does not support the sample_rate used by our audio data, in which case the audio will be played too slow or too fast.
&nb_frames_per_callback,
&audio_callback,
this
);

backend().startStream();
}

void Player::set_audio_data(AudioData data)
{
if (backend().isStreamOpen())
backend().closeStream(); // Otherwise data race with the audio thread that is reading _audio_data. Could cause crashes.

float const current_time = get_time();

_data = std::move(data);
set_time(current_time); // Need to adjust the _next_frame_to_play so that we will be at the same point in time in both audios even if they have different sample rates.

recreate_stream_adapted_to_current_audio_data();
}

void Player::reset_audio_data()
{
set_audio_data({});
}

void Player::play()
{
_is_playing = true;
}

void Player::pause()
{
_is_playing = false;
}

void Player::set_time(float time_in_seconds)
{
_next_frame_to_play = static_cast<int64_t>(
static_cast<float>(_data.sample_rate)
* time_in_seconds
);
}

auto Player::get_time() const -> float
{
if (_data.sample_rate == 0)
return 0.f;
return static_cast<float>(_next_frame_to_play)
/ static_cast<float>(_data.sample_rate);
}

static auto mod(int64_t a, int64_t b) -> int64_t
{
auto res = a % b;
if (res < 0)
res += b;
return res;
}

auto Player::sample(int64_t frame_index, int64_t channel_index) const -> float
{
if (_properties.is_muted)
return 0.f;
return _properties.volume * sample_unaltered_volume(frame_index, channel_index);
}

auto Player::sample_unaltered_volume(int64_t frame_index, int64_t channel_index) const -> float
{
if (!has_audio_data())
return 0.f;

auto const sample_index = frame_index * _data.channels_count
+ channel_index % _data.channels_count;
if ((sample_index < 0
|| sample_index >= static_cast<int64_t>(_data.samples.size())
)
&& !_properties.does_loop)
return 0.f;

return _data.samples[static_cast<size_t>(mod(sample_index, static_cast<int64_t>(_data.samples.size())))];
}

auto Player::sample(int64_t frame_index) const -> float
{
// The arithmetic mean is a good way of combining the values of the different channels, according to ChatGPT.
float res{0.f};
for (unsigned int i = 0; i < _data.channels_count; ++i)
res += sample(frame_index, i);
return res / static_cast<float>(_data.channels_count);
}

auto Player::sample_unaltered_volume(int64_t frame_index) const -> float
{
// The arithmetic mean is a good way of combining the values of the different channels, according to ChatGPT.
float res{0.f};
for (unsigned int i = 0; i < _data.channels_count; ++i)
res += sample_unaltered_volume(frame_index, i);
return res / static_cast<float>(_data.channels_count);
}

void set_error_callback(RtAudioErrorCallback callback)
{
backend().setErrorCallback(std::move(callback));
}

auto player() -> Player&
{
static Player instance{};
return instance;
}

void shut_down()
{
if (backend().isStreamOpen())
backend().closeStream();
}

} // namespace Audio
Loading

0 comments on commit 34db1ac

Please sign in to comment.