From 1d8f869b5bcd1fd07c667422139c275b0c3396da Mon Sep 17 00:00:00 2001 From: hanez Date: Mon, 26 Oct 2026 22:51:25 +0100 Subject: [PATCH] Initial commit. --- .gitignore | 16 + AGENT.md | 35 + LICENSE | 17 + Makefile | 60 ++ README.md | 60 ++ docs/ARCHITECTURE.md | 38 + docs/AUDIO_ENGINE.md | 27 + docs/GUI.md | 18 + docs/IDEA.md | 723 +++++++++++++++++++ docs/PROJECT_FORMAT.md | 18 + docs/ROADMAP.md | 42 ++ docs/decisions/0001-modular-engine-first.md | 23 + docs/decisions/0002-c-core-fltk-gui.md | 20 + docs/decisions/0003-realtime-thread-rules.md | 19 + examples/README.md | 4 + include/xtlite/xtlite.h | 26 + include/xtlite/xtlite_engine.h | 44 ++ include/xtlite/xtlite_project.h | 22 + include/xtlite/xtlite_types.h | 29 + include/xtlite/xtlite_version.h | 7 + src/backend/README.md | 8 + src/core/README.md | 1 + src/core/xtlite.c | 46 ++ src/dsp/README.md | 1 + src/dsp/gain.c | 15 + src/engine/README.md | 1 + src/engine/offline_engine.c | 13 + src/graph/README.md | 1 + src/graph/graph.c | 189 +++++ src/gui/main.cpp | 10 + src/gui/main_window.cpp | 52 ++ src/gui/main_window.hpp | 12 + src/midi/README.md | 5 + src/persistence/README.md | 5 + src/project/README.md | 1 + src/util/README.md | 5 + tests/fixtures/README.md | 4 + tests/integration/README.md | 11 + tests/unit/test_core.c | 43 ++ 39 files changed, 1671 insertions(+) create mode 100644 .gitignore create mode 100644 AGENT.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/AUDIO_ENGINE.md create mode 100644 docs/GUI.md create mode 100644 docs/IDEA.md create mode 100644 docs/PROJECT_FORMAT.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/decisions/0001-modular-engine-first.md create mode 100644 docs/decisions/0002-c-core-fltk-gui.md create mode 100644 docs/decisions/0003-realtime-thread-rules.md create mode 100644 examples/README.md create mode 100644 include/xtlite/xtlite.h create mode 100644 include/xtlite/xtlite_engine.h create mode 100644 include/xtlite/xtlite_project.h create mode 100644 include/xtlite/xtlite_types.h create mode 100644 include/xtlite/xtlite_version.h create mode 100644 src/backend/README.md create mode 100644 src/core/README.md create mode 100644 src/core/xtlite.c create mode 100644 src/dsp/README.md create mode 100644 src/dsp/gain.c create mode 100644 src/engine/README.md create mode 100644 src/engine/offline_engine.c create mode 100644 src/graph/README.md create mode 100644 src/graph/graph.c create mode 100644 src/gui/main.cpp create mode 100644 src/gui/main_window.cpp create mode 100644 src/gui/main_window.hpp create mode 100644 src/midi/README.md create mode 100644 src/persistence/README.md create mode 100644 src/project/README.md create mode 100644 src/util/README.md create mode 100644 tests/fixtures/README.md create mode 100644 tests/integration/README.md create mode 100644 tests/unit/test_core.c diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..009e903 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Build products +/build/ +/*.o +*.a +*.so +*.so.* + +# IDE and editor files +.idea/ +.vscode/ +*.swp +*~ + +# Local test outputs +*.wav +*.mid diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..fe79ce5 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,35 @@ +# Instructions for the AI coding agent + +This file is the working contract for AI-assisted development in this repository. Read it and inspect the repository before editing. + +## Rules + +1. Make small, buildable changes and run the relevant tests after each change. +2. Do not replace or remove existing behavior without explaining why. +3. Keep audio engine, DSP, project model, graph, persistence, and public core APIs in C (C99 baseline). +4. Keep FLTK and GUI code in C++ only. The GUI must call the core through C headers. +5. Use C `snake_case`; prefer explicit ownership and small functions. +6. Validate pointers, sizes, IDs, enum values, and allocation results. +7. Never allocate/free memory, perform file I/O, log, block on a mutex, or call GUI APIs from an audio callback. +8. Do not mutate a graph while the audio thread is traversing it. Prepare and publish a safe immutable execution plan. +9. Do not add dependencies without explaining the need, license, maintenance status, and build impact. +10. Do not claim incomplete features are implemented. Distinguish prototype, stub, and finished behavior. +11. Do not copy energyXT code, artwork, or proprietary resources. +12. Before a task, briefly state the plan and affected files. After it, report files changed, commands run, test results, and limitations. + +## Definition of done + +- Builds with warnings enabled. +- Relevant tests pass. +- Error paths and cleanup are handled. +- Documentation is updated when APIs or behavior change. +- No unrelated functionality is removed. +- Unverified behavior is reported honestly. + +## First task + +Run `make clean all test`. Inspect compiler and `pkg-config` availability. Do not install packages or alter system configuration without permission. Report current state and propose the next smallest step. + +## Roadmap + +Follow `docs/ROADMAP.md` in order. The initial implementation should improve the graph and offline engine before introducing hardware audio I/O, recording, MIDI, or external plugin hosting. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d14b88a --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +MIT License + +Copyright (c) 2026 Johannes Findeisen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..984d2cb --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +CC ?= cc +CXX ?= c++ +PKG_CONFIG ?= pkg-config +BUILD_DIR ?= build +PREFIX ?= /usr/local + +CFLAGS ?= -O2 -g +CFLAGS += -std=c99 -Wall -Wextra -Wpedantic -Wconversion -Wshadow -Iinclude +CXXFLAGS ?= -O2 -g +CXXFLAGS += -std=c++17 -Wall -Wextra -Wpedantic -Iinclude +LDFLAGS ?= + +CORE_SOURCES := src/core/xtlite.c src/graph/graph.c src/dsp/gain.c src/engine/offline_engine.c +CORE_OBJECTS := $(CORE_SOURCES:%.c=$(BUILD_DIR)/%.o) +TEST_OBJECT := $(BUILD_DIR)/tests/unit/test_core.o +GUI_AVAILABLE := $(shell $(PKG_CONFIG) --exists fltk 2>/dev/null && echo yes) + +.PHONY: all clean test gui install +all: $(BUILD_DIR)/libxtlite.a + +$(BUILD_DIR)/libxtlite.a: $(CORE_OBJECTS) + @mkdir -p $(@D) + $(AR) rcs $@ $^ + +$(BUILD_DIR)/%.o: %.c + @mkdir -p $(@D) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/%.o: %.cpp + @mkdir -p $(@D) + $(CXX) $(CXXFLAGS) $(shell $(PKG_CONFIG) --cflags fltk 2>/dev/null) -c $< -o $@ + +$(TEST_OBJECT): tests/unit/test_core.c + @mkdir -p $(@D) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD_DIR)/test_core: $(TEST_OBJECT) $(BUILD_DIR)/libxtlite.a + $(CC) $(LDFLAGS) -o $@ $(TEST_OBJECT) $(BUILD_DIR)/libxtlite.a -lm + +test: $(BUILD_DIR)/test_core + ./$(BUILD_DIR)/test_core + +ifeq ($(GUI_AVAILABLE),yes) +gui: $(BUILD_DIR)/xtlite + +$(BUILD_DIR)/xtlite: $(BUILD_DIR)/src/gui/main.o $(BUILD_DIR)/src/gui/main_window.o $(BUILD_DIR)/libxtlite.a + $(CXX) $(LDFLAGS) -o $@ $^ $(shell $(PKG_CONFIG) --libs fltk) -lm +else +gui: + @echo "FLTK was not found by pkg-config. Install FLTK development files to build the GUI." + @exit 1 +endif + +install: all + install -d "$(DESTDIR)$(PREFIX)/include/xtlite" "$(DESTDIR)$(PREFIX)/lib" + install -m 0644 include/xtlite/*.h "$(DESTDIR)$(PREFIX)/include/xtlite/" + install -m 0644 "$(BUILD_DIR)/libxtlite.a" "$(DESTDIR)$(PREFIX)/lib/" + +clean: + rm -rf "$(BUILD_DIR)" diff --git a/README.md b/README.md new file mode 100644 index 0000000..5d5d57b --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# xtlite — a lightweight, modular DAW + +`xtlite` is a new, energyXT-inspired digital audio workstation project. It is not a clone and contains no energyXT source code or assets. + +## Design decisions + +- **Modular engine first:** sequencer, instruments, effects, mixers and I/O are nodes in one processing graph. +- **C core:** project model, graph, DSP and audio engine are implemented in C. +- **FLTK/C++ GUI:** C++ is confined to the graphical interface and calls the core through a C API. +- **Linux first:** keep backend-specific details behind an interface. +- **Small increments:** build and test each milestone before adding features. + +## Current status + +This repository is a starter scaffold, not a production DAW. It currently provides: +- a small C library/API; +- a minimal graph model with validation and topological ordering; +- a gain processor and offline audio-block processing; +- a C unit test; +- an optional FLTK GUI shell; +- architecture, format, GUI, roadmap, and decision documents. + +Real audio-device integration, recording, MIDI, plugin hosting, project persistence, and the actual sequencer UI are not implemented yet. + +## Build + +Requirements: a C compiler, a C++ compiler, GNU Make, and `pkg-config`. FLTK is optional for the core and tests. + +```sh +make +make test +``` + +Build the optional FLTK GUI if FLTK is installed: + +```sh +make gui +``` + +Run the GUI: + +```sh +./build/xtlite +``` + +The current GUI is only a shell and does not yet produce sound. The core's offline graph test is the first executable audio-processing milestone. + +Clean generated files: + +```sh +make clean +``` + +## Suggested first agent task + +Read `AGENT.md` and `docs/ARCHITECTURE.md`. Inspect the existing implementation, run `make test`, and report gaps before adding the next feature. Do not jump directly to plugin hosting or a full sequencer. + +## License + +The project is scaffolded under the MIT License; see `LICENSE`. Review the licenses of any future dependencies before distribution. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..b75c9ec --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,38 @@ +# Architecture + +## Decision: modular engine first + +The processing graph is the common model for audio sources, instruments, +effects, mixers, and outputs. The sequencer produces time-based content and +events consumed by graph nodes. The default workspace remains sequencer-first; +the graph editor is an advanced view of the same project. + +## Layers + +1. **FLTK/C++ GUI:** input, drawing, windows, view state. +2. **C core:** project model, commands, parameters, persistence, validation. +3. **C engine:** prepared processing graph, DSP, transport timing, event delivery. +4. **Backend:** hardware and system audio/MIDI integration. + +The GUI must not directly modify engine internals. It calls a stable C API. + +## Graph execution + +The first implementation should reject cycles. Compile a graph off the audio +thread: validate, topologically sort, determine buffer requirements, allocate +resources, and publish a prepared immutable execution plan. The audio thread +must not traverse a graph being edited. + +The current scaffold is deliberately incomplete: `xt_graph_process_offline()` +validates the graph and applies gain nodes in insertion order. It is a test +placeholder, not the final graph execution algorithm. + +## Ownership and threads + +- GUI thread owns FLTK widgets and view state. +- Project/control thread owns mutable project data and graph compilation. +- Audio thread owns the active processing plan and DSP state. +- Backend callbacks follow the backend's documented threading contract. + +Do not free an active plan until the audio thread has released it. Pick and +document a bounded handoff/reclamation strategy before real-time graph swaps. diff --git a/docs/AUDIO_ENGINE.md b/docs/AUDIO_ENGINE.md new file mode 100644 index 0000000..7222c43 --- /dev/null +++ b/docs/AUDIO_ENGINE.md @@ -0,0 +1,27 @@ +# Audio engine requirements + +## Real-time callback rules + +The callback must not allocate/free memory, lock blocking mutexes, perform +filesystem/network/console I/O, log, load plugins or samples, call GUI APIs, +or wait for another thread. + +Use preallocated buffers and bounded work. Publish parameter and graph changes +through a documented real-time-safe mechanism. Verify atomics are lock-free +where required on the target platform. + +## Processing model + +Process a bounded number of frames per block. Do not assume a fixed block size +unless the backend guarantees it. Define maximum frame and event counts and +safe behavior when those limits are reached. + +The initial milestone is offline processing, then a test oscillator -> gain -> +meter/output graph, then a hardware backend. + +## Clock and transport + +Audio sample position is the authoritative playback clock. Use integer or +rational timeline representations to avoid cumulative floating-point drift. +Define tempo-map conversion and behavior at tempo changes before implementing +tempo automation. diff --git a/docs/GUI.md b/docs/GUI.md new file mode 100644 index 0000000..8ba1350 --- /dev/null +++ b/docs/GUI.md @@ -0,0 +1,18 @@ +# GUI guidelines + +Use FLTK/C++ for windows, menus, dialogs, and the event loop. Keep all audio +engine and project-domain logic in C. + +Main views: +- Sequencer: tracks, clips, ruler, playhead, selection, zoom and scrolling. +- Mixer: channels, faders, pan, mute/solo and meters. +- Modular graph: nodes, ports, connections, validation feedback. +- Transport: play/stop/record, tempo, meter, loop and position. + +Use custom-drawn widgets where ordinary controls are not a good fit: timeline, +waveforms, piano roll, mixer strips and graph canvas. Keep drawing separate +from project mutation. Never call FLTK from the audio thread. + +Visual direction: compact, high-contrast, restrained dark interface. Avoid +excessive animation and modal configuration. Recreate broad usability ideas, +not energyXT's exact artwork or proprietary resources. diff --git a/docs/IDEA.md b/docs/IDEA.md new file mode 100644 index 0000000..e414b09 --- /dev/null +++ b/docs/IDEA.md @@ -0,0 +1,723 @@ +# Project Brief: A Modern, Lightweight, energyXT-Inspired DAW + +**Document purpose:** This file is the authoritative project brief and implementation guide for an AI coding agent working inside an IDE. + +**Working project name:** `xtc` (temporary; keep the project name configurable and do not hard-code it into public APIs). + +**Primary goal:** Build a modern, reliable, lightweight digital audio workstation inspired by the workflow and modular philosophy of energyXT. This is a new implementation, not a clone of energyXT's proprietary code or assets. + +**Architecture decision:** **B — Modular engine first.** The sequencer, instruments, effects, mixers, audio inputs, and outputs are components in one unified processing graph. The default user experience should still be a simple sequencer-first workspace; users can open the modular view when they need to inspect or edit signal flow. + +**Language decision:** +- **100% C for the audio engine and core domain logic.** +- **FLTK with C++ for the graphical user interface only.** +- The GUI talks to the core through a small, documented C API. +- Do not introduce C++ into the audio engine or core library. +- Keep dependencies small, prefer open-source components, and avoid unnecessary frameworks. + +--- + +## 1. Instructions to the AI coding agent + +Treat this document as the project contract. Follow it in every implementation task. + +1. Read this file and the existing project before making changes. +2. Inspect the current build system, source tree, and tests. Do not assume the repository is empty. +3. Implement work in small, buildable increments. Each increment must compile and have relevant tests before proceeding. +4. Prefer complete, coherent file updates over partial snippets. Do not silently remove existing functionality. +5. Before editing, state briefly what you intend to change and which files are affected. Then make the changes. +6. Do not invent APIs, FLTK features, audio backend capabilities, or dependency behavior. Check installed headers and official documentation when uncertain. +7. Keep C source compatible with **C99** initially. Use C11 features only when explicitly justified and supported by the selected compiler/toolchain. +8. Use C snake_case identifiers. Use clear names; avoid cryptic one-letter names except conventional loop counters. +9. Validate external input, lengths, indexes, counts, enum values, and allocation sizes. Handle errors explicitly. +10. Do not hide failures, silently clamp invalid data, or continue with partially initialized objects. +11. Avoid unnecessary dependencies, generated-code systems, global mutable state, and elaborate abstractions. +12. Never perform audio-thread-unsafe operations from the real-time callback. See the real-time rules below. +13. Keep the project usable on Linux first. Do not make Linux-specific details leak into the public core API. +14. Add or update tests and documentation with each feature. +15. At the end of a task, report: files changed, behavior added, build/test commands run, results, known limitations, and the next sensible step. +16. If a requirement is ambiguous, choose the smallest safe implementation consistent with this document and explain the assumption. Ask a question only if the ambiguity blocks safe progress. +17. Do not claim that a feature works unless it has been implemented and tested. Clearly distinguish a prototype, stub, and finished feature. +18. Never copy energyXT source code, proprietary resources, UI artwork, or undocumented internal implementation. Use energyXT only as inspiration for high-level workflow ideas. + +### Definition of done for every task + +A task is complete only when: +- the code builds with warnings enabled; +- relevant tests pass; +- error paths are considered; +- resource ownership and cleanup are clear; +- documentation is updated where behavior or APIs changed; +- no unrelated functionality was removed; +- the agent reports any untested behavior honestly. + +--- + +## 2. Product principles + +The application should feel like a small, responsive music instrument rather than a general-purpose content-creation suite. + +### Core principles + +- **Simple by default, powerful when opened up.** +- A single unified component graph underlies the application. +- The sequencer, mixer, instruments, effects, audio I/O, and utility processors use a common node model. +- Common operations should require few clicks and avoid modal dialogs when practical. +- The modular graph should expose real signal flow, not be a decorative diagram. +- The project must remain understandable to a human developer. +- Audio reliability is more important than visual effects or feature count. +- Favor predictable behavior over clever implicit behavior. +- Use non-destructive editing where practical. +- Avoid feature bloat. Every feature must justify its complexity. +- Save files should be versioned and recoverable. +- The application should remain useful at every development milestone. + +### Explicit non-goals for the first release + +Do not initially implement: +- video, notation, collaboration, cloud accounts, or online services; +- an integrated marketplace; +- scripting as a requirement for basic operation; +- dozens of bundled instruments or effects; +- advanced surround/spatial audio; +- distributed processing; +- a complicated theme system; +- an elaborate plugin sandbox; +- automatic online update services; +- a proprietary project format that cannot be inspected or migrated. + +These may be considered later, but they must not delay the first reliable recording and playback workflow. + +--- + +## 3. High-level architecture + +Use distinct layers with narrow interfaces. + +```text ++------------------------------------------------------------+ +| FLTK / C++ GUI | +| Main window, transport, sequencer, mixer, modular editor, | +| dialogs, custom drawing, keyboard and mouse interaction | ++----------------------------+-------------------------------+ + | + documented C API + | ++----------------------------v-------------------------------+ +| C application core | +| Project model, commands/undo, transport state, persistence, | +| parameter model, asset references, validation | ++----------------------------+-------------------------------+ + | + real-time-safe bridge + | ++----------------------------v-------------------------------+ +| C audio engine | +| Graph compilation, node processing, scheduling, buffers, | +| transport sample position, MIDI/event delivery, metering | ++----------------------------+-------------------------------+ + | + backend abstraction + | ++----------------------------v-------------------------------+ +| Audio/MIDI backend(s): start with JACK-compatible audio | +| and ALSA MIDI on Linux; add backends only when justified | ++------------------------------------------------------------+ +``` + +The C core and audio engine may be separate libraries or clearly separated modules within one C library. Keep their interfaces explicit even if they initially compile into one library. + +### Suggested repository layout + +Adapt this to an existing repository instead of replacing it blindly. + +```text +xtlite/ +├── README.md +├── LICENSE +├── Makefile +├── docs/ +│ ├── ARCHITECTURE.md +│ ├── AUDIO_ENGINE.md +│ ├── PROJECT_FORMAT.md +│ ├── GUI.md +│ ├── ROADMAP.md +│ └── decisions/ +│ ├── 0001-modular-engine-first.md +│ ├── 0002-c-core-fltk-gui.md +│ └── 0003-realtime-thread-rules.md +├── include/ +│ └── xtlite/ +│ ├── xtlite.h +│ ├── xtlite_types.h +│ ├── xtlite_project.h +│ ├── xtlite_engine.h +│ └── xtlite_version.h +├── src/ +│ ├── core/ +│ ├── project/ +│ ├── graph/ +│ ├── engine/ +│ ├── backend/ +│ ├── midi/ +│ ├── dsp/ +│ ├── persistence/ +│ ├── util/ +│ └── gui/ +│ ├── main.cpp +│ ├── main_window.cpp +│ ├── main_window.hpp +│ ├── transport_widget.cpp +│ ├── sequencer_widget.cpp +│ ├── mixer_widget.cpp +│ └── graph_widget.cpp +├── tests/ +│ ├── unit/ +│ ├── integration/ +│ └── fixtures/ +├── examples/ +└── build/ # generated; do not commit +``` + +Keep C and C++ files clearly separated. C headers exposed to C++ must use `extern "C"` guards. + +--- + +## 4. Language and API boundary + +### C code owns + +- project and track data; +- graph nodes, ports, and connections; +- audio processing and DSP; +- transport and sample-position calculations; +- MIDI event representation and scheduling; +- parameter values and automation evaluation; +- project serialization and validation; +- undoable core commands where they affect domain state; +- backend-independent engine state; +- error codes and diagnostic messages. + +### C++/FLTK code owns + +- windows and widgets; +- layout and drawing; +- mouse and keyboard interaction; +- selection and view state; +- GUI-only drag previews; +- user-facing dialogs and menus; +- conversion between user gestures and C API commands. + +### Boundary rules + +- Public C headers must compile from both C and C++. +- Do not expose C++ classes, STL containers, exceptions, or FLTK types through the C API. +- Prefer opaque handles for objects whose internal layout must remain private. +- Use explicit integer error codes and documented output parameters. +- Define ownership for every pointer: borrowed, caller-owned, or transferred. +- Do not return pointers into mutable engine memory unless their lifetime is guaranteed. +- Do not let GUI widgets mutate internal engine structures directly. +- Avoid a public API that mirrors every internal struct field. Provide stable operations instead. + +Example API shape (illustrative only; refine after reviewing the actual codebase): + +```c +#ifndef XTLITE_H +#define XTLITE_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct xt_app xt_app; + +typedef enum { + XT_OK = 0, + XT_ERR_INVALID_ARGUMENT, + XT_ERR_OUT_OF_MEMORY, + XT_ERR_NOT_FOUND, + XT_ERR_INVALID_STATE, + XT_ERR_IO, + XT_ERR_UNSUPPORTED +} xt_result; + +xt_result xt_app_create(xt_app **out_app); +void xt_app_destroy(xt_app *app); + +xt_result xt_transport_play(xt_app *app); +xt_result xt_transport_stop(xt_app *app); + +#ifdef __cplusplus +} +#endif + +#endif +``` + +Do not implement this exact API blindly if the existing design calls for a better interface. Keep it small and consistent. + +--- + +## 5. Modular processing graph — primary design decision + +**Decision B: Modular engine first.** The graph is the common representation of signal processing and routing. The sequencer is a source of time-based events and clips, not a separate audio system disconnected from the graph. + +### Node categories + +Start with a small set: +- audio input; +- audio output; +- audio clip player; +- MIDI input; +- MIDI clip/event player; +- instrument; +- audio effect; +- gain/pan; +- mixer; +- metering/analyzer; +- utility nodes such as channel mapping. + +Do not build every node at once. First prove the graph with a test tone, gain node, and output node. + +### Ports and connections + +Each node has a stable identifier, a node type, and declared ports. A port has: +- a unique identifier within its node; +- a direction (input or output); +- a signal/data type (audio, MIDI/event, or control); +- a channel layout or event format where applicable; +- a documented sample/block processing contract. + +Validate connections before accepting them: +- source and destination exist; +- source port is an output and destination is an input; +- signal types are compatible; +- channel layouts can be connected or explicitly converted; +- the connection does not violate graph rules; +- identifiers and connection counts are valid. + +### Cycles and feedback + +For the first working engine, reject cycles in the audio-processing graph. A directed acyclic graph is easier to schedule safely and predictably. Report a useful error to the user. Do not silently break a cycle. + +Feedback support can be considered later using explicit delay/feedback nodes and a defined block-boundary model. Never introduce arbitrary graph cycles without a correct scheduling design. + +### Graph compilation + +Compile the editable graph into an execution plan outside the real-time thread: +1. validate nodes, ports, and edges; +2. identify source nodes and reachable output nodes; +3. detect cycles; +4. topologically sort processing nodes; +5. determine channel layouts and required buffers; +6. allocate buffers and prepare node state; +7. publish an immutable, ready-to-run plan. + +The audio callback processes the prepared plan. It must not perform graph discovery, sorting, dynamic allocation, file loading, or plugin creation. + +### Graph replacement during playback + +The editor must not modify the graph currently being traversed by the audio thread. Prepare a new plan off-thread and publish it through a bounded real-time-safe mechanism. Define how the old plan is retired only after the audio thread can no longer access it. Do not use a naive pointer swap followed by immediate free. + +Possible approaches include a carefully designed single-producer/single-consumer handoff, epochs, or another proven reclamation scheme. Choose one, document it, and test it under stress. Keep the initial design as simple as safety allows. + +### Sequencer integration + +The sequencer stores timeline content and emits events to the graph at the correct sample positions. A clip player or instrument node consumes those events. The GUI timeline is a view/editor for project data; it is not the audio scheduler. + +--- + +## 6. Real-time audio requirements + +These rules are mandatory. + +### Audio callback must not + +- allocate or free memory; +- acquire blocking mutexes or wait on condition variables; +- perform file, network, or console I/O; +- log using potentially blocking facilities; +- create or destroy GUI objects; +- load plugins, sample files, or project files; +- resize containers or buffers; +- perform unbounded work based on arbitrary user input; +- wait for the GUI or another thread. + +### Audio callback should + +- process a bounded number of frames and events; +- use preallocated buffers; +- use predictable algorithms; +- avoid unnecessary copying; +- detect invalid runtime states and recover safely; +- update meters through a bounded, non-blocking communication path; +- process silence safely when a source is unavailable; +- handle backend buffer sizes and channel counts within documented limits. + +Do not assume all backends use a fixed block size unless the backend contract guarantees it. Establish maximum supported frame/event counts and define what happens when limits are exceeded. + +### Thread ownership + +Document which thread owns each mutable resource: +- GUI thread: widgets and GUI state; +- control/project thread: editable project state, file operations, graph compilation; +- audio thread: active processing plan and real-time DSP state; +- backend callbacks: follow the backend's documented threading model. + +Avoid sharing mutable structs across threads without an explicit synchronization strategy. Atomics are not automatically safe for every use: verify that the selected atomic operations are lock-free where required and use correct memory ordering. + +### Timing + +Use audio sample position as the authoritative playback clock. Represent timeline positions with an integer or rational scheme that avoids accumulated floating-point drift. Convert musical time (bars/beats/ticks) to sample positions using tempo and meter maps. Define behavior at tempo changes before implementing advanced tempo automation. + +--- + +## 7. Audio backend strategy + +Start on Linux and keep the backend replaceable. + +1. Define a small backend interface in C. +2. Implement one backend first, selected after checking the target system and installed development packages. JACK-compatible audio is a reasonable first target on Linux. +3. Test device enumeration, start/stop, sample rate, buffer size, channel count, disconnects, and shutdown. +4. Keep backend-specific headers out of the public API. +5. Add another backend only after the engine works and tests pass. + +Do not assume PipeWire-native, JACK compatibility, ALSA PCM, and desktop audio routing are interchangeable. Document the chosen backend's setup and limitations. + +The engine should also have an offline/test mode that processes buffers without physical audio hardware. This makes unit and integration testing repeatable. + +--- + +## 8. MIDI, parameters, automation, and DSP + +### MIDI/events + +- Use a compact, well-defined event representation. +- Store event time relative to a project timeline and convert it to sample offsets for a processing block. +- Validate event sizes and event counts. +- Handle note-on, note-off, velocity, channel, controller, pitch bend, and tempo/transport events as needed. +- Ensure all held notes can be released on stop, panic, seek, device loss, or project teardown. +- Avoid unbounded event queues. + +### Parameters + +Every automatable control should have: +- a stable parameter ID; +- a type and valid range; +- a default value; +- a documented unit and mapping; +- a safe update mechanism; +- optional automation and MIDI mapping. + +Do not silently clamp invalid serialized values. Validate project data and return an error or apply a clearly documented migration rule. UI controls may constrain user input, but core validation must remain authoritative. + +### Automation + +Implement automation only after transport, graph processing, and basic parameter updates work. Define interpolation types explicitly (initially step and linear are enough). Automation evaluation must be deterministic and bounded. + +### DSP + +Start with a few well-tested processors: +- gain; +- pan; +- simple meter/peak detector; +- test oscillator; +- basic delay only after buffer/lifetime handling is sound. + +Keep DSP algorithms in C and independently testable. Use appropriate numerical safeguards, including finite-value checks where needed. Avoid premature SIMD optimization until profiling shows a real bottleneck. + +--- + +## 9. FLTK/C++ GUI guidelines + +The GUI is a thin client of the C API, not the owner of the engine. + +### Main views + +1. **Sequencer:** tracks, audio/MIDI clips, timeline ruler, playhead, selection, zoom and horizontal scrolling. +2. **Mixer:** channels, faders, pan, mute/solo, inserts/sends when supported, and meters. +3. **Modular graph:** nodes, ports, connections, zoom/pan, selection, and connection validation feedback. +4. **Transport:** play, stop, record, tempo, meter, loop range, and current position. +5. **Project/file dialogs:** create, open, save, import audio, and manage missing assets. + +The default workspace should emphasize the sequencer and transport. The graph view is an advanced but first-class view of the same underlying project. + +### GUI implementation + +- Use FLTK for conventional widgets and application windows. +- Use custom-drawn widgets for the timeline, piano roll, mixer strips, waveform display, and graph canvas where needed. +- Keep drawing code separate from project mutation. +- Use GUI timers or the toolkit's appropriate event mechanism for refreshing meters; never poll from the audio callback or block the GUI waiting for audio. +- Avoid redrawing the entire application when only a small region changes. +- Support keyboard shortcuts and predictable focus behavior. +- Make selection, dragging, snapping, zooming, and scrolling consistent across editors. +- Never call FLTK APIs from the audio thread. +- Do not hold GUI locks while calling potentially blocking engine operations. +- Keep C++ simple: use RAII for GUI resources, clear ownership, and small classes. Do not force the C core into C++ abstractions. + +### Visual direction + +Aim for a compact, dark, high-contrast interface with restrained decoration. Prioritize legible waveforms, clip boundaries, transport state, and meter levels. Avoid excessive gradients, animations, nested panels, and modal configuration windows. + +Do not copy energyXT's artwork, exact visual assets, or proprietary UI resources. Recreate the general principles of compactness, direct manipulation, and modular routing in an original design. + +--- + +## 10. Project format and persistence + +The project format must be versioned from its first release. + +### Requirements + +- Include a format version and application version. +- Store stable IDs for tracks, nodes, ports where necessary, clips, parameters, and connections. +- Save relative asset paths when possible. +- Define behavior for missing audio files. +- Validate every field before allocating large amounts of memory. +- Handle truncated, malformed, unsupported, or corrupted files gracefully. +- Never execute code while loading a project. +- Save atomically: write a temporary file, flush as appropriate, and rename it into place when safe. +- Preserve unknown optional fields when practical, or explicitly document when they are discarded. +- Include migration functions when the format changes. +- Keep project serialization outside the real-time thread. + +Choose a simple, well-supported format after considering the dependency and robustness trade-offs. A small, documented text format can help inspect projects, but large binary audio data should generally live in separate assets rather than be embedded in a text document. Do not add a serialization dependency without a clear reason. + +--- + +## 11. Undo/redo and editing commands + +Use explicit commands for user-visible edits, such as: +- create/delete/move/resize clip; +- create/delete/rename track; +- change parameter; +- add/remove node; +- connect/disconnect ports; +- change tempo or meter. + +A command should validate its inputs and have a well-defined inverse or stored previous state. Avoid designing a generic event-sourcing framework before it is needed. + +The initial implementation may begin without a complete undo stack, but design edit operations so undo/redo can be added without rewriting all editors. Never make an edit partially visible if it fails validation. + +--- + +## 12. Build, coding standards, and dependencies + +### Build system + +Use a straightforward Makefile unless the existing repository has a sound reason to use another build system. + +The build must: +- compile C with the selected C standard and warnings enabled; +- compile C++ GUI sources with the standard supported by the chosen FLTK version; +- link the GUI with FLTK and the C core; +- link the required audio backend; +- provide `all`, `clean`, `test`, and a debug build target; +- allow compiler and installation-prefix overrides; +- avoid embedding machine-specific paths; +- separate generated objects and binaries into a build directory. + +Use `pkg-config` where supported by the installed dependencies. Check actual package names on the target distribution rather than hard-coding unverified names. + +### C style + +- Use C99 initially. +- Use `snake_case`. +- Prefer explicit structs and functions over clever macros. +- Keep functions focused. +- Validate pointers and sizes at API boundaries. +- Check allocation and I/O results. +- Avoid variable-length arrays for untrusted sizes. +- Make ownership and cleanup obvious. +- Use `const` where appropriate. +- Do not suppress warnings merely to get a clean build. + +### C++ GUI style + +- Keep C++ confined to the GUI and any unavoidable C++-only third-party wrapper. +- Prefer small classes with explicit ownership. +- Use RAII for GUI-side resources. +- Avoid exceptions crossing the C API boundary. +- Catch exceptions at the GUI's top-level boundary if any dependency can throw. +- Never pass STL types across the C API. + +### Dependencies + +Before adding a dependency: +1. identify the exact feature it provides; +2. check license and maintenance status; +3. determine whether the existing code can provide the functionality simply and safely; +4. document the dependency and its version constraints; +5. keep it optional if the feature is optional. + +Prefer open-source dependencies with clear licensing. Do not claim the whole application is license-compatible until all direct and bundled dependencies have been reviewed. + +--- + +## 13. Testing and diagnostics + +### Unit tests + +Test at least: +- graph validation and topological sorting; +- invalid ports and incompatible connections; +- cycle detection; +- parameter validation; +- sample-position and tempo conversion; +- project serialization/deserialization; +- malformed project files and missing assets; +- node lifecycle and buffer bounds; +- DSP output for known inputs; +- command execution and undo/redo when implemented. + +### Integration tests + +Test: +- test oscillator → gain → output processing; +- offline rendering with deterministic output; +- graph replacement during active processing; +- start/stop and repeated initialization/cleanup; +- project save/load round trips; +- missing audio assets; +- MIDI note start/stop and panic; +- backend disconnect/reconnect where supported. + +### Tools + +Where available, use: +- compiler warnings; +- AddressSanitizer and UndefinedBehaviorSanitizer in non-real-time test builds; +- static analysis; +- a leak checker; +- stress tests for graph edits and shutdown; +- audio loopback or offline render comparisons. + +Sanitizers and diagnostic logging should not be enabled in production real-time callbacks unless their real-time implications have been specifically evaluated. + +--- + +## 14. Development roadmap and acceptance criteria + +Implement phases in order. Do not jump ahead merely because a later feature is exciting. + +### Phase 0 — Repository and design baseline + +- Inspect the repository and available toolchain. +- Create or update architecture documentation and the Makefile. +- Verify C compiler, C++ compiler, FLTK, audio development headers, and test tools. +- Record dependency and licensing decisions. +- Add a minimal application that starts and exits cleanly. + +**Acceptance:** clean build on the target Linux environment; no audio engine assumptions hidden in GUI code. + +### Phase 1 — C core and offline processing + +- Create the C library/API. +- Define node, port, connection, and graph data structures. +- Implement graph validation and topological sorting. +- Implement preallocated audio buffers. +- Add oscillator, gain, meter, and output test nodes. +- Implement deterministic offline block processing. + +**Acceptance:** automated test proves oscillator → gain → meter/output produces expected samples without hardware. + +### Phase 2 — Real-time backend + +- Add the first Linux audio backend. +- Implement start/stop, device configuration, and error handling. +- Ensure callback rules are followed. +- Add safe control-to-audio communication. +- Stress-test shutdown and graph replacement. + +**Acceptance:** stable playback for a sustained test; no known callback allocations or blocking calls; failures are reported clearly. + +### Phase 3 — FLTK shell + +- Create main window, menus, transport, status area, and view switching. +- Call the C API through the public header. +- Add an audio engine status indicator. +- Implement clean startup and shutdown. + +**Acceptance:** GUI can start/stop the engine, report backend errors, and close without leaks or hangs. + +### Phase 4 — Modular graph editor + +- Draw nodes and ports. +- Add/select/move nodes. +- Create/delete connections. +- Display invalid connection feedback. +- Submit graph edits through validated C API commands. + +**Acceptance:** graph shown by the GUI matches the graph used by the engine; invalid connections are rejected without corrupting active playback. + +### Phase 5 — Project and sequencer foundation + +- Add project persistence and stable IDs. +- Add tracks and clip objects. +- Implement timeline rendering, selection, moving, trimming, zoom, and scrolling. +- Add transport/sample-position synchronization. + +**Acceptance:** save/load a project with clips and reproduce the same arrangement. + +### Phase 6 — Audio clips and recording + +- Add audio file import and clip playback. +- Add recording through a safe producer/consumer buffer path. +- Keep file I/O outside the audio callback. +- Add missing-asset reporting. + +**Acceptance:** record, save, reopen, and play a basic audio project without callback file I/O. + +### Phase 7 — MIDI and a simple instrument + +- Add MIDI input and clip events. +- Add a basic internal synthesizer or test instrument. +- Handle note-off, panic, seeking, and stop correctly. +- Add a minimal piano-roll editor only after event scheduling works. + +**Acceptance:** record or draw MIDI notes, play them in time, save the project, and reopen it. + +### Phase 8 — Mixer, automation, and editing polish + +- Add mixer channels, gain/pan, meters, mute/solo. +- Add automation lanes and interpolation. +- Add a bounded undo/redo system for common edits. +- Improve keyboard interaction and workflow. + +**Acceptance:** basic composition and mixing can be completed without editing project files manually. + +### Phase 9 — Extensibility and release preparation + +- Define and document the native node/instrument/effect API. +- Evaluate external plugin hosting only after core lifecycle handling is robust. +- Review project format migration, crash behavior, performance, and licensing. +- Package the application and document Linux setup. + +**Acceptance:** reproducible release build, tested install/uninstall, clear limitations, and complete user/developer documentation. + +--- + +## 15. Initial implementation task for the agent + +Start with **Phase 0 only**. + +1. Inspect the current repository and report its state. +2. Identify the available C/C++ compilers, FLTK version, audio backend development packages, `pkg-config`, and test tools. +3. Do not install packages or modify system configuration without permission. +4. Propose the smallest initial source tree compatible with this brief. +5. Add or update the Makefile and documentation only as needed. +6. Create a minimal program that builds and exits cleanly; do not yet implement a full audio engine or GUI. +7. Build it and run its tests. +8. Report exact commands, results, assumptions, and any missing dependencies. + +After Phase 0 is reviewed, continue with Phase 1. Keep each phase independently buildable and testable. + +--- + +## 16. Long-term quality bar + +The project succeeds if a musician can open it quickly, connect components without fighting the interface, record and arrange audio/MIDI, and make music without needing to understand every detail of the engine. + +The implementation succeeds if a developer can read the C code, understand ownership and thread boundaries, test components independently, and add functionality without turning the application into an unmaintainable framework. + +**Guiding rule:** build a small, coherent system whose components compose well. Do not build a large DAW and then attempt to simplify it afterward. diff --git a/docs/PROJECT_FORMAT.md b/docs/PROJECT_FORMAT.md new file mode 100644 index 0000000..893cb16 --- /dev/null +++ b/docs/PROJECT_FORMAT.md @@ -0,0 +1,18 @@ +# Project format + +Project persistence is not implemented yet. Before implementation, define a +versioned format with stable IDs for tracks, nodes, clips, connections, and +parameters. + +Requirements: +- reject malformed, truncated, unsupported, or unreasonable input safely; +- validate sizes before allocation; +- store external audio assets separately from project metadata; +- define missing-asset behavior; +- never execute code during loading; +- perform file I/O only off the audio thread; +- save atomically using a temporary file and a safe rename; +- add explicit migration code when the format changes. + +Choose a simple format after evaluating dependency, robustness, and human +inspectability trade-offs. Document the schema before treating it as stable. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..e58f05d --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,42 @@ +# Roadmap + +## Phase 0 — Baseline +Inspect repository/toolchain, verify Makefile and minimal executable, document +dependencies. No system changes without permission. + +## Phase 1 — C core and offline processing +Graph data structures, validation, cycle detection, topological ordering, +preallocated buffers, test oscillator, gain and offline render tests. + +## Phase 2 — Real-time backend +Choose and implement a Linux backend. Document callback semantics, start/stop, +device errors, shutdown, and safe graph publication. + +## Phase 3 — FLTK shell +Main window, transport controls, view switching, backend status, clean startup +and shutdown through the C API. + +## Phase 4 — Modular editor +Draw nodes/ports/edges; create, move and connect nodes; reject invalid edges; +safely submit edits to the C core. + +## Phase 5 — Project and sequencer +Versioned persistence, stable IDs, tracks, clips, timeline, selection, zoom, +scrolling, and transport synchronization. + +## Phase 6 — Audio clips and recording +Audio import/playback, recording through bounded buffers, missing-asset +handling, no file I/O in callbacks. + +## Phase 7 — MIDI and instrument +MIDI input/events/clips, simple internal instrument, note-off/panic handling, +then a basic piano roll. + +## Phase 8 — Mixer and editing +Gain/pan, meters, mute/solo, automation, undo/redo, keyboard workflow. + +## Phase 9 — Extensibility and release +Native node API, evaluate plugin hosting, licensing review, packaging, +installation tests, performance and recovery tests. + +Do not skip phases without documenting why and preserving acceptance criteria. diff --git a/docs/decisions/0001-modular-engine-first.md b/docs/decisions/0001-modular-engine-first.md new file mode 100644 index 0000000..39bc339 --- /dev/null +++ b/docs/decisions/0001-modular-engine-first.md @@ -0,0 +1,23 @@ +# Decision 0001: Modular engine first + +**Status:** Accepted + +## Context + +The project aims to preserve energyXT's combination of a simple workflow and a +flexible modular routing model. + +## Decision + +Use one unified processing graph for sources, instruments, effects, mixers, +and outputs. The sequencer is a timeline/event source integrated with that +graph. The default GUI is simple and sequencer-oriented, while a graph editor +exposes routing when needed. + +## Consequences + +- Graph validation, scheduling, buffer allocation, and safe graph replacement + are foundational requirements. +- Initial audio graph rejects cycles. +- Graph execution plans are prepared off the audio thread. +- The UI must not create a separate routing model disconnected from the engine. diff --git a/docs/decisions/0002-c-core-fltk-gui.md b/docs/decisions/0002-c-core-fltk-gui.md new file mode 100644 index 0000000..834a788 --- /dev/null +++ b/docs/decisions/0002-c-core-fltk-gui.md @@ -0,0 +1,20 @@ +# Decision 0002: C core with FLTK/C++ GUI + +**Status:** Accepted + +## Context + +The project prioritizes a readable, small C implementation while using FLTK +for a lightweight desktop GUI. + +## Decision + +Keep the engine, DSP, project model, graph, and public core API in C (C99 +baseline). Use C++ only for FLTK windows and widgets. Communicate through +documented C headers with `extern "C"` guards. + +## Consequences + +- No C++ classes, STL containers, exceptions, or FLTK types cross the C API. +- GUI code cannot mutate engine internals directly. +- Keep C++ simple and limited to the presentation layer. diff --git a/docs/decisions/0003-realtime-thread-rules.md b/docs/decisions/0003-realtime-thread-rules.md new file mode 100644 index 0000000..72b35d2 --- /dev/null +++ b/docs/decisions/0003-realtime-thread-rules.md @@ -0,0 +1,19 @@ +# Decision 0003: Real-time thread rules + +**Status:** Accepted + +## Context + +Audio glitches and unsafe graph changes can undermine the whole application. + +## Decision + +The audio callback performs bounded processing using preallocated resources. +It performs no allocation/free, blocking locks, file I/O, logging, GUI calls, +plugin loading, or waits. Graphs are prepared outside the callback and active +graph resources are retired only after safe reclamation. + +## Consequences + +Thread ownership and publication mechanisms must be documented and tested. +Any exception to these rules requires explicit evidence and review. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..06d512d --- /dev/null +++ b/examples/README.md @@ -0,0 +1,4 @@ +# Examples + +Add small runnable examples here as the C API stabilizes. Keep examples focused +on one behavior and make sure they build through the normal Makefile. diff --git a/include/xtlite/xtlite.h b/include/xtlite/xtlite.h new file mode 100644 index 0000000..1c25f9a --- /dev/null +++ b/include/xtlite/xtlite.h @@ -0,0 +1,26 @@ +#ifndef XTLITE_H +#define XTLITE_H + +#include "xtlite_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define XTLITE_VERSION_MAJOR 0 +#define XTLITE_VERSION_MINOR 1 +#define XTLITE_VERSION_PATCH 0 + +typedef struct xt_app xt_app; + +/* Creates an application context. On success, *out_app is caller-owned. */ +xt_result xt_app_create(xt_app **out_app); + +/* Destroys an application context; NULL is accepted. */ +void xt_app_destroy(xt_app *app); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/xtlite/xtlite_engine.h b/include/xtlite/xtlite_engine.h new file mode 100644 index 0000000..4d0c91e --- /dev/null +++ b/include/xtlite/xtlite_engine.h @@ -0,0 +1,44 @@ +#ifndef XTLITE_ENGINE_H +#define XTLITE_ENGINE_H + +#include "xtlite_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct xt_graph xt_graph; + +typedef enum { + XT_SIGNAL_AUDIO = 1, + XT_SIGNAL_EVENT = 2, + XT_SIGNAL_CONTROL = 3 +} xt_signal_type; + +typedef enum { + XT_PORT_INPUT = 1, + XT_PORT_OUTPUT = 2 +} xt_port_direction; + +typedef struct { + float *samples; + size_t frame_count; + size_t channel_count; +} xt_audio_buffer; + +/* Graph API is intentionally small in this first scaffold. */ +xt_result xt_graph_create(size_t max_nodes, xt_graph **out_graph); +void xt_graph_destroy(xt_graph *graph); +xt_result xt_graph_add_gain(xt_graph *graph, float gain, size_t *out_node_id); +xt_result xt_graph_connect(xt_graph *graph, size_t source_node, + size_t destination_node); +xt_result xt_graph_validate(const xt_graph *graph); +xt_result xt_graph_process_offline(xt_graph *graph, const float *input, + float *output, size_t frame_count, + float input_gain); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/xtlite/xtlite_project.h b/include/xtlite/xtlite_project.h new file mode 100644 index 0000000..ac321d8 --- /dev/null +++ b/include/xtlite/xtlite_project.h @@ -0,0 +1,22 @@ +#ifndef XTLITE_PROJECT_H +#define XTLITE_PROJECT_H + +#include "xtlite_types.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct xt_project xt_project; + +/* Project persistence is not implemented in this scaffold yet. */ +xt_result xt_project_create(xt_project **out_project); +void xt_project_destroy(xt_project *project); +xt_result xt_project_save(const xt_project *project, const char *path); +xt_result xt_project_load(xt_project *project, const char *path); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/xtlite/xtlite_types.h b/include/xtlite/xtlite_types.h new file mode 100644 index 0000000..663636a --- /dev/null +++ b/include/xtlite/xtlite_types.h @@ -0,0 +1,29 @@ +#ifndef XTLITE_TYPES_H +#define XTLITE_TYPES_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + XT_OK = 0, + XT_ERR_INVALID_ARGUMENT, + XT_ERR_OUT_OF_MEMORY, + XT_ERR_NOT_FOUND, + XT_ERR_INVALID_STATE, + XT_ERR_IO, + XT_ERR_UNSUPPORTED, + XT_ERR_CAPACITY, + XT_ERR_GRAPH_CYCLE +} xt_result; + +const char *xt_result_string(xt_result result); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/xtlite/xtlite_version.h b/include/xtlite/xtlite_version.h new file mode 100644 index 0000000..3efac0e --- /dev/null +++ b/include/xtlite/xtlite_version.h @@ -0,0 +1,7 @@ +#ifndef XTLITE_VERSION_H +#define XTLITE_VERSION_H + +#define XTLITE_VERSION_STRING "0.1.0" +#define XTLITE_PROJECT_NAME "xtlite" + +#endif diff --git a/src/backend/README.md b/src/backend/README.md new file mode 100644 index 0000000..4a27622 --- /dev/null +++ b/src/backend/README.md @@ -0,0 +1,8 @@ +# Audio backend + +No hardware backend is implemented yet. Keep backend-specific headers out of +public headers. The first backend should be selected after checking the target +Linux environment and its development packages. + +A backend implementation must document callback threading, buffer sizes, +sample-rate negotiation, device loss, and shutdown semantics. diff --git a/src/core/README.md b/src/core/README.md new file mode 100644 index 0000000..0a34630 --- /dev/null +++ b/src/core/README.md @@ -0,0 +1 @@ +Core lifecycle and public C API implementation. diff --git a/src/core/xtlite.c b/src/core/xtlite.c new file mode 100644 index 0000000..9d1e0c6 --- /dev/null +++ b/src/core/xtlite.c @@ -0,0 +1,46 @@ +#include "xtlite/xtlite.h" + +#include + +struct xt_app { + unsigned int reserved; +}; + +const char *xt_result_string(xt_result result) +{ + switch (result) { + case XT_OK: return "success"; + case XT_ERR_INVALID_ARGUMENT: return "invalid argument"; + case XT_ERR_OUT_OF_MEMORY: return "out of memory"; + case XT_ERR_NOT_FOUND: return "not found"; + case XT_ERR_INVALID_STATE: return "invalid state"; + case XT_ERR_IO: return "I/O error"; + case XT_ERR_UNSUPPORTED: return "unsupported operation"; + case XT_ERR_CAPACITY: return "capacity exceeded"; + case XT_ERR_GRAPH_CYCLE: return "graph contains a cycle"; + default: return "unknown error"; + } +} + +xt_result xt_app_create(xt_app **out_app) +{ + xt_app *app; + + if (out_app == NULL) { + return XT_ERR_INVALID_ARGUMENT; + } + *out_app = NULL; + + app = (xt_app *)calloc(1U, sizeof(*app)); + if (app == NULL) { + return XT_ERR_OUT_OF_MEMORY; + } + + *out_app = app; + return XT_OK; +} + +void xt_app_destroy(xt_app *app) +{ + free(app); +} diff --git a/src/dsp/README.md b/src/dsp/README.md new file mode 100644 index 0000000..14bb317 --- /dev/null +++ b/src/dsp/README.md @@ -0,0 +1 @@ +Small, independently testable DSP primitives implemented in C. diff --git a/src/dsp/gain.c b/src/dsp/gain.c new file mode 100644 index 0000000..635b58e --- /dev/null +++ b/src/dsp/gain.c @@ -0,0 +1,15 @@ +#include + +/* Kept as a separately testable DSP primitive. */ +void xt_dsp_gain_process(const float *input, float *output, + size_t frame_count, float gain) +{ + size_t frame; + + if (input == NULL || output == NULL) { + return; + } + for (frame = 0U; frame < frame_count; frame++) { + output[frame] = input[frame] * gain; + } +} diff --git a/src/engine/README.md b/src/engine/README.md new file mode 100644 index 0000000..17f866e --- /dev/null +++ b/src/engine/README.md @@ -0,0 +1 @@ +Offline and real-time processing coordination. Respect callback safety rules. diff --git a/src/engine/offline_engine.c b/src/engine/offline_engine.c new file mode 100644 index 0000000..8197ca1 --- /dev/null +++ b/src/engine/offline_engine.c @@ -0,0 +1,13 @@ +/* + * Offline engine integration point. + * + * The first scaffold exposes xt_graph_process_offline() from graph.c. This + * file is reserved for the next milestone: a compiled immutable graph plan, + * preallocated node buffers, and block-based processing independent of audio + * hardware. Keep hardware callbacks and real-time constraints out of this + * initial offline implementation. + */ +int xt_offline_engine_translation_unit_anchor(void) +{ + return 0; +} diff --git a/src/graph/README.md b/src/graph/README.md new file mode 100644 index 0000000..67ebdf8 --- /dev/null +++ b/src/graph/README.md @@ -0,0 +1 @@ +Graph nodes, ports, connections, validation, and compiled execution plans. diff --git a/src/graph/graph.c b/src/graph/graph.c new file mode 100644 index 0000000..41e76f4 --- /dev/null +++ b/src/graph/graph.c @@ -0,0 +1,189 @@ +#include "xtlite/xtlite_engine.h" + +#include +#include + +typedef struct { + size_t source; + size_t destination; +} xt_edge; + +typedef struct { + float gain; +} xt_gain_node; + +struct xt_graph { + size_t max_nodes; + size_t node_count; + size_t edge_count; + xt_gain_node *nodes; + xt_edge *edges; +}; + +xt_result xt_graph_create(size_t max_nodes, xt_graph **out_graph) +{ + xt_graph *graph; + + if (out_graph == NULL || max_nodes == 0U) { + return XT_ERR_INVALID_ARGUMENT; + } + *out_graph = NULL; + + graph = (xt_graph *)calloc(1U, sizeof(*graph)); + if (graph == NULL) { + return XT_ERR_OUT_OF_MEMORY; + } + + graph->nodes = (xt_gain_node *)calloc(max_nodes, sizeof(*graph->nodes)); + graph->edges = (xt_edge *)calloc(max_nodes * max_nodes, sizeof(*graph->edges)); + if (graph->nodes == NULL || graph->edges == NULL) { + free(graph->nodes); + free(graph->edges); + free(graph); + return XT_ERR_OUT_OF_MEMORY; + } + + graph->max_nodes = max_nodes; + *out_graph = graph; + return XT_OK; +} + +void xt_graph_destroy(xt_graph *graph) +{ + if (graph != NULL) { + free(graph->nodes); + free(graph->edges); + free(graph); + } +} + +xt_result xt_graph_add_gain(xt_graph *graph, float gain, size_t *out_node_id) +{ + if (graph == NULL || out_node_id == NULL) { + return XT_ERR_INVALID_ARGUMENT; + } + if (graph->node_count >= graph->max_nodes) { + return XT_ERR_CAPACITY; + } + + graph->nodes[graph->node_count].gain = gain; + *out_node_id = graph->node_count; + graph->node_count++; + return XT_OK; +} + +xt_result xt_graph_connect(xt_graph *graph, size_t source_node, + size_t destination_node) +{ + size_t i; + size_t edge_capacity; + + if (graph == NULL) { + return XT_ERR_INVALID_ARGUMENT; + } + if (source_node >= graph->node_count || destination_node >= graph->node_count) { + return XT_ERR_NOT_FOUND; + } + + edge_capacity = graph->max_nodes * graph->max_nodes; + if (graph->edge_count >= edge_capacity) { + return XT_ERR_CAPACITY; + } + for (i = 0U; i < graph->edge_count; i++) { + if (graph->edges[i].source == source_node && + graph->edges[i].destination == destination_node) { + return XT_ERR_INVALID_STATE; + } + } + + graph->edges[graph->edge_count].source = source_node; + graph->edges[graph->edge_count].destination = destination_node; + graph->edge_count++; + return XT_OK; +} + +xt_result xt_graph_validate(const xt_graph *graph) +{ + size_t *indegree; + size_t *queue; + size_t head = 0U; + size_t tail = 0U; + size_t visited = 0U; + size_t i; + + if (graph == NULL) { + return XT_ERR_INVALID_ARGUMENT; + } + if (graph->node_count == 0U) { + return XT_OK; + } + + indegree = (size_t *)calloc(graph->node_count, sizeof(*indegree)); + queue = (size_t *)calloc(graph->node_count, sizeof(*queue)); + if (indegree == NULL || queue == NULL) { + free(indegree); + free(queue); + return XT_ERR_OUT_OF_MEMORY; + } + + for (i = 0U; i < graph->edge_count; i++) { + if (graph->edges[i].source >= graph->node_count || + graph->edges[i].destination >= graph->node_count) { + free(indegree); + free(queue); + return XT_ERR_INVALID_STATE; + } + indegree[graph->edges[i].destination]++; + } + + for (i = 0U; i < graph->node_count; i++) { + if (indegree[i] == 0U) { + queue[tail++] = i; + } + } + + while (head < tail) { + size_t node = queue[head++]; + visited++; + for (i = 0U; i < graph->edge_count; i++) { + if (graph->edges[i].source == node) { + size_t dest = graph->edges[i].destination; + if (indegree[dest] > 0U) { + indegree[dest]--; + if (indegree[dest] == 0U) { + queue[tail++] = dest; + } + } + } + } + } + + free(indegree); + free(queue); + return visited == graph->node_count ? XT_OK : XT_ERR_GRAPH_CYCLE; +} + +xt_result xt_graph_process_offline(xt_graph *graph, const float *input, + float *output, size_t frame_count, + float input_gain) +{ + size_t i; + float cumulative_gain = input_gain; + + if (graph == NULL || input == NULL || output == NULL) { + return XT_ERR_INVALID_ARGUMENT; + } + if (xt_graph_validate(graph) != XT_OK) { + return XT_ERR_INVALID_STATE; + } + + /* Temporary starter behavior: apply gains in node insertion order. + This is not yet graph-plan execution and is only for offline tests. */ + for (i = 0U; i < graph->node_count; i++) { + cumulative_gain *= graph->nodes[i].gain; + } + for (i = 0U; i < frame_count; i++) { + output[i] = input[i] * cumulative_gain; + } + return XT_OK; +} diff --git a/src/gui/main.cpp b/src/gui/main.cpp new file mode 100644 index 0000000..aa9bda5 --- /dev/null +++ b/src/gui/main.cpp @@ -0,0 +1,10 @@ +#include + +#include "main_window.hpp" + +int main(int argc, char **argv) +{ + MainWindow window; + window.show(argc, argv); + return Fl::run(); +} diff --git a/src/gui/main_window.cpp b/src/gui/main_window.cpp new file mode 100644 index 0000000..ceccb1d --- /dev/null +++ b/src/gui/main_window.cpp @@ -0,0 +1,52 @@ +#include "main_window.hpp" + +#include +#include +#include +#include + +extern "C" { +#include "xtlite/xtlite.h" +} + +MainWindow::MainWindow() + : Fl_Double_Window(1000, 680, "xtlite — modular audio workstation") +{ + begin(); + + Fl_Menu_Bar *menu = new Fl_Menu_Bar(0, 0, 1000, 28); + menu->add("File/New", 0, nullptr); + menu->add("File/Open...", 0, nullptr); + menu->add("File/Save", 0, nullptr); + menu->add("Help/About", 0, nullptr); + + Fl_Tabs *tabs = new Fl_Tabs(8, 36, 984, 600); + tabs->begin(); + + Fl_Group *sequencer = new Fl_Group(8, 60, 984, 576, "Sequencer"); + sequencer->begin(); + new Fl_Box(24, 84, 940, 40, + "Sequencer placeholder — timeline and clips are not implemented yet."); + sequencer->end(); + + Fl_Group *mixer = new Fl_Group(8, 60, 984, 576, "Mixer"); + mixer->begin(); + new Fl_Box(24, 84, 940, 40, + "Mixer placeholder — channels and meters are not implemented yet."); + mixer->end(); + + Fl_Group *graph = new Fl_Group(8, 60, 984, 576, "Modular Graph"); + graph->begin(); + new Fl_Box(24, 84, 940, 40, + "Modular graph placeholder — the C graph API is the current prototype."); + graph->end(); + + tabs->end(); + end(); + resizable(tabs); +} + +void MainWindow::show(int argc, char **argv) +{ + Fl_Double_Window::show(argc, argv); +} diff --git a/src/gui/main_window.hpp b/src/gui/main_window.hpp new file mode 100644 index 0000000..2b8df2e --- /dev/null +++ b/src/gui/main_window.hpp @@ -0,0 +1,12 @@ +#ifndef XTLITE_MAIN_WINDOW_HPP +#define XTLITE_MAIN_WINDOW_HPP + +#include + +class MainWindow : public Fl_Double_Window { +public: + MainWindow(); + void show(int argc, char **argv); +}; + +#endif diff --git a/src/midi/README.md b/src/midi/README.md new file mode 100644 index 0000000..1fd840f --- /dev/null +++ b/src/midi/README.md @@ -0,0 +1,5 @@ +# MIDI and events + +MIDI input, MIDI clips, event scheduling, panic behavior, and device handling +are not implemented yet. Define a bounded event representation and explicit +sample-offset semantics before adding real-time event delivery. diff --git a/src/persistence/README.md b/src/persistence/README.md new file mode 100644 index 0000000..1ccc1b9 --- /dev/null +++ b/src/persistence/README.md @@ -0,0 +1,5 @@ +# Persistence + +Project save/load is intentionally unimplemented in this scaffold. The project +format must be versioned, validated, and saved atomically. File I/O must never +occur in the audio callback. See `docs/PROJECT_FORMAT.md`. diff --git a/src/project/README.md b/src/project/README.md new file mode 100644 index 0000000..fbb0d9a --- /dev/null +++ b/src/project/README.md @@ -0,0 +1 @@ +Project domain model will live here. Keep it in C and separate from FLTK widgets. diff --git a/src/util/README.md b/src/util/README.md new file mode 100644 index 0000000..634738e --- /dev/null +++ b/src/util/README.md @@ -0,0 +1,5 @@ +# Utilities + +Put small, reusable C helpers here only when they have a clear owner and tests. +Avoid creating a generic utility framework. Validate sizes and allocation +arithmetic, and document ownership for allocated data. diff --git a/tests/fixtures/README.md b/tests/fixtures/README.md new file mode 100644 index 0000000..07f600d --- /dev/null +++ b/tests/fixtures/README.md @@ -0,0 +1,4 @@ +# Test fixtures + +Store small, redistributable fixtures here. Document their origin and license. +Do not commit large recordings or assets with unclear licensing. diff --git a/tests/integration/README.md b/tests/integration/README.md new file mode 100644 index 0000000..d19d0c3 --- /dev/null +++ b/tests/integration/README.md @@ -0,0 +1,11 @@ +# Integration tests + +Planned integration coverage: +- deterministic offline render; +- repeated engine initialization and shutdown; +- graph replacement while processing; +- project save/load round trip; +- backend disconnect/reconnect; +- MIDI panic and transport stop. + +Do not mark these tests as passing until they exist and run. diff --git a/tests/unit/test_core.c b/tests/unit/test_core.c new file mode 100644 index 0000000..0d97e48 --- /dev/null +++ b/tests/unit/test_core.c @@ -0,0 +1,43 @@ +#include "xtlite/xtlite.h" +#include "xtlite/xtlite_engine.h" + +#include +#include + +#define CHECK(condition) do { \ + if (!(condition)) { \ + (void)fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, #condition); \ + return 1; \ + } \ +} while (0) + +int main(void) +{ + xt_app *app = NULL; + xt_graph *graph = NULL; + size_t node_a = 0U; + size_t node_b = 0U; + float input[4] = {1.0f, -1.0f, 0.5f, 0.0f}; + float output[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + + CHECK(xt_app_create(&app) == XT_OK); + CHECK(app != NULL); + xt_app_destroy(app); + + CHECK(xt_graph_create(4U, &graph) == XT_OK); + CHECK(xt_graph_add_gain(graph, 0.5f, &node_a) == XT_OK); + CHECK(xt_graph_add_gain(graph, 2.0f, &node_b) == XT_OK); + CHECK(xt_graph_connect(graph, node_a, node_b) == XT_OK); + CHECK(xt_graph_validate(graph) == XT_OK); + CHECK(xt_graph_process_offline(graph, input, output, 4U, 1.0f) == XT_OK); + CHECK(fabsf(output[0] - 1.0f) < 0.0001f); + CHECK(fabsf(output[1] + 1.0f) < 0.0001f); + CHECK(fabsf(output[2] - 0.5f) < 0.0001f); + + CHECK(xt_graph_connect(graph, node_b, node_a) == XT_OK); + CHECK(xt_graph_validate(graph) == XT_ERR_GRAPH_CYCLE); + + xt_graph_destroy(graph); + (void)puts("All xtlite core tests passed."); + return 0; +}