Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
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)
16 changes: 16 additions & 0 deletions examples/audio/CMakeLists.txt
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 ()
7 changes: 7 additions & 0 deletions examples/audio/synced/CMakeLists.txt
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}")
25 changes: 25 additions & 0 deletions examples/audio/synced/README.md
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.
Binary file added examples/audio/synced/assets/samples/0/0.ogg
Binary file not shown.
Binary file added examples/audio/synced/assets/samples/0/1.ogg
Binary file not shown.
Binary file added examples/audio/synced/assets/samples/0/2.ogg
Binary file not shown.
208 changes: 208 additions & 0 deletions examples/audio/synced/src/main.cpp
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);
}
}
Comment on lines +53 to +64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The prevFrame variable 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, prevFrame should be re-initialized inside the loop for each channel.

        float accumulator = 0.f;
        for (int channel = 0; channel < channelCount; ++channel) {
            float prevFrame = std::numeric_limits<float>::infinity();
            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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The volume property is initialized to 255, but the underlying type aui::audio::VolumeLevel supports a range up to 256. Using 255 prevents the use of the full volume range, especially if the maximum is intended. Consider initializing to 256 to be consistent with the type definition and allow for maximum volume. This is also related to the slider logic where 255.f is used for scaling.

        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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The volume slider logic uses 255.f for scaling. This is inconsistent with the aui::audio::VolumeLevel type which has a range of [0, 256]. Using 255.f prevents the slider from reaching the maximum possible volume. To ensure the full volume range is accessible, please use 256.f for scaling.

                              .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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The switch statement on column is implicitly exhaustive because ranges::view::iota(0, 2) only produces 0 and 1. However, the return nullptr; after the switch makes the control flow less clear. For better robustness and readability, consider adding a default case to the switch to handle unexpected values and make the exhaustiveness explicit.

                      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;
}

Loading