#include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto pi = 3.141592653589793238462643383279502884; auto no_device_config() -> sonus::audio_config { auto config = sonus::audio_config{}; config.no_device = true; return config; } auto test_file(std::string_view name) -> std::filesystem::path { return std::filesystem::temp_directory_path() / name; } auto peak(std::vector const& samples, std::size_t begin_frame, std::size_t frame_count, std::size_t channels) -> std::float32_t { auto result = 0.0F; auto const begin = begin_frame * channels; auto const end = std::min(samples.size(), begin + frame_count * channels); for (auto index = begin; index < end; ++index) { result = std::max(result, std::abs(samples[index])); } return result; } auto write_bytes(std::ofstream& output, std::string_view value) -> void { output.write(value.data(), static_cast(value.size())); } auto write_u16_le(std::ofstream& output, std::uint16_t value) -> void { auto bytes = std::array{ static_cast(value & 0xFFU), static_cast((value >> 8U) & 0xFFU), }; output.write(bytes.data(), static_cast(bytes.size())); } auto write_u32_le(std::ofstream& output, std::uint32_t value) -> void { auto bytes = std::array{ static_cast(value & 0xFFU), static_cast((value >> 8U) & 0xFFU), static_cast((value >> 16U) & 0xFFU), static_cast((value >> 24U) & 0xFFU), }; output.write(bytes.data(), static_cast(bytes.size())); } auto write_test_wav(std::filesystem::path const& path, std::uint32_t sample_rate, std::uint16_t channels, std::uint32_t frame_count, double frequency_hz) -> void { auto output = std::ofstream{path, std::ios::binary | std::ios::trunc}; BOOST_REQUIRE(output.good()); auto const bits_per_sample = std::uint16_t{16}; auto const bytes_per_sample = bits_per_sample / 8U; auto const block_align = static_cast(channels * bytes_per_sample); auto const byte_rate = sample_rate * block_align; auto const data_size = frame_count * block_align; write_bytes(output, "RIFF"); write_u32_le(output, 36U + data_size); write_bytes(output, "WAVE"); write_bytes(output, "fmt "); write_u32_le(output, 16U); write_u16_le(output, 1U); write_u16_le(output, channels); write_u32_le(output, sample_rate); write_u32_le(output, byte_rate); write_u16_le(output, block_align); write_u16_le(output, bits_per_sample); write_bytes(output, "data"); write_u32_le(output, data_size); for (auto frame = std::uint32_t{}; frame < frame_count; ++frame) { auto const t = static_cast(frame) / static_cast(sample_rate); auto const sample = static_cast(std::sin(2.0 * pi * frequency_hz * t) * 12000.0); for (auto channel = std::uint16_t{}; channel < channels; ++channel) { write_u16_le(output, static_cast(sample)); } } BOOST_REQUIRE(output.good()); } } BOOST_AUTO_TEST_CASE(sonus_validates_configs) { auto audio = sonus::audio_config{}; BOOST_TEST(static_cast(sonus::validate(audio))); audio.sample_rate = 0; BOOST_TEST(!sonus::validate(audio)); audio = {}; audio.channels = 0; BOOST_TEST(!sonus::validate(audio)); audio = {}; audio.master_volume = 1.5F; BOOST_TEST(!sonus::validate(audio)); audio = {}; audio.period_size_frames = 128; BOOST_TEST(static_cast(sonus::validate(audio))); audio.period_size_milliseconds = 10; BOOST_TEST(!sonus::validate(audio)); audio = {}; audio.period_size_milliseconds = 1001; BOOST_TEST(!sonus::validate(audio)); audio = {}; audio.period_count = 17; BOOST_TEST(!sonus::validate(audio)); audio = {}; audio.no_device = true; audio.playback_device_name = "default"; BOOST_TEST(!sonus::validate(audio)); audio = {}; audio.playback_device_name.assign("a\0b", 3); BOOST_TEST(!sonus::validate(audio)); auto metronome = sonus::metronome_config{}; BOOST_TEST(static_cast(sonus::validate(metronome))); metronome.bpm = 0; BOOST_TEST(!sonus::validate(metronome)); metronome = {}; metronome.beats_per_bar = 0; BOOST_TEST(!sonus::validate(metronome)); metronome = {}; metronome.volume = -0.1F; BOOST_TEST(!sonus::validate(metronome)); } BOOST_AUTO_TEST_CASE(sonus_no_device_engine_renders_silence_when_idle) { auto created = sonus::engine::create(no_device_config()); BOOST_REQUIRE(created); auto samples = std::vector(2048U * created->channels(), 1.0F); auto rendered = created->render(samples.data(), 2048U); BOOST_REQUIRE(rendered); BOOST_TEST(std::all_of(samples.begin(), samples.end(), [](auto sample) { return sample == 0.0F; })); } BOOST_AUTO_TEST_CASE(sonus_metronome_renders_accented_clicks) { auto config = no_device_config(); config.sample_rate = 48000; config.channels = 2; auto created = sonus::engine::create(config); BOOST_REQUIRE(created); auto metro = sonus::metronome_config{}; metro.bpm = 120; metro.beats_per_bar = 4; metro.volume = 0.70F; metro.click_duration = std::chrono::milliseconds{25}; BOOST_REQUIRE(created->start_metronome(metro)); auto samples = std::vector(config.sample_rate * config.channels); BOOST_REQUIRE(created->render(samples.data(), config.sample_rate)); auto downbeat_peak = peak(samples, 0, 2400, config.channels); auto beat_peak = peak(samples, config.sample_rate / 2, 2400, config.channels); auto between_peak = peak(samples, config.sample_rate / 4, 2400, config.channels); BOOST_TEST(downbeat_peak > 0.2F); BOOST_TEST(beat_peak > 0.1F); BOOST_TEST(downbeat_peak > beat_peak * 1.2F); BOOST_TEST(between_peak < 0.05F); } BOOST_AUTO_TEST_CASE(sonus_metronome_update_keeps_inactive_metronome_stopped) { auto created = sonus::engine::create(no_device_config()); BOOST_REQUIRE(created); auto metro = sonus::metronome_config{}; metro.bpm = 140; BOOST_REQUIRE(created->update_metronome(metro)); BOOST_TEST(!created->metronome_active()); BOOST_REQUIRE(created->start_metronome(metro)); BOOST_TEST(created->metronome_active()); metro.bpm = 180; BOOST_REQUIRE(created->update_metronome(metro)); BOOST_TEST(created->metronome_active()); } BOOST_AUTO_TEST_CASE(sonus_metronome_can_be_updated_concurrently) { auto created = sonus::engine::create(no_device_config()); BOOST_REQUIRE(created); auto worker = [&created](std::uint32_t offset) { for (auto index = std::uint32_t{}; index < 100U; ++index) { auto metro = sonus::metronome_config{}; metro.bpm = 80U + ((index + offset) % 80U); static_cast(created->start_metronome(metro)); if (index % 7U == 0U) { created->stop_metronome(); } } }; auto first = std::thread{worker, 0U}; auto second = std::thread{worker, 11U}; auto third = std::thread{worker, 23U}; first.join(); second.join(); third.join(); auto samples = std::vector(4096U * created->channels()); BOOST_TEST(static_cast(created->render(samples.data(), 4096U))); } BOOST_AUTO_TEST_CASE(sonus_file_playback_reports_missing_files) { auto created = sonus::engine::create(no_device_config()); BOOST_REQUIRE(created); auto missing = std::filesystem::path{"/tmp/sonus-missing-file.wav"}; auto played = created->play_file(missing); BOOST_TEST(!played); } BOOST_AUTO_TEST_CASE(sonus_file_playback_renders_audio) { auto config = no_device_config(); config.sample_rate = 48000; config.channels = 2; auto created = sonus::engine::create(config); BOOST_REQUIRE(created); auto path = test_file("sonus-tone-long.wav"); write_test_wav(path, config.sample_rate, 1, config.sample_rate, 440.0); auto played = created->play_file(path); BOOST_REQUIRE(played); BOOST_TEST(created->active_sound_count() == 1U); auto samples = std::vector(4096U * config.channels); BOOST_REQUIRE(created->render(samples.data(), 4096U)); BOOST_TEST(peak(samples, 0, 4096U, config.channels) > 0.05F); auto playing = created->is_playing(*played); BOOST_REQUIRE(playing); BOOST_TEST(*playing); } BOOST_AUTO_TEST_CASE(sonus_finished_file_playback_can_be_collected) { auto config = no_device_config(); config.sample_rate = 48000; config.channels = 2; auto created = sonus::engine::create(config); BOOST_REQUIRE(created); auto path = test_file("sonus-tone-short.wav"); write_test_wav(path, config.sample_rate, 1, 256U, 880.0); auto played = created->play_file(path); BOOST_REQUIRE(played); auto samples = std::vector(2048U * config.channels); BOOST_REQUIRE(created->render(samples.data(), 2048U)); auto playing = created->is_playing(*played); BOOST_REQUIRE(playing); BOOST_TEST(!*playing); BOOST_TEST(created->active_sound_count() == 0U); } BOOST_AUTO_TEST_CASE(sonus_repeated_short_sounds_are_retired) { auto config = no_device_config(); config.sample_rate = 48000; config.channels = 2; auto created = sonus::engine::create(config); BOOST_REQUIRE(created); auto path = test_file("sonus-tone-repeated.wav"); write_test_wav(path, config.sample_rate, 1, 64U, 660.0); auto samples = std::vector(512U * config.channels); auto collected = std::uint64_t{}; for (auto index = 0; index < 20; ++index) { auto played = created->play_file(path); BOOST_REQUIRE(played); BOOST_REQUIRE(created->render(samples.data(), 512U)); collected += created->collect_finished(); } BOOST_TEST(collected == 20U); BOOST_TEST(created->active_sound_count() == 0U); } BOOST_AUTO_TEST_CASE(sonus_no_device_render_serializes_with_control_paths) { auto config = no_device_config(); config.sample_rate = 48000; config.channels = 2; auto created = sonus::engine::create(config); BOOST_REQUIRE(created); auto path = test_file("sonus-tone-concurrent.wav"); write_test_wav(path, config.sample_rate, 1, 512U, 330.0); auto render_worker = [&created, channels = config.channels] { auto samples = std::vector(512U * channels); for (auto index = 0; index < 150; ++index) { BOOST_REQUIRE(created->render(samples.data(), 512U)); } }; auto control_worker = [&created, &path] { for (auto index = 0; index < 80; ++index) { auto metro = sonus::metronome_config{}; metro.bpm = 90U + static_cast(index % 80); if (index % 5 == 0) { created->stop_metronome(); } else if (created->metronome_active()) { static_cast(created->update_metronome(metro)); } else { static_cast(created->start_metronome(metro)); } auto played = created->play_file(path); if (played && index % 3 == 0) { static_cast(created->stop(*played)); } static_cast(created->collect_finished()); } }; auto renderer = std::thread{render_worker}; auto controller = std::thread{control_worker}; renderer.join(); controller.join(); created->stop_all(); created->stop_metronome(); BOOST_TEST(created->active_sound_count() == 0U); }