Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
94ba04f
test(audio): achieve 100% portaudio coverage and bypass gcc 13 dwarf …
MohitBareja16 May 25, 2026
e625ffb
build: add portaudio tests to CMake configuration
MohitBareja16 May 25, 2026
211978d
Merge branch 'main' into test-portaudio-coverage
MohitBareja16 May 25, 2026
0ec038f
test coverage for required files
MohitBareja16 May 25, 2026
f9a1b42
test coverage for required files
MohitBareja16 May 25, 2026
02ca35d
test coverage for required files
MohitBareja16 May 25, 2026
40d06ce
feat: implement comprehensive 2D audio graph undo/redo system
MohitBareja16 May 26, 2026
4675868
Merge branch 'main' into feature/graph-undo-redo
sudip-mondal-2002 May 26, 2026
217205e
Merge branch 'main' into feature/graph-undo-redo
MohitBareja16 May 26, 2026
4cf55e3
coderabbitai cases + test setup properly done
MohitBareja16 May 26, 2026
634725f
Merge branch 'main' into feature/graph-undo-redo
MohitBareja16 May 27, 2026
d0b5ff9
moved test code to test/fixtures/ and conflict issues resolved
MohitBareja16 May 27, 2026
8f9b835
Merge upstream/main into feature/graph-undo-redo and resolve UI refac…
MohitBareja16 May 28, 2026
36c2023
Merge upstream/main into feature/graph-undo-redo and resolve UI refac…
MohitBareja16 May 28, 2026
7322119
Merge upstream/main into feature/graph-undo-redo and resolve UI refac…
MohitBareja16 May 28, 2026
e3d89c0
Fix graph command edge cases, clean up singleton leaks, and achieve 1…
MohitBareja16 May 28, 2026
4513660
Merge branch 'main' into feature/graph-undo-redo
MohitBareja16 May 28, 2026
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 CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ else() # NOT EMSCRIPTEN, ANDROID, or IOS
tests/integration/test_clipboard_preset.cpp
tests/integration/test_audio_engine.cpp
tests/integration/test_audio_backend_portaudio.cpp
tests/integration/test_command_graph.cpp
tests/e2e/test_e2e_preset_workflow.cpp
tests/ui/test_gui_manager.cpp
tests/ui/test_pedal_board.cpp
Expand Down
12 changes: 6 additions & 6 deletions src/audio/backend/audio_backend_portaudio_devices.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ bool AudioEngine::set_input_device(int device_index) {
return false;
}

