-
Notifications
You must be signed in to change notification settings - Fork 41
added synced audio example #691
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| add_subdirectory(app) | ||
| add_subdirectory(audio) | ||
| add_subdirectory(ui) | ||
| add_subdirectory(7guis) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| if (AUI_PLATFORM_WIN) | ||
| if ($ENV{CI}) | ||
| # on windows on github ci there's old msvc compiler which literally crashes (C1001 Internal compiler error) | ||
| # these examples are used in code snippets, so i don't want to screw them up just because shitty microsoft | ||
| # compiler can't accept perfectly valid c++ code. | ||
| return() | ||
| endif() | ||
| endif() | ||
|
|
||
| file(GLOB items "*") | ||
| foreach (item ${items}) | ||
| if (NOT EXISTS "${item}/CMakeLists.txt") | ||
| continue() | ||
| endif() | ||
| add_subdirectory(${item}) | ||
| endforeach () |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| cmake_minimum_required(VERSION 3.10) | ||
|
|
||
| get_filename_component(_t ${CMAKE_CURRENT_SOURCE_DIR} NAME) | ||
|
|
||
| aui_executable("aui.example.audio.${_t}") | ||
| aui_link("aui.example.audio.${_t}" PRIVATE aui::core aui::views aui::audio) | ||
| aui_compile_assets("aui.example.audio.${_t}") |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Audio synced playback | ||
|
|
||
| <!-- aui:example audio --> | ||
| Demonstrates synchronized playback of multiple audio tracks with per-track volume and a simple visual gain meter. | ||
|
|
||
| - Choose a sample set with radio buttons; each set contains a base layer and additional layers. | ||
| - Control playback with Play/Pause and toggle Loop to automatically restart when tracks finish. | ||
| - Each track row shows: | ||
| - A small color bar reflecting recent amplitude changes (greener = higher activity). | ||
| - A volume slider (0–100%) applied in real time. | ||
|
|
||
| When any track finishes, the example resets all tracks to keep them aligned. If Loop is enabled, playback restarts | ||
| immediately. | ||
|
|
||
| ## UI | ||
|
|
||
| - Playback controls: Play, Pause, Loop. | ||
| - Sample selector: radio group to switch between predefined layered samples. | ||
| - Tracks panel: rows for each layer with activity indicator and volume slider. | ||
|
|
||
| ## Concepts | ||
|
|
||
| - Synchronized start/stop across multiple audio players. | ||
| - Reactive state for play/pause, loop, selection, and per-track volume. | ||
| - Lightweight visual gain analysis for a live activity indicator. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| /* | ||
| * AUI Framework - Declarative UI toolkit for modern C++20 | ||
| * Copyright (C) 2020-2025 Alex2772 and Contributors | ||
| * | ||
| * SPDX-License-Identifier: MPL-2.0 | ||
| * | ||
| * This Source Code Form is subject to the terms of the Mozilla Public | ||
| * License, v. 2.0. If a copy of the MPL was not distributed with this | ||
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. | ||
| */ | ||
|
|
||
| #include "AUI/Audio/IAudioPlayer.h" | ||
| #include "AUI/Platform/AMessageBox.h" | ||
| #include "AUI/View/AButton.h" | ||
| #include "AUI/View/ACheckBox.h" | ||
| #include "AUI/View/AGroupBox.h" | ||
|
|
||
| #include <AUI/Platform/Entry.h> | ||
| #include <AUI/Platform/AWindow.h> | ||
| #include <AUI/Util/UIBuildingHelpers.h> | ||
| #include <AUI/View/AForEachUI.h> | ||
| #include "AUI/View/ARadioButton.h" | ||
| #include "AUI/View/ASlider.h" | ||
|
|
||
| #include <range/v3/view/cartesian_product.hpp> | ||
| #include <range/v3/view/enumerate.hpp> | ||
| #include <range/v3/view/iota.hpp> | ||
|
|
||
| using namespace ass; | ||
| using namespace declarative; | ||
|
|
||
| static constexpr auto SAMPLES = std::array { | ||
| "Dire Shred Cover Pack (credit: Youssry Askar)", | ||
| }; | ||
|
|
||
| class GainAnalysis: public AObject, public ISoundInputStream { | ||
| public: | ||
| explicit GainAnalysis(_<ISoundInputStream> inner) : mInner(std::move(inner)) {} | ||
|
|
||
| size_t read(char* dst, size_t size) override { | ||
| auto r = mInner->read(dst, size); | ||
| if (r == 0) { | ||
| return r; | ||
| } | ||
|
|
||
| const auto inputFormat = info(); | ||
| size_t sampleCount = size / aui::audio::bytesPerSample(inputFormat.sampleFormat); | ||
| mSamplesBuffer.resize(sampleCount); | ||
| aui::audio::convertSampleFormat(inputFormat.sampleFormat, ASampleFormat::F32, | ||
| dst, reinterpret_cast<char*>(mSamplesBuffer.data()), sampleCount); | ||
|
|
||
| int channelCount = int(inputFormat.channelCount); | ||
| float prevFrame = std::numeric_limits<float>::infinity(); | ||
| float accumulator = 0.f; | ||
| for (int channel = 0; channel < channelCount; ++channel) { | ||
| for (size_t i = 0; i < mSamplesBuffer.size(); i += channelCount) { | ||
| float currentFrame = mSamplesBuffer[i + channel]; | ||
| AUI_DEFER { prevFrame = currentFrame; }; | ||
| if (prevFrame == std::numeric_limits<float>::infinity()) { | ||
| continue; | ||
| } | ||
| accumulator += glm::distance(currentFrame, prevFrame); | ||
| } | ||
| } | ||
| accumulator = glm::sqrt(glm::clamp(accumulator / mSamplesBuffer.size() * 100.f)); | ||
| lastFrame = accumulator; | ||
| return r; | ||
| } | ||
|
|
||
| AAudioFormat info() override { | ||
| return mInner->info(); | ||
| } | ||
|
|
||
| ~GainAnalysis() override = default; | ||
|
|
||
| AProperty<float> lastFrame; | ||
|
|
||
| private: | ||
| _<ISoundInputStream> mInner; | ||
| std::vector<float> mSamplesBuffer; | ||
| }; | ||
|
|
||
| struct State { | ||
| AProperty<int> sample = 0; | ||
|
|
||
| struct Track { | ||
| _<IAudioPlayer> player; | ||
| _<GainAnalysis> gainAnalysis; | ||
|
|
||
| AProperty<aui::audio::VolumeLevel> volume = 255; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The AProperty<aui::audio::VolumeLevel> volume = 256; |
||
| }; | ||
|
|
||
| AProperty<AVector<_<Track>>> tracks; | ||
| AProperty<bool> isPlaying = false; | ||
| AProperty<bool> isLoop = true; | ||
|
|
||
| State() { | ||
| AObject::connect(sample, AObject::GENERIC_OBSERVER, [this] { | ||
| setup(); | ||
| }); | ||
| AObject::connect(isPlaying, AObject::GENERIC_OBSERVER, [this](bool isPlaying) { | ||
| for (const auto& track : *tracks) { | ||
| isPlaying ? track->player->play() : track->player->pause(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| ~State() { | ||
| stopAllPlayers(); | ||
| } | ||
|
|
||
| void stopAllPlayers() { | ||
| isPlaying = false; | ||
| for (const auto& track : *tracks) { | ||
| track->player->stop(); | ||
| } | ||
| tracks.writeScope()->clear(); | ||
| } | ||
|
|
||
| void setup() { | ||
| stopAllPlayers(); | ||
| for (const auto& i : ranges::view::iota(0, 3)) { | ||
| auto stream = _new<GainAnalysis>(ISoundInputStream::fromUrl(AUrl(":samples/{}/{}.ogg"_format(*sample, i)))); | ||
| auto track = aui::ptr::manage_shared(new Track { | ||
| .player = IAudioPlayer::fromStream(stream), | ||
| .gainAnalysis = std::move(stream), | ||
| }); | ||
| AObject::connect(track->player->finished, AObject::GENERIC_OBSERVER, [this] { | ||
| setup(); | ||
| if (isLoop) { | ||
| isPlaying = true; | ||
| } | ||
| }); | ||
| AObject::connect(track->volume, track->player, [&player = *track->player](aui::audio::VolumeLevel volume) { | ||
| player.setVolume(volume); | ||
| }); | ||
| tracks << std::move(track); | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| _<AView> radioButtons(_<State> state) { | ||
| return AUI_DECLARATIVE_FOR(i, SAMPLES | ranges::view::enumerate, AVerticalLayout) { | ||
| const auto& [index, text] = i; | ||
| return RadioButton { | ||
| .checked = AUI_REACT(state->sample == index), | ||
| .onClick = [state, index] { state->sample = index; }, | ||
| .content = Label { text }, | ||
| }; | ||
| }; | ||
| } | ||
|
|
||
| AUI_ENTRY { | ||
| auto window = _new<AWindow>("Synced audio playback", 600_dp, 300_dp); | ||
| auto state = _new<State>(); | ||
| window->setContents( | ||
| Vertical { | ||
| Horizontal { | ||
| Button { Label { "Play" }, [state] { state->isPlaying = true; } } AUI_LET { | ||
| AObject::connect(AUI_REACT(!state->isPlaying), AUI_SLOT(it)::setEnabled); | ||
| }, | ||
| Button { Label { "Pause" }, [state] { state->isPlaying = false; } } AUI_LET { | ||
| AObject::connect(AUI_REACT(state->isPlaying), AUI_SLOT(it)::setEnabled); | ||
| }, | ||
| CheckBox { | ||
| .checked = AUI_REACT(state->isLoop), | ||
| .onCheckedChange = [=](bool v) { state->isLoop = v; }, | ||
| .content = Label { "Loop" }, | ||
| }, | ||
| } AUI_OVERRIDE_STYLE { LayoutSpacing { 4_dp } }, | ||
| GroupBox { | ||
| Label { "Sample" }, | ||
| radioButtons(state), | ||
| }, | ||
| GroupBox { | ||
| Label { "Tracks" }, | ||
| Vertical { | ||
| aui::detail::makeForEach([=]() -> decltype(auto) { return ranges::view::cartesian_product(*state->tracks | ranges::view::enumerate, ranges::view::iota(0, 2)); }, std::make_unique<AAdvancedGridLayout>(2, 1)) - [=](const auto& value) -> _<AView> { | ||
| const auto&[layerAndPlayer, column] = value; | ||
| const auto&[layer, track] = layerAndPlayer; | ||
| switch (column) { | ||
| case 0: return Horizontal { | ||
| _new<AView>() AUI_LET { | ||
| it->setFixedSize({10_dp, 0}); | ||
| AObject::connect(track->gainAnalysis->lastFrame, it, [&it = *it](float frame) { | ||
| it AUI_OVERRIDE_STYLE { | ||
| BackgroundSolid { glm::mix(glm::vec4(AColor::BLACK), glm::vec4(AColor::GREEN), frame) }, | ||
| }; | ||
| }); | ||
| }, | ||
|
|
||
| Centered { Label { layer == 0 ? "Base" : "Layer {}"_format(layer) } }, | ||
| } AUI_OVERRIDE_STYLE { LayoutSpacing { 4_dp } }; | ||
| case 1: return Slider { | ||
| .value = AUI_REACT(aui::float_within_0_1(float(*track->volume) / 255.f)), | ||
| .onValueChanged = [=](float v) { track->volume = v * 255.f; }, | ||
|
Comment on lines
+195
to
+196
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The volume slider logic uses .value = AUI_REACT(aui::float_within_0_1(float(*track->volume) / 256.f)),
.onValueChanged = [=](float v) { track->volume = v * 256.f; }, |
||
| }; | ||
| } | ||
| return nullptr; | ||
|
Comment on lines
+181
to
+199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The switch (column) {
case 0: return Horizontal {
_new<AView>() AUI_LET {
it->setFixedSize({10_dp, 0});
AObject::connect(track->gainAnalysis->lastFrame, it, [&it = *it](float frame) {
it AUI_OVERRIDE_STYLE {
BackgroundSolid { glm::mix(glm::vec4(AColor::BLACK), glm::vec4(AColor::GREEN), frame) },
};
});
},
Centered { Label { layer == 0 ? "Base" : "Layer {}"_format(layer) } },
} AUI_OVERRIDE_STYLE { LayoutSpacing { 4_dp } };
case 1: return Slider {
.value = AUI_REACT(aui::float_within_0_1(float(*track->volume) / 255.f)),
.onValueChanged = [=](float v) { track->volume = v * 255.f; },
};
default:
return nullptr;
} |
||
| } AUI_OVERRIDE_STYLE { LayoutSpacing { 4_dp } }, | ||
| }, | ||
| }, | ||
| } AUI_OVERRIDE_STYLE { LayoutSpacing { 4_dp } } | ||
| ); | ||
| window->show(); | ||
| return 0; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
prevFramevariable is initialized outside the channel loop. This means that for multi-channel audio, the analysis for the second channel (and subsequent channels) will incorrectly use the last sample from the previous channel for its initial calculation. This can lead to inaccurate gain analysis. To fix this,prevFrameshould be re-initialized inside the loop for each channel.