generated from CoolLibs/library-template
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🚚 Move code from RtAudioWrapper to here
- Loading branch information
1 parent
0bad2c9
commit 34db1ac
Showing
11 changed files
with
544 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
Submodule RtAudioWrapper
updated
9 files
+0 −204 | .github/workflows/build_and_run_tests.yml | |
+5 −33 | CMakeLists.txt | |
+1 −3 | include/RtAudioWrapper/RtAudioWrapper.hpp | |
+0 −99 | src/InputStream.cpp | |
+0 −53 | src/InputStream.hpp | |
+0 −208 | src/Player.cpp | |
+0 −119 | src/Player.hpp | |
+0 −49 | tests/CMakeLists.txt | |
+0 −9 | tests/tests.cpp |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, ¶ms, 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.