if (!devices_share_host_api(device_index, output_device_)) {
const PaDeviceInfo* out_info = Pa_GetDeviceInfo(output_device_);
const PaDeviceInfo* out_info = Pa_GetDeviceInfo(output_device_);
if (out_info && info->hostApi != out_info->hostApi) {
const PaHostApiInfo* in_api = Pa_GetHostApiInfo(info->hostApi);
const PaHostApiInfo* out_api = out_info ? Pa_GetHostApiInfo(out_info->hostApi) : nullptr;
const PaHostApiInfo* out_api = Pa_GetHostApiInfo(out_info->hostApi);
std::cerr << "[Amplitron] Warning: Input (" << (in_api ? in_api->name : "?")
<< ") and output (" << (out_api ? out_api->name : "?")
<< ") are on different host APIs. Stream may fail." << std::endl;
Expand Down Expand Up @@ -105,9 +105,9 @@ bool AudioEngine::set_output_device(int device_index) {
return false;
}

if (!devices_share_host_api(input_device_, device_index)) {
const PaDeviceInfo* in_info = Pa_GetDeviceInfo(input_device_);
const PaHostApiInfo* in_api = in_info ? Pa_GetHostApiInfo(in_info->hostApi) : nullptr;
const PaDeviceInfo* in_info = Pa_GetDeviceInfo(input_device_);
if (in_info && in_info->hostApi != info->hostApi) {
const PaHostApiInfo* in_api = Pa_GetHostApiInfo(in_info->hostApi);
const PaHostApiInfo* out_api = Pa_GetHostApiInfo(info->hostApi);
std::cerr << "[Amplitron] Warning: Input (" << (in_api ? in_api->name : "?")
<< ") and output (" << (out_api ? out_api->name : "?")
Expand Down
1 change: 0 additions & 1 deletion src/audio/backend/audio_backend_portaudio_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,4 @@ struct AudioBackendState {
PaStream* stream = nullptr;
};


} // namespace Amplitron
3 changes: 0 additions & 3 deletions src/audio/backend/audio_backend_portaudio_lifecycle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@

namespace Amplitron {




// PortAudio callback
int pa_audio_callback(const void* input, void* output,
unsigned long frame_count,
Expand Down
24 changes: 24 additions & 0 deletions src/audio/engine/audio_graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,28 @@ const DSPNode *AudioGraph::find_node(int node_id) const {
return nullptr;
}

void AudioGraph::restore_node(const DSPNode& node) {
nodes_.push_back(node);
if (node.id >= next_id_) next_id_ = node.id + 1;
for (int pin : node.input_pin_ids) {
if (pin >= next_id_) next_id_ = pin + 1;
}
for (int pin : node.output_pin_ids) {
if (pin >= next_id_) next_id_ = pin + 1;
}
rebuild_topology();
}

void AudioGraph::restore_link(const GraphLink& link) {
int prev_next_id = next_id_;
links_.push_back(link);
if (link.id >= next_id_) next_id_ = link.id + 1;
if (!rebuild_topology()) {
links_.pop_back();
next_id_ = prev_next_id;
rebuild_topology();
}
}
Comment thread
MohitBareja16 marked this conversation as resolved.


} // namespace Amplitron
4 changes: 4 additions & 0 deletions src/audio/engine/audio_graph.h
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ class AudioGraph {
// Topological Order & Loop Validation Core
bool rebuild_topology();

// For Undo/Redo System (forces cache reload)
void restore_node(const DSPNode& node);
void restore_link(const GraphLink& link);

// Accessors
const std::vector<int> &get_sorted_nodes() const { return sorted_node_ids_; }
const std::vector<DSPNode> &get_nodes() const { return nodes_; }
Expand Down
180 changes: 180 additions & 0 deletions src/gui/command_graph.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
#pragma once

#include "gui/command_base.h"
#include "audio/audio_engine.h"
#include "audio/audio_graph.h"
#include "gui/gui_graph_state.h"

namespace Amplitron {

using NodeId = int;
using EffectType = NodeRoutingType;

struct AddGraphNodeCommand : public Command {
AudioEngine& engine_;
NodeId node_id = -1; // Assigned on first execute
std::string name;
EffectType type;
std::shared_ptr<Effect> pedal;
ImVec2 position;
DSPNode cached_node; // To remember exactly what was added for redo

AddGraphNodeCommand(AudioEngine& engine, const std::string& name, EffectType type, std::shared_ptr<Effect> pedal, ImVec2 pos)
: engine_(engine), name(name), type(type), pedal(pedal), position(pos) {}

bool execute() override {
if (node_id == -1) {
node_id = engine_.graph().add_node(name, type, pedal);
auto* added_node = engine_.graph().find_node(node_id);
if (added_node) cached_node = *added_node;
} else {
// Re-adding the previously deleted/undone node
engine_.graph().restore_node(cached_node);
}
// Only write a fixed position when one was explicitly requested;
// if position is (0,0) the auto-placement logic in render_signal_chain
// will assign the correct cascading position on the next frame.
if (position.x != 0.0f || position.y != 0.0f) {
GuiGraphState::get_instance().node_positions[node_id] = { position, false, ImVec2(0, 0) };
}
engine_.commit_graph_changes();
return true;
}

void undo() override {
engine_.graph().remove_node(node_id);
GuiGraphState::get_instance().node_positions.erase(node_id);
engine_.commit_graph_changes();
}

const char* description() const override { return "Add Node"; }
};

struct RemoveGraphNodeCommand : public Command {
AudioEngine& engine_;
NodeId node_id;
EffectType type;
ImVec2 position;
std::vector<GraphLink> severed_links; // cache for undo
DSPNode cached_node; // full node data for exact restoration

RemoveGraphNodeCommand(AudioEngine& engine, NodeId id, EffectType t, ImVec2 pos)
: engine_(engine), node_id(id), type(t), position(pos) {}

bool execute() override {
auto* node_to_remove = engine_.graph().find_node(node_id);
if (node_to_remove) {
cached_node = *node_to_remove;
}

// Cache severed links before removal
severed_links.clear();
for (const auto& link : engine_.graph().get_links()) {
if (std::find(cached_node.input_pin_ids.begin(), cached_node.input_pin_ids.end(), link.dest_pin_id) != cached_node.input_pin_ids.end() ||
std::find(cached_node.output_pin_ids.begin(), cached_node.output_pin_ids.end(), link.source_pin_id) != cached_node.output_pin_ids.end()) {
severed_links.push_back(link);
}
}

engine_.graph().remove_node(node_id);
GuiGraphState::get_instance().node_positions.erase(node_id);
engine_.commit_graph_changes();
return true;
}

void undo() override {
engine_.graph().restore_node(cached_node);
GuiGraphState::get_instance().node_positions[node_id] = { position, false, ImVec2(0, 0) };

for (const auto& link : severed_links) {
engine_.graph().restore_link(link);
}
engine_.commit_graph_changes();
}

const char* description() const override { return "Remove Node"; }
};

struct AddGraphLinkCommand : public Command {
AudioEngine& engine_;
GraphLink link;
bool was_successful = false;

AddGraphLinkCommand(AudioEngine& engine, int src_pin, int dst_pin)
: engine_(engine) {
link.source_pin_id = src_pin;
link.dest_pin_id = dst_pin;
link.id = -1; // Unknown until execute
}

bool execute() override {
if (link.id == -1) {
link.id = engine_.graph().add_link(link.source_pin_id, link.dest_pin_id);
was_successful = (link.id != -1);
} else if (was_successful) {
engine_.graph().restore_link(link);
}
if (was_successful) {
engine_.commit_graph_changes();
}
return was_successful;
}

void undo() override {
if (was_successful) {
engine_.graph().remove_link(link.id);
engine_.commit_graph_changes();
}
}

const char* description() const override { return "Add Link"; }
};

struct RemoveGraphLinkCommand : public Command {
AudioEngine& engine_;
GraphLink link;

RemoveGraphLinkCommand(AudioEngine& engine, const GraphLink& l)
: engine_(engine), link(l) {}

bool execute() override {
engine_.graph().remove_link(link.id);
engine_.commit_graph_changes();
return true;
}

void undo() override {
engine_.graph().restore_link(link);
engine_.commit_graph_changes();
}

const char* description() const override { return "Remove Link"; }
};

struct MoveGraphNodeCommand : public Command {
NodeId node_id;
ImVec2 old_pos;
ImVec2 new_pos;

MoveGraphNodeCommand(NodeId id, ImVec2 old_pos, ImVec2 new_pos)
: node_id(id), old_pos(old_pos), new_pos(new_pos) {}

bool execute() override {
auto& positions = GuiGraphState::get_instance().node_positions;
if (positions.count(node_id)) {
positions[node_id].position = new_pos;
}
return true;
}

void undo() override {
auto& positions = GuiGraphState::get_instance().node_positions;
if (positions.count(node_id)) {
positions[node_id].position = old_pos;
}
}

const char* description() const override { return "Move Node"; }
};

} // namespace Amplitron
1 change: 1 addition & 0 deletions src/gui/commands/command.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@
#include "gui/commands/command_preset.h"
#include "gui/commands/command_clear.h"
#include "gui/commands/command_reset.h"
#include "gui/commands/command_graph.h"
4 changes: 2 additions & 2 deletions src/gui/commands/command_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ class Command {
public:
virtual ~Command() = default;

/** @brief Apply this command's action. */
virtual void execute() = 0;
/** @brief Apply this command's action. Returns true if a mutation occurred. */
virtual bool execute() { return true; }

/** @brief Reverse this command's action. */
virtual void undo() = 0;
Expand Down
8 changes: 6 additions & 2 deletions src/gui/commands/command_chain.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include "gui/commands/command_base.h"
#include "audio/engine/audio_engine.h"
#include "audio/effects/effect.h"
#include <cstring>
#include <algorithm>

Expand All @@ -22,7 +24,7 @@ class AddEffectCommand : public Command {
: engine_(engine), effect_(std::move(effect)) {}

/** @brief Append the effect to the engine's chain (before the amp if present). */
void execute() override {
bool execute() override {
int amp_idx = -1;
auto& fx = engine_.effects();
for (int i = 0; i < static_cast<int>(fx.size()); ++i) {
Expand All @@ -36,6 +38,7 @@ class AddEffectCommand : public Command {
} else {
engine_.add_effect(effect_);
}
return true;
}

/** @brief Remove the previously added effect from the chain. */
Expand Down Expand Up @@ -82,8 +85,9 @@ class RemoveEffectCommand : public Command {
}

/** @brief Remove the effect at the stored index. */
void execute() override {
bool execute() override {
engine_.remove_effect(index_);
return true;
}

/** @brief Re-insert the captured effect at its original chain position. */
Expand Down
5 changes: 4 additions & 1 deletion src/gui/commands/command_clear.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#pragma once

#include "gui/commands/command_base.h"
#include "audio/engine/audio_engine.h"
#include "audio/effects/effect.h"
#include <vector>

namespace Amplitron {
Expand All @@ -19,10 +21,11 @@ class ClearAllCommand : public Command {
}
}

void execute() override {
bool execute() override {
while (!engine_.effects().empty()) {
engine_.remove_effect(static_cast<int>(engine_.effects().size()) - 1);
}
return true;
}

void undo() override {
Expand Down
Loading
Loading