baseline: recover workspace from iCloud damage

This commit is contained in:
jess 2026-03-30 18:34:56 -07:00
commit 725b18dc72
198 changed files with 28516 additions and 0 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
*.code-workspace
*.py
*.lock
target/
.DS_Store
.worktree/
.claude/
.loki/
.idea/
.vscode/

26
Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[workspace]
resolver = "3"
exclude = [
"oxide-modules/input",
"oxide-modules/output",
]
default-members = ["au-o2-gui"]
members = [
"oxforge",
"au-o2-gui",
"oxide-modules/hilbert",
"oxide-modules/region_player",
"oxide-modules/input_router",
"oxide-modules/recorder",
"oxide-modules/output_mixer",
"oxide-modules/spiral_visualizer",
"oxide-modules/passthrough",
"oxide-modules/latency",
"oxide-modules/metronome_midi",
"oxide-modules/click_instrument",
"oxide-modules/gain",
"oxide-modules/eq",
"oxide-modules/compressor",
"oxide-modules/midi_player",
"oxide-modules/phase_compressor",
]

1
au-o2-gui/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

55
au-o2-gui/Cargo.toml Normal file
View File

@ -0,0 +1,55 @@
[package]
name = "au-o2-gui"
version = "0.1.0"
edition = "2024"
[[bin]]
name = "au-o2-gui"
path = "src/main.rs"
[features]
debug-log = []
[dependencies]
# --- GUI Framework ---
iced = { version = "0.13.1", features = ["tokio", "debug", "canvas", "advanced", "multi-window", "system", "svg", "tiny-skia", "web-colors", "markdown", "image"] }
# --- Core Audio & Hardware ---
cpal = "0.16.0"
ringbuf = "0.4.8"
crossbeam-channel = "0.5.12"
rubato = "0.14"
# --- Module System ---
oxforge = { path = "../oxforge" }
libloading = "0.8"
rustfft = "6"
# --- Oxide Modules ---
oxide-hilbert = { path = "../oxide-modules/hilbert" }
oxide-region-player = { path = "../oxide-modules/region_player" }
oxide-input-router = { path = "../oxide-modules/input_router" }
oxide-recorder = { path = "../oxide-modules/recorder" }
oxide-output-mixer = { path = "../oxide-modules/output_mixer" }
oxide-spiral-visualizer = { path = "../oxide-modules/spiral_visualizer" }
oxide-metronome-midi = { path = "../oxide-modules/metronome_midi" }
oxide-click-instrument = { path = "../oxide-modules/click_instrument" }
oxide-gain = { path = "../oxide-modules/gain" }
oxide-eq = { path = "../oxide-modules/eq" }
oxide-compressor = { path = "../oxide-modules/compressor" }
oxide-midi-player = { path = "../oxide-modules/midi_player" }
# --- Codec ---
flacenc = "0.4"
claxon = "0.4"
hound = "3.5"
# --- Configuration & Project Management ---
confy = "1.0.0"
serde = { version = "1.0", features = ["derive"] }
toml = "0.9.7"
dirs = "6.0.0"
uuid = { version = "1.18.1", features = ["v4", "serde"] }
chrono = { version = "0.4.42", features = ["serde"] }
tokio = "1.47.1"
muda = { version = "0.17.1", default-features = false }

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>Audio Oxide</string>
<key>CFBundleDisplayName</key>
<string>Audio Oxide</string>
<key>CFBundleIdentifier</key>
<string>org.else-if.audio-oxide</string>
<key>CFBundleVersion</key>
<string>0.1.0</string>
<key>CFBundleShortVersionString</key>
<string>0.1.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>
<string>au-o2-gui</string>
<key>CFBundleIconFile</key>
<string>icon</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>NSMicrophoneUsageDescription</key>
<string>Audio Oxide needs microphone access for audio recording.</string>
</dict>
</plist>

BIN
au-o2-gui/assets/icon.icns Normal file

Binary file not shown.

24
au-o2-gui/assets/icon.svg Normal file
View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 66.3 66.3" style="enable-background:new 0 0 66.3 66.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#11007F;}
.st1{fill:#F9C543;}
.st2{font-family:'ArialMT';}
.st3{font-size:20.3402px;}
.st4{fill:#DD0074;}
.st5{font-size:12px;}
.st6{font-size:28.3253px;}
</style>
<g id="Text">
<circle class="st0" cx="45.5" cy="33.1" r="20"/>
<circle class="st1" cx="20.8" cy="33.1" r="20"/>
<circle class="st1" cx="45.5" cy="33.1" r="18.8"/>
<circle class="st0" cx="20.8" cy="33.1" r="18.8"/>
<text transform="matrix(1 0 0 1 7.4957 39.8482)" class="st1 st2 st3">Au</text>
<text transform="matrix(1 0 0 1 30.163 42.6969)" class="st4 st2 st5">2</text>
<text transform="matrix(1 0 0 1 38.9518 42.6969)" class="st0 st2 st6">O</text>
<text transform="matrix(1 0 0 1 56.5909 43.1403)" class="st4 st2 st5">3</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 18 8 13 13 15 21 6"/>
<circle class="fillable" fill="none" cx="8" cy="13" r="2"/>
<circle class="fillable" fill="none" cx="13" cy="15" r="2"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect class="fillable" fill="none" x="9" y="9" width="13" height="13" rx="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</svg>

After

Width:  |  Height:  |  Size: 339 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="19" x2="20" y2="19"/>
<line x1="6" y1="19" x2="6" y2="9"/>
<line x1="10" y1="19" x2="10" y2="12"/>
<line x1="14" y1="19" x2="14" y2="12"/>
<line x1="18" y1="19" x2="18" y2="12"/>
<circle class="fillable" fill="none" cx="6" cy="7" r="2"/>
</svg>

After

Width:  |  Height:  |  Size: 447 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle class="fillable" fill="none" cx="6" cy="6" r="3"/>
<circle class="fillable" fill="none" cx="6" cy="18" r="3"/>
<line x1="20" y1="4" x2="8.12" y2="15.88"/>
<line x1="14.47" y1="14.48" x2="20" y2="20"/>
<line x1="8.12" y1="8.12" x2="12" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 445 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.65 6.35A8 8 0 0 0 4 12"/>
<polyline points="4 8 4 12 8 12"/>
<path d="M6.35 17.65A8 8 0 0 0 20 12"/>
<polyline points="20 16 20 12 16 12"/>
</svg>

After

Width:  |  Height:  |  Size: 345 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12c2-4 5-6 7-4s3 6 5 4 4-6 5-4"/>
</svg>

After

Width:  |  Height:  |  Size: 238 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polygon class="fillable" fill="none" points="3,6 11,12 3,18"/>
<polygon class="fillable" fill="none" points="11,6 19,12 11,18"/>
</svg>

After

Width:  |  Height:  |  Size: 322 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="2" x2="12" y2="22"/>
<line x1="2" y1="12" x2="22" y2="12"/>
<line x1="4.93" y1="4.93" x2="19.07" y2="19.07"/>
<line x1="19.07" y1="4.93" x2="4.93" y2="19.07"/>
<circle cx="12" cy="12" r="2"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polygon class="fillable" fill="none" points="11,5 6,9 2,9 2,15 6,15 11,19"/>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
</svg>

After

Width:  |  Height:  |  Size: 355 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v8"/>
<circle class="fillable" fill="none" cx="12" cy="14" r="4"/>
<path d="M12 18v4"/>
<path d="M8 14H2"/>
<path d="M22 14h-6"/>
</svg>

After

Width:  |  Height:  |  Size: 336 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 7 8 12 4 17"/>
<polyline points="20 7 16 12 20 17"/>
<line x1="8" y1="12" x2="16" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect class="fillable" fill="none" x="3" y="11" width="18" height="10" rx="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M8 20h8l-2-14h-4z"/>
<line x1="12" y1="16" x2="16" y2="6"/>
<circle cx="16.5" cy="5.5" r="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polygon class="fillable" fill="none" points="11,5 6,9 2,9 2,15 6,15 11,19"/>
<line x1="22" y1="9" x2="16" y2="15"/>
<line x1="16" y1="9" x2="22" y2="15"/>
</svg>

After

Width:  |  Height:  |  Size: 348 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 8 7 12 3 16"/>
<polyline points="21 8 17 12 21 16"/>
<line x1="7" y1="12" x2="17" y2="12"/>
<line x1="12" y1="7" x2="12" y2="17"/>
</svg>

After

Width:  |  Height:  |  Size: 343 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
<rect x="8" y="2" width="8" height="4" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect class="fillable" fill="none" x="5" y="4" width="4" height="16" rx="1"/>
<rect class="fillable" fill="none" x="15" y="4" width="4" height="16" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 349 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polygon class="fillable" fill="none" points="6,4 20,12 6,20"/>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 4v16"/>
<path d="M16 4v16"/>
<path d="M4 8h4"/>
<path d="M4 16h4"/>
<path d="M16 8h4"/>
<path d="M16 16h4"/>
</svg>

After

Width:  |  Height:  |  Size: 299 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="8"/>
<circle class="fillable" fill="none" cx="12" cy="12" r="4"/>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle class="fillable" fill="none" cx="12" cy="12" r="7"/>
</svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.13-9.36L23 10"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polygon class="fillable" fill="none" points="13,6 5,12 13,18"/>
<polygon class="fillable" fill="none" points="21,6 13,12 21,18"/>
</svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="5" x2="5" y2="19"/>
<polygon class="fillable" fill="none" points="20,5 9,12 20,19"/>
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
<polyline points="17 21 17 13 7 13 7 21"/>
<polyline points="7 3 7 8 15 8"/>
</svg>

After

Width:  |  Height:  |  Size: 374 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="7" y1="17" x2="17" y2="7"/>
<polyline points="7 7 17 7 17 17"/>
</svg>

After

Width:  |  Height:  |  Size: 266 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="3"/>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg>

After

Width:  |  Height:  |  Size: 851 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M3 18V6a2 2 0 0 1 2-2h3.28a1 1 0 0 1 .71.3l2.3 2.4a1 1 0 0 0 .71.3H19a2 2 0 0 1 2 2v2"/>
<path d="M8 18a6 6 0 0 1 12 0"/>
<circle cx="14" cy="12" r="2"/>
</svg>

After

Width:  |  Height:  |  Size: 384 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect class="fillable" fill="none" x="5" y="5" width="14" height="14" rx="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 273 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M7 21l-4-4a1 1 0 0 1 0-1.41l9.59-9.59a2 2 0 0 1 2.83 0l3.58 3.59a2 2 0 0 1 0 2.82L10 21z"/>
<line x1="7" y1="21" x2="21" y2="21"/>
</svg>

After

Width:  |  Height:  |  Size: 361 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M7 11h10l1 9H6z"/>
<path d="M8 11V8a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v3"/>
<path d="M10 7V5h4v2"/>
<line x1="9" y1="15" x2="15" y2="15"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M16.5 3.5a2.12 2.12 0 0 1 3 3L7 19l-4 1 1-4z"/>
</svg>

After

Width:  |  Height:  |  Size: 278 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M4 4l6 15 2-6 6-2z"/>
<line x1="13" y1="13" x2="20" y2="20"/>
</svg>

After

Width:  |  Height:  |  Size: 292 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle class="fillable" fill="none" cx="6" cy="6" r="3"/>
<circle class="fillable" fill="none" cx="6" cy="18" r="3"/>
<line x1="20" y1="4" x2="8.12" y2="15.88"/>
<line x1="14.47" y1="14.48" x2="20" y2="20"/>
<line x1="8.12" y1="8.12" x2="12" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 445 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12c1.5-3 3-5 5-5s3.5 3 5 5 3 5 5 5 3.5-3 5-5"/>
</svg>

After

Width:  |  Height:  |  Size: 252 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="3" x2="12" y2="12"/>
<line x1="12" y1="12" x2="5" y2="21"/>
<line x1="12" y1="12" x2="19" y2="21"/>
</svg>

After

Width:  |  Height:  |  Size: 310 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="3" x2="5" y2="12"/>
<line x1="19" y1="3" x2="19" y2="12"/>
<line x1="5" y1="12" x2="12" y2="12"/>
<line x1="19" y1="12" x2="12" y2="12"/>
<line x1="12" y1="12" x2="12" y2="21"/>
</svg>

After

Width:  |  Height:  |  Size: 387 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="4" width="4" height="16" rx="1"/>
<rect class="fillable" fill="none" x="7" y="4" width="4" height="8" rx="0"/>
<rect x="11" y="4" width="4" height="16" rx="1"/>
<rect class="fillable" fill="none" x="15" y="4" width="4" height="8" rx="0"/>
<rect x="19" y="4" width="2" height="16" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<polyline points="1 4 1 10 7 10"/>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
</svg>

After

Width:  |  Height:  |  Size: 273 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<rect x="3" y="3" width="7" height="5" rx="1"/>
<rect x="14" y="3" width="7" height="5" rx="1"/>
<rect x="3" y="11" width="7" height="5" rx="1"/>
<rect x="14" y="11" width="7" height="5" rx="1"/>
<polygon points="5,13 5,15 7,14" fill="currentColor" stroke="none"/>
</svg>

After

Width:  |  Height:  |  Size: 396 B

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect x="3" y="3" width="18" height="18" rx="2"/>
<line x1="3" y1="9" x2="21" y2="9"/>
<line x1="3" y1="15" x2="21" y2="15"/>
<line x1="9" y1="3" x2="9" y2="21"/>
<line x1="15" y1="3" x2="15" y2="21"/>
</svg>

After

Width:  |  Height:  |  Size: 394 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="9"/>
<line x1="12" y1="11" x2="12" y2="17"/>
<circle class="fillable" fill="none" cx="12" cy="8" r="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
<path class="fillable" fill="none" d="M4 19.5V5a2 2 0 0 1 2-2h14v14H6.5A2.5 2.5 0 0 0 4 19.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

View File

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<line x1="4" y1="21" x2="4" y2="3"/>
<line x1="12" y1="21" x2="12" y2="3"/>
<line x1="20" y1="21" x2="20" y2="3"/>
<rect class="fillable" fill="none" x="2" y="8" width="4" height="4" rx="1"/>
<rect class="fillable" fill="none" x="10" y="14" width="4" height="4" rx="1"/>
<rect class="fillable" fill="none" x="18" y="6" width="4" height="4" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 541 B

View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path class="fillable" fill="none" d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="8" y1="13" x2="16" y2="13"/>
<line x1="8" y1="17" x2="13" y2="17"/>
</svg>

After

Width:  |  Height:  |  Size: 406 B

View File

@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<rect class="fillable" fill="none" x="3" y="3" width="4" height="4" rx="0.5"/>
<rect x="9" y="3" width="4" height="4" rx="0.5"/>
<rect class="fillable" fill="none" x="15" y="3" width="4" height="4" rx="0.5"/>
<rect x="3" y="10" width="4" height="4" rx="0.5"/>
<rect class="fillable" fill="none" x="9" y="10" width="4" height="4" rx="0.5"/>
<rect x="15" y="10" width="4" height="4" rx="0.5"/>
<rect x="3" y="17" width="4" height="4" rx="0.5"/>
<rect class="fillable" fill="none" x="9" y="17" width="4" height="4" rx="0.5"/>
<rect class="fillable" fill="none" x="15" y="17" width="4" height="4" rx="0.5"/>
</svg>

After

Width:  |  Height:  |  Size: 796 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12h3l2-6 3 12 3-12 2 6h3"/>
<rect class="fillable" fill="none" x="19" y="8" width="3" height="8" rx="1"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 66.3 66.3" style="enable-background:new 0 0 66.3 66.3;" xml:space="preserve">
<style type="text/css">
.st0{fill:#11007F;}
.st1{fill:#F9C543;}
.st2{font-family:'ArialMT';}
.st3{font-size:20.3402px;}
.st4{fill:#DD0074;}
.st5{font-size:12px;}
.st6{font-size:28.3253px;}
.st7{fill:none;stroke:#FFFFFF;stroke-width:0.75;stroke-miterlimit:10;}
</style>
<g id="Text">
<circle class="st0" cx="45.5" cy="33.1" r="20"/>
<circle class="st1" cx="20.8" cy="33.1" r="20"/>
<circle class="st1" cx="45.5" cy="33.1" r="18.8"/>
<circle class="st0" cx="20.8" cy="33.1" r="18.8"/>
<text transform="matrix(1 0 0 1 7.4957 39.8482)" class="st1 st2 st3">Au</text>
<text transform="matrix(1 0 0 1 30.163 42.6969)" class="st4 st2 st5">2</text>
<text transform="matrix(1 0 0 1 38.9518 42.6969)" class="st0 st2 st6">O</text>
<text transform="matrix(1 0 0 1 56.5909 43.1403)" class="st4 st2 st5">3</text>
</g>
<circle class="st7" cx="33.1" cy="33.1" r="32.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,49 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" width="256" height="256">
<defs>
<linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#007AFF"/>
<stop offset="100%" stop-color="#005CBF"/>
</linearGradient>
</defs>
<circle cx="128" cy="128" r="120" fill="none" stroke="url(#g1)" stroke-width="4"/>
<circle cx="128" cy="128" r="100" fill="none" stroke="#007AFF" stroke-width="1.5" opacity="0.3"/>
<!-- Waveform -->
<path d="M 40 128
Q 56 128, 64 108
Q 72 88, 80 128
Q 88 168, 96 128
Q 100 108, 108 68
Q 116 28, 124 128
Q 132 228, 140 128
Q 144 88, 152 128
Q 160 168, 168 128
Q 176 88, 184 128
Q 192 148, 200 128
Q 208 118, 216 128"
fill="none" stroke="url(#g1)" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Analytic envelope (ghost) -->
<path d="M 40 128
Q 56 126, 64 118
Q 72 110, 80 114
Q 88 118, 96 98
Q 104 78, 112 70
Q 120 62, 128 66
Q 136 70, 144 82
Q 152 94, 160 102
Q 168 110, 176 114
Q 184 118, 192 122
Q 200 124, 216 128"
fill="none" stroke="#007AFF" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.5" stroke-linecap="round"/>
<path d="M 40 128
Q 56 130, 64 138
Q 72 146, 80 142
Q 88 138, 96 158
Q 104 178, 112 186
Q 120 194, 128 190
Q 136 186, 144 174
Q 152 162, 160 154
Q 168 146, 176 142
Q 184 138, 192 134
Q 200 132, 216 128"
fill="none" stroke="#007AFF" stroke-width="1.5" stroke-dasharray="4 3" opacity="0.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

140
au-o2-gui/src/automation.rs Normal file
View File

@ -0,0 +1,140 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AutomationMode {
Off,
Read,
Write,
Touch,
Latch,
}
impl Default for AutomationMode {
fn default() -> Self { Self::Off }
}
impl AutomationMode {
pub const ALL: [AutomationMode; 5] = [
AutomationMode::Off,
AutomationMode::Read,
AutomationMode::Write,
AutomationMode::Touch,
AutomationMode::Latch,
];
pub fn reads(&self) -> bool {
matches!(self, Self::Read | Self::Touch | Self::Latch)
}
pub fn writes(&self) -> bool {
matches!(self, Self::Write | Self::Touch | Self::Latch)
}
}
impl std::fmt::Display for AutomationMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Off => write!(f, "Off"),
Self::Read => write!(f, "Read"),
Self::Write => write!(f, "Write"),
Self::Touch => write!(f, "Touch"),
Self::Latch => write!(f, "Latch"),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum AutomationTarget {
Volume,
Pan,
Mute,
ModuleParam { module_id: u32, key: String },
}
impl std::fmt::Display for AutomationTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Volume => write!(f, "Volume"),
Self::Pan => write!(f, "Pan"),
Self::Mute => write!(f, "Mute"),
Self::ModuleParam { key, .. } => write!(f, "{key}"),
}
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct AutomationPoint {
pub sample_pos: u64,
pub value: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutomationLane {
pub target: AutomationTarget,
pub points: Vec<AutomationPoint>,
pub visible: bool,
}
impl AutomationLane {
pub fn new(target: AutomationTarget) -> Self {
Self {
target,
points: Vec::new(),
visible: true,
}
}
pub fn insert_point(&mut self, sample_pos: u64, value: f32) {
let pt = AutomationPoint { sample_pos, value };
match self.points.binary_search_by_key(&sample_pos, |p| p.sample_pos) {
Ok(idx) => self.points[idx] = pt,
Err(idx) => self.points.insert(idx, pt),
}
}
pub fn remove_point(&mut self, index: usize) {
if index < self.points.len() {
self.points.remove(index);
}
}
/// Linear interpolation at a given sample position
pub fn value_at(&self, sample_pos: u64) -> Option<f32> {
if self.points.is_empty() {
return None;
}
if self.points.len() == 1 {
return Some(self.points[0].value);
}
let first = &self.points[0];
if sample_pos <= first.sample_pos {
return Some(first.value);
}
let last = &self.points[self.points.len() - 1];
if sample_pos >= last.sample_pos {
return Some(last.value);
}
// Binary search for surrounding points
let idx = match self.points.binary_search_by_key(&sample_pos, |p| p.sample_pos) {
Ok(i) => return Some(self.points[i].value),
Err(i) => i,
};
let a = &self.points[idx - 1];
let b = &self.points[idx];
let t = (sample_pos - a.sample_pos) as f32 / (b.sample_pos - a.sample_pos) as f32;
Some(a.value + (b.value - a.value) * t)
}
pub fn value_range(&self) -> (f32, f32) {
match &self.target {
AutomationTarget::Volume => (0.0, 1.5),
AutomationTarget::Pan => (-1.0, 1.0),
AutomationTarget::Mute => (0.0, 1.0),
AutomationTarget::ModuleParam { .. } => (0.0, 1.0),
}
}
}

View File

@ -0,0 +1,49 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
// Time utility
TimeUtilityTapPressed,
TimeUtilityTapReleased,
// File menu (Cmd+key)
NewProject,
OpenProject,
SaveProject,
SaveProjectAs,
CloseProject,
// App menu
OpenSettings,
// Edit menu (Cmd+key)
Undo,
Redo,
Cut,
Copy,
Paste,
Duplicate,
SelectAll,
Delete,
// Editor transport
EditorTogglePlayback,
EditorStop,
EditorToggleRecord,
EditorPlayFromBeginning,
EditorRewind,
// Editor view toggles
EditorToggleInspector,
EditorToggleBottomPanel,
EditorToggleMixer,
EditorToggleToolbar,
// Editor mode toggles
EditorToggleCycle,
EditorToggleMetronome,
// Editor zoom
ZoomInH,
ZoomOutH,
ZoomInV,
ZoomOutV,
}

View File

@ -0,0 +1,18 @@
use crate::region::Region;
#[derive(Debug, Clone)]
pub struct ClipboardEntry {
pub region: Region,
pub source_track_index: usize,
}
#[derive(Debug, Clone, Default)]
pub struct Clipboard {
pub entries: Vec<ClipboardEntry>,
}
impl Clipboard {
pub fn new() -> Self { Self { entries: Vec::new() } }
pub fn is_empty(&self) -> bool { self.entries.is_empty() }
pub fn clear(&mut self) { self.entries.clear(); }
}

View File

@ -0,0 +1,28 @@
use std::fmt;
#[derive(Debug)]
pub enum XtcError {
Io(std::io::Error),
Encode(String),
Decode(String),
InvalidFormat(String),
}
impl fmt::Display for XtcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
XtcError::Io(e) => write!(f, "I/O error: {}", e),
XtcError::Encode(e) => write!(f, "encode error: {}", e),
XtcError::Decode(e) => write!(f, "decode error: {}", e),
XtcError::InvalidFormat(e) => write!(f, "invalid format: {}", e),
}
}
}
impl std::error::Error for XtcError {}
impl From<std::io::Error> for XtcError {
fn from(e: std::io::Error) -> Self {
XtcError::Io(e)
}
}

View File

@ -0,0 +1,4 @@
mod error;
mod xtc;
pub use xtc::{XtcDecoder, XtcEncoder};

239
au-o2-gui/src/codec/xtc.rs Normal file
View File

@ -0,0 +1,239 @@
use std::path::Path;
use flacenc::component::BitRepr;
use flacenc::error::Verify;
use super::error::XtcError;
const XTC_VERSION: &str = "1";
const XTC_CHANNEL_LAYOUT: &str = "REAL_L,REAL_R,HILBERT_L,HILBERT_R";
const CHANNELS: u32 = 4;
pub struct XtcEncoder {
sample_rate: u32,
bit_depth: u16,
fft_size: u32,
}
impl XtcEncoder {
pub fn new(sample_rate: u32, bit_depth: u16, fft_size: u32) -> Self {
Self { sample_rate, bit_depth, fft_size }
}
pub fn encode_to_file(
&self,
path: &Path,
real_l: &[f32],
real_r: &[f32],
imag_l: &[f32],
imag_r: &[f32],
) -> Result<(), XtcError> {
let n = real_l.len();
if real_r.len() != n || imag_l.len() != n || imag_r.len() != n {
return Err(XtcError::Encode("channel length mismatch".into()));
}
let interleaved = interleave_4ch(real_l, real_r, imag_l, imag_r, self.bit_depth);
let config = flacenc::config::Encoder::default()
.into_verified()
.map_err(|e| XtcError::Encode(format!("{:?}", e)))?;
let source = flacenc::source::MemSource::from_samples(
&interleaved,
CHANNELS as usize,
self.bit_depth as usize,
self.sample_rate as usize,
);
let mut stream = flacenc::encode_with_fixed_block_size(&config, source, config.block_size)
.map_err(|e| XtcError::Encode(format!("{:?}", e)))?;
let vorbis_block = build_vorbis_comment(self.fft_size)?;
stream.add_metadata_block(vorbis_block);
let mut sink = flacenc::bitsink::ByteSink::new();
stream.write(&mut sink)
.map_err(|_| XtcError::Encode("failed to write stream".into()))?;
std::fs::write(path, sink.as_slice())?;
Ok(())
}
}
#[allow(dead_code)] // metadata fields parsed from XTC header, used in tests + future playback
pub struct XtcDecoder {
pub sample_rate: u32,
pub bit_depth: u32,
pub fft_size: u32,
pub total_samples: u64,
}
impl XtcDecoder {
pub fn open(path: &Path) -> Result<Self, XtcError> {
let reader = claxon::FlacReader::open(path)
.map_err(|e| XtcError::Decode(format!("{}", e)))?;
let info = reader.streaminfo();
if info.channels != CHANNELS {
return Err(XtcError::InvalidFormat(
format!("expected {} channels, got {}", CHANNELS, info.channels),
));
}
let mut fft_size = 2048u32;
for val in reader.get_tag("XTC_FFT_SIZE") {
if let Ok(v) = val.parse::<u32>() {
fft_size = v;
}
}
Ok(Self {
sample_rate: info.sample_rate,
bit_depth: info.bits_per_sample,
total_samples: info.samples.unwrap_or(0),
fft_size,
})
}
pub fn decode_real(&self, path: &Path) -> Result<(Vec<f32>, Vec<f32>), XtcError> {
let mut reader = claxon::FlacReader::open(path)
.map_err(|e| XtcError::Decode(format!("{}", e)))?;
let info = reader.streaminfo();
let scale = f32_scale(info.bits_per_sample);
let ch = info.channels as usize;
let n = info.samples.unwrap_or(0) as usize;
let total = n * ch;
let mut left = Vec::with_capacity(n);
let mut right = Vec::with_capacity(n);
let mut idx = 0usize;
for sample in reader.samples() {
if idx >= total { break; }
let s = sample.map_err(|e| XtcError::Decode(format!("{}", e)))?;
let v = s as f32 * scale;
match idx % ch {
0 => left.push(v),
1 => right.push(v),
_ => {}
}
idx += 1;
}
Ok((left, right))
}
}
fn f32_scale(bits_per_sample: u32) -> f32 {
let bits = bits_per_sample.clamp(1, 32);
1.0 / ((1i64 << (bits - 1)) - 1) as f32
}
fn quantize(sample: f32, bit_depth: u16) -> i32 {
let bits = bit_depth.clamp(1, 32);
let max = ((1i64 << (bits - 1)) - 1) as f32;
(sample.clamp(-1.0, 1.0) * max) as i32
}
fn interleave_4ch(
ch0: &[f32],
ch1: &[f32],
ch2: &[f32],
ch3: &[f32],
bit_depth: u16,
) -> Vec<i32> {
let n = ch0.len();
let mut out = Vec::with_capacity(n * 4);
for i in 0..n {
out.push(quantize(ch0[i], bit_depth));
out.push(quantize(ch1[i], bit_depth));
out.push(quantize(ch2[i], bit_depth));
out.push(quantize(ch3[i], bit_depth));
}
out
}
fn build_vorbis_comment(fft_size: u32) -> Result<flacenc::component::MetadataBlockData, XtcError> {
let vendor = b"audio-oxide";
let comments = [
format!("XTC_VERSION={}", XTC_VERSION),
format!("XTC_CHANNEL_LAYOUT={}", XTC_CHANNEL_LAYOUT),
format!("XTC_FFT_SIZE={}", fft_size),
];
let mut data = Vec::new();
// Vendor string (LE u32 length + bytes)
data.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
data.extend_from_slice(vendor);
// Number of comments (LE u32)
data.extend_from_slice(&(comments.len() as u32).to_le_bytes());
for comment in &comments {
let bytes = comment.as_bytes();
data.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
data.extend_from_slice(bytes);
}
// VORBIS_COMMENT is metadata block type 4
flacenc::component::MetadataBlockData::new_unknown(4, &data)
.map_err(|e| XtcError::Encode(format!("vorbis comment block: {:?}", e)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_24bit() {
let n = 4096;
let real_l: Vec<f32> = (0..n).map(|i| (i as f32 / n as f32 * std::f32::consts::TAU).sin()).collect();
let real_r: Vec<f32> = (0..n).map(|i| (i as f32 / n as f32 * std::f32::consts::TAU * 2.0).sin()).collect();
let imag_l: Vec<f32> = (0..n).map(|i| (i as f32 / n as f32 * std::f32::consts::TAU).cos()).collect();
let imag_r: Vec<f32> = (0..n).map(|i| (i as f32 / n as f32 * std::f32::consts::TAU * 2.0).cos()).collect();
let path = std::env::temp_dir().join("test_xtc_roundtrip.xtc");
let encoder = XtcEncoder::new(48000, 24, 2048);
encoder.encode_to_file(&path, &real_l, &real_r, &imag_l, &imag_r).unwrap();
let decoder = XtcDecoder::open(&path).unwrap();
assert_eq!(decoder.sample_rate, 48000);
assert_eq!(decoder.bit_depth, 24);
assert_eq!(decoder.fft_size, 2048);
let (left, right) = decoder.decode_real(&path).unwrap();
assert_eq!(left.len(), n);
assert_eq!(right.len(), n);
let tolerance = 2.0 / (1 << 23) as f32;
for i in 0..n {
assert!((left[i] - real_l[i]).abs() < tolerance, "real_l[{}]: {} vs {}", i, left[i], real_l[i]);
assert!((right[i] - real_r[i]).abs() < tolerance, "real_r[{}]", i);
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn decode_real_only() {
let n = 1024;
let real_l: Vec<f32> = (0..n).map(|i| (i as f32 / n as f32 * std::f32::consts::TAU).sin() * 0.5).collect();
let real_r = real_l.clone();
let imag_l = vec![0.0f32; n];
let imag_r = vec![0.0f32; n];
let path = std::env::temp_dir().join("test_xtc_real_only.xtc");
let encoder = XtcEncoder::new(44100, 16, 1024);
encoder.encode_to_file(&path, &real_l, &real_r, &imag_l, &imag_r).unwrap();
let decoder = XtcDecoder::open(&path).unwrap();
let (left, right) = decoder.decode_real(&path).unwrap();
assert_eq!(left.len(), n);
assert_eq!(right.len(), n);
let _ = std::fs::remove_file(&path);
}
}

179
au-o2-gui/src/config.rs Normal file
View File

@ -0,0 +1,179 @@
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AudioOxideConfig {
pub first_run: bool,
pub project_dir: PathBuf,
// Audio defaults
#[serde(default = "default_sample_rate")]
pub default_sample_rate: u32,
#[serde(default = "default_buffer_size", alias = "default_buffer_size")]
pub default_output_buffer_size: u32,
#[serde(default = "default_buffer_size")]
pub default_input_buffer_size: u32,
#[serde(default = "default_audio_device")]
pub default_audio_device: String,
#[serde(default = "default_audio_device")]
pub default_input_device: String,
#[serde(default = "default_recording_format")]
pub recording_format: RecordingFormat,
#[serde(default = "default_bit_depth")]
pub recording_bit_depth: u16,
#[serde(default = "default_true")]
pub auto_oversample: bool,
#[serde(default = "default_true")]
pub auto_undersample: bool,
#[serde(default = "default_fft_size")]
pub hilbert_fft_size: u32,
#[serde(default = "default_viz_buffer_size")]
pub visualizer_buffer_size: u32,
// General
#[serde(default = "default_true")]
pub auto_save: bool,
#[serde(default = "default_auto_save_interval")]
pub auto_save_interval_secs: u32,
#[serde(default = "default_true")]
pub ask_to_save_on_close: bool,
// Display
#[serde(default = "default_track_height")]
pub default_track_height: f32,
#[serde(default = "default_true")]
pub show_toolbar_on_open: bool,
#[serde(default)]
pub show_inspector_on_open: bool,
#[serde(default)]
pub zoom_mode: ZoomMode,
}
impl Default for AudioOxideConfig {
fn default() -> Self {
Self {
first_run: true,
project_dir: dirs::home_dir().unwrap_or_default().join("Oxide/Projects"),
default_sample_rate: 48000,
default_output_buffer_size: 512,
default_input_buffer_size: 512,
default_audio_device: "Default".to_string(),
default_input_device: "Default".to_string(),
recording_format: RecordingFormat::Wav,
recording_bit_depth: 24,
auto_oversample: true,
auto_undersample: true,
hilbert_fft_size: 2048,
visualizer_buffer_size: 4096,
auto_save: true,
auto_save_interval_secs: 300,
ask_to_save_on_close: true,
default_track_height: 160.0,
show_toolbar_on_open: true,
show_inspector_on_open: false,
zoom_mode: ZoomMode::default(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecordingFormat {
Wav,
Aiff,
Caf,
Xtc,
}
impl RecordingFormat {
pub const ALL: [RecordingFormat; 4] = [
RecordingFormat::Wav,
RecordingFormat::Aiff,
RecordingFormat::Caf,
RecordingFormat::Xtc,
];
}
impl std::fmt::Display for RecordingFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RecordingFormat::Wav => write!(f, "WAV"),
RecordingFormat::Aiff => write!(f, "AIFF"),
RecordingFormat::Caf => write!(f, "CAF"),
RecordingFormat::Xtc => write!(f, "XTC"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ProjectConfig {
pub name: String,
pub sample_rate: u32,
#[serde(default = "default_buffer_size", alias = "buffer_size")]
pub output_buffer_size: u32,
#[serde(default = "default_buffer_size")]
pub input_buffer_size: u32,
pub audio_device: String,
#[serde(default = "default_audio_device")]
pub audio_input_device: String,
#[serde(default = "default_true")]
pub auto_oversample: bool,
#[serde(default = "default_true")]
pub auto_undersample: bool,
pub tempo: f32,
pub time_signature_numerator: u8,
pub time_signature_denominator: u8,
#[serde(default)]
pub tracks: Vec<crate::track::Track>,
#[serde(default)]
pub markers: Vec<crate::timing::Marker>,
#[serde(default)]
pub tempo_points: Vec<crate::timing::TempoPoint>,
#[serde(default)]
pub groups: Vec<crate::track::TrackGroup>,
}
impl Default for ProjectConfig {
fn default() -> Self {
Self {
name: "Untitled".to_string(),
sample_rate: 48000,
output_buffer_size: 512,
input_buffer_size: 512,
audio_device: "Default".to_string(),
audio_input_device: "Default".to_string(),
auto_oversample: true,
auto_undersample: true,
tempo: 120.0,
time_signature_numerator: 4,
time_signature_denominator: 4,
tracks: Vec::new(),
markers: Vec::new(),
tempo_points: Vec::new(),
groups: Vec::new(),
}
}
}
fn default_sample_rate() -> u32 { 48000 }
fn default_buffer_size() -> u32 { 512 }
fn default_audio_device() -> String { "Default".to_string() }
fn default_recording_format() -> RecordingFormat { RecordingFormat::Wav }
fn default_bit_depth() -> u16 { 24 }
fn default_fft_size() -> u32 { 2048 }
fn default_viz_buffer_size() -> u32 { 4096 }
fn default_true() -> bool { true }
fn default_auto_save_interval() -> u32 { 300 }
fn default_track_height() -> f32 { 160.0 }
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZoomMode {
Keyboard,
Scientific,
}
impl Default for ZoomMode {
fn default() -> Self {
ZoomMode::Keyboard
}
}

70
au-o2-gui/src/debug.rs Normal file
View File

@ -0,0 +1,70 @@
#[cfg(feature = "debug-log")]
pub mod enabled {
use std::fs::{self, File};
use std::io::{BufWriter, Write};
use std::sync::{Mutex, OnceLock};
static LOGGER: OnceLock<Mutex<BufWriter<Box<dyn Write + Send>>>> = OnceLock::new();
pub fn init() {
LOGGER.get_or_init(|| {
let home = dirs::home_dir().expect("no home directory");
let log_dir = home.join("audio-oxide");
let _ = fs::create_dir_all(&log_dir);
let log_path = log_dir.join("debug.log");
match File::create(&log_path) {
Ok(f) => {
eprintln!("[debug] logging to {}", log_path.display());
let writer: Box<dyn Write + Send> = Box::new(f);
let mut bw = BufWriter::new(writer);
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let _ = writeln!(bw, "--- session start: {} ---", now);
let _ = bw.flush();
Mutex::new(bw)
}
Err(e) => {
eprintln!("[debug] failed to open {}: {}, using stderr", log_path.display(), e);
let writer: Box<dyn Write + Send> = Box::new(std::io::stderr());
Mutex::new(BufWriter::new(writer))
}
}
});
}
pub fn write_log(file: &str, line: u32, args: std::fmt::Arguments<'_>) {
if let Some(logger) = LOGGER.get() {
if let Ok(mut w) = logger.lock() {
let now = chrono::Local::now().format("%H:%M:%S%.3f");
let _ = writeln!(w, "[DEBUG] {} {}:{} {}", now, file, line, args);
let _ = w.flush();
}
}
}
}
#[cfg(feature = "debug-log")]
pub fn init() {
enabled::init();
}
#[cfg(not(feature = "debug-log"))]
pub fn init() {}
pub fn is_debug_mode() -> bool {
cfg!(feature = "debug-log")
}
#[cfg(feature = "debug-log")]
#[macro_export]
macro_rules! debug_log {
($($arg:tt)*) => {
$crate::debug::enabled::write_log(file!(), line!(), format_args!($($arg)*))
};
}
#[cfg(not(feature = "debug-log"))]
#[macro_export]
macro_rules! debug_log {
($($arg:tt)*) => {()};
}

View File

@ -0,0 +1,150 @@
use super::{Editor, Message};
use crate::engine::EngineCommand;
impl Editor {
pub(crate) fn handle_automation(&mut self, message: Message) {
match message {
Message::SetTrackAutomationMode(track_index, mode) => {
if let Some(track) = self.tracks.get_mut(track_index) {
track.automation_mode = mode;
track.show_automation = mode != crate::automation::AutomationMode::Off;
if track.show_automation && track.automation_lanes.is_empty() {
use crate::automation::{AutomationLane, AutomationTarget};
track.automation_lanes.push(AutomationLane::new(AutomationTarget::Volume));
}
self.dirty = true;
let bus_name = track.bus_name.clone();
if let Some(ref engine) = self.engine {
let flag = match mode {
crate::automation::AutomationMode::Off => crate::engine::AutomationModeFlag::Off,
crate::automation::AutomationMode::Read => crate::engine::AutomationModeFlag::Read,
crate::automation::AutomationMode::Write => crate::engine::AutomationModeFlag::Write,
crate::automation::AutomationMode::Touch => crate::engine::AutomationModeFlag::Touch,
crate::automation::AutomationMode::Latch => crate::engine::AutomationModeFlag::Latch,
};
engine.send(EngineCommand::SetAutomationMode { bus_name, mode: flag });
}
}
}
Message::AddAutomationLane(track_index, target) => {
if let Some(track) = self.tracks.get_mut(track_index) {
use crate::automation::AutomationLane;
if !track.automation_lanes.iter().any(|l| l.target == target) {
track.automation_lanes.push(AutomationLane::new(target));
track.show_automation = true;
self.dirty = true;
}
}
}
Message::AddAutomationPoint { track_index, lane_index, sample_pos, value } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(lane) = track.automation_lanes.get_mut(lane_index) {
lane.insert_point(sample_pos, value);
self.dirty = true;
self.sync_automation_to_engine(track_index);
}
}
}
Message::RemoveAutomationPoint { track_index, lane_index, point_index } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(lane) = track.automation_lanes.get_mut(lane_index) {
lane.remove_point(point_index);
self.dirty = true;
self.sync_automation_to_engine(track_index);
}
}
}
Message::MoveAutomationPoint { track_index, lane_index, point_index, sample_pos, value } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(lane) = track.automation_lanes.get_mut(lane_index) {
if point_index < lane.points.len() {
lane.points.remove(point_index);
lane.insert_point(sample_pos, value);
self.dirty = true;
self.sync_automation_to_engine(track_index);
}
}
}
}
Message::AddTempoPoint { sample_pos, tempo } => {
self.tempo_map.insert_point(sample_pos, tempo);
self.dirty = true;
self.sync_tempo_to_engine();
}
Message::RemoveTempoPoint(index) => {
self.tempo_map.remove_point(index);
self.dirty = true;
self.sync_tempo_to_engine();
}
Message::MoveTempoPoint { index, sample_pos, tempo } => {
self.tempo_map.remove_point(index);
self.tempo_map.insert_point(sample_pos, tempo);
self.dirty = true;
self.sync_tempo_to_engine();
}
_ => {}
}
}
pub(crate) fn record_automation_point(&mut self, track_index: usize, target: crate::automation::AutomationTarget, sample_pos: u64, value: f32) {
let Some(track) = self.tracks.get_mut(track_index) else { return };
let lane_idx = if let Some(idx) = track.automation_lanes.iter().position(|l| l.target == target) {
idx
} else {
track.automation_lanes.push(crate::automation::AutomationLane::new(target));
track.automation_lanes.len() - 1
};
track.automation_lanes[lane_idx].insert_point(sample_pos, value);
self.sync_automation_to_engine(track_index);
}
pub(crate) fn sync_automation_to_engine(&self, track_index: usize) {
let Some(ref engine) = self.engine else { return };
let Some(track) = self.tracks.get(track_index) else { return };
let bus_name = &track.bus_name;
for lane in &track.automation_lanes {
let target = match &lane.target {
crate::automation::AutomationTarget::Volume => crate::engine::AutomationTarget::Volume,
crate::automation::AutomationTarget::Pan => crate::engine::AutomationTarget::Pan,
crate::automation::AutomationTarget::Mute => crate::engine::AutomationTarget::Mute,
crate::automation::AutomationTarget::ModuleParam { module_id, key } => {
crate::engine::AutomationTarget::ModuleParam { module_id: *module_id, key: key.clone() }
}
};
let points: Vec<(u64, f32)> = lane.points.iter().map(|p| (p.sample_pos, p.value)).collect();
engine.send(EngineCommand::SetAutomationData {
bus_name: bus_name.clone(),
target,
points,
});
}
}
pub(crate) fn sync_tempo_to_engine(&self) {
let Some(ref engine) = self.engine else { return };
let points: Vec<(u64, f32)> = self.tempo_map.points.iter()
.map(|p| (p.sample_pos, p.tempo))
.collect();
engine.send(EngineCommand::SetTempoCurve { points });
}
pub(crate) fn sync_midi_region_to_engine(&self, track: &crate::track::Track, region: &crate::region::Region) {
let Some(ref engine) = self.engine else { return };
let start_beat = region.start_time.to_total_beats(self.time_signature_numerator as u32);
let notes: Vec<oxforge::mdk::MidiPlaybackNote> = region.midi_notes.iter().map(|n| {
oxforge::mdk::MidiPlaybackNote {
start_tick: n.start_tick,
duration_ticks: n.duration_ticks,
note: n.note,
velocity: n.velocity,
channel: n.channel,
}
}).collect();
engine.send(EngineCommand::LoadMidiRegion {
bus_name: track.bus_name.clone(),
region_id: region.id,
start_beat,
notes,
});
}
}

View File

@ -0,0 +1,78 @@
use iced::Task;
use super::{decode_region_audio, Editor, Message};
use crate::engine::EngineCommand;
impl Editor {
pub(crate) fn handle_clip_launcher(&mut self, message: Message) -> Task<Message> {
match message {
Message::TriggerClip { track_index, region_id } => {
if let Some(track) = self.tracks.get(track_index) {
let track_clip_ids: Vec<uuid::Uuid> = track.regions.iter()
.filter(|r| self.active_clips.contains(&r.id))
.map(|r| r.id)
.collect();
for cid in track_clip_ids {
self.active_clips.remove(&cid);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: cid });
}
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
let s = (region.start_sample as usize).min(audio_l.len());
let e = (s + region.length_samples as usize).min(audio_l.len());
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: track.bus_name.clone(),
region_id,
start_sample: region.start_sample,
audio_l: audio_l[s..e].to_vec(),
audio_r: audio_r[s.min(audio_r.len())..e.min(audio_r.len())].to_vec(),
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
} else if region.is_midi() {
self.sync_midi_region_to_engine(track, region);
}
self.active_clips.insert(region_id);
}
}
}
Message::StopClip { region_id } => {
self.active_clips.remove(&region_id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id });
engine.send(EngineCommand::UnloadMidiRegion { region_id });
}
}
Message::TriggerScene(scene_index) => {
let trigger_list: Vec<(usize, uuid::Uuid)> = self.tracks.iter().enumerate()
.filter_map(|(ti, track)| {
track.regions.get(scene_index).map(|r| (ti, r.id))
})
.collect();
if let Some((ti, rid)) = trigger_list.into_iter().next() {
return self.update(Message::TriggerClip { track_index: ti, region_id: rid });
}
}
Message::StopAllClips => {
let clip_ids: Vec<uuid::Uuid> = self.active_clips.drain().collect();
if let Some(ref engine) = self.engine {
for rid in clip_ids {
engine.send(EngineCommand::UnloadRegionAudio { region_id: rid });
engine.send(EngineCommand::UnloadMidiRegion { region_id: rid });
}
}
}
_ => {}
}
Task::none()
}
}

View File

@ -0,0 +1,133 @@
use super::{decode_region_audio, Editor};
use crate::clipboard::ClipboardEntry;
use crate::engine::EngineCommand;
use crate::history::EditCommand;
use crate::region::Region;
use crate::timing::MusicalTime;
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn clipboard_copy(&mut self) {
self.clipboard.clear();
for (ti, track) in self.tracks.iter().enumerate() {
for region in &track.regions {
if region.selected {
self.clipboard.entries.push(ClipboardEntry {
region: region.clone(),
source_track_index: ti,
});
}
}
}
}
pub(crate) fn clipboard_cut(&mut self) {
let mut cut_entries = Vec::new();
for ti in 0..self.tracks.len() {
let mut ri = 0;
while ri < self.tracks[ti].regions.len() {
if self.tracks[ti].regions[ri].selected {
let region = self.tracks[ti].regions.remove(ri);
self.waveform_cache.remove(&region.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: region.id });
}
cut_entries.push((ti, region));
} else {
ri += 1;
}
}
}
if !cut_entries.is_empty() {
self.history.push(EditCommand::CutRegions { entries: cut_entries });
}
}
pub(crate) fn clipboard_paste(&mut self) {
if self.clipboard.is_empty() || self.tracks.is_empty() {
return;
}
let paste_time = self.current_position;
let paste_sample = paste_time.to_samples_mapped(
&self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let anchor_track = self.selected_track
.unwrap_or_else(|| {
self.clipboard.entries.first()
.map(|e| e.source_track_index)
.unwrap_or(0)
});
let source_anchor = self.clipboard.entries.first()
.map(|e| e.source_track_index)
.unwrap_or(0);
let earliest_sample = self.clipboard.entries.iter()
.map(|e| e.region.start_sample)
.min()
.unwrap_or(0);
let mut pasted = Vec::new();
for entry in &self.clipboard.entries {
let track_offset = entry.source_track_index as isize - source_anchor as isize;
let target_track = ((anchor_track as isize + track_offset).max(0) as usize)
.min(self.tracks.len().saturating_sub(1));
let sample_offset = entry.region.start_sample.saturating_sub(earliest_sample);
let new_start_sample = paste_sample + sample_offset;
let new_start_time = MusicalTime::from_samples_mapped(
new_start_sample,
&self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let new_id = uuid::Uuid::new_v4();
let new_region = Region {
id: new_id,
start_time: new_start_time,
duration: entry.region.duration,
audio_file: entry.region.audio_file.clone(),
start_sample: new_start_sample,
length_samples: entry.region.length_samples,
selected: false,
fade_in_samples: entry.region.fade_in_samples,
fade_out_samples: entry.region.fade_out_samples,
midi_notes: Vec::new(),
playback_rate: entry.region.playback_rate,
};
if let Some(ref audio_file) = new_region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
self.waveform_cache.insert(
new_id,
WaveformPeaks::from_stereo(&audio_l, &audio_r),
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[target_track].bus_name.clone(),
region_id: new_id,
start_sample: new_start_sample,
audio_l,
audio_r,
fade_in_samples: new_region.fade_in_samples,
fade_out_samples: new_region.fade_out_samples,
});
}
}
}
self.tracks[target_track].regions.push(new_region.clone());
pasted.push((target_track, new_region));
}
if !pasted.is_empty() {
self.history.push(EditCommand::PasteRegions { entries: pasted });
}
}
}

View File

@ -0,0 +1,77 @@
use iced::Task;
use super::{Editor, Message};
use crate::behaviors;
use crate::engine::EngineCommand;
use crate::history::EditCommand;
impl Editor {
pub(crate) fn handle_edit_actions(&mut self, message: Message) -> Task<Message> {
if let Message::EditAction(action) = message {
use behaviors::Action::*;
match action {
Undo => {
self.perform_undo();
self.mark_dirty();
}
Redo => {
self.perform_redo();
self.mark_dirty();
}
SelectAll => {
for t in &mut self.tracks {
t.selected = true;
for r in &mut t.regions {
r.selected = true;
}
}
}
Delete => {
self.delete_selected();
self.mark_dirty();
}
Duplicate => {
if let Some(i) = self.selected_track {
if let Some(track) = self.tracks.get(i) {
let mut dup = track.clone();
dup.id = uuid::Uuid::new_v4();
dup.name = format!("{} Copy", dup.name);
dup.bus_name = format!("track_{}", dup.id.as_simple());
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::CreateBus {
name: dup.bus_name.clone(),
is_midi: dup.track_type == crate::track::TrackType::Midi,
});
}
self.tracks.insert(i + 1, dup);
self.track_count += 1;
self.history.push(EditCommand::DuplicateTrack {
source_index: i,
new_index: i + 1,
});
self.mark_dirty();
}
}
}
Cut => {
self.clipboard_copy();
self.clipboard_cut();
self.mark_dirty();
}
Copy => {
self.clipboard_copy();
}
Paste => {
self.clipboard_paste();
self.mark_dirty();
}
Quantize => {
self.quantize_selected();
self.mark_dirty();
}
_ => {}
}
}
Task::none()
}
}

View File

@ -0,0 +1,215 @@
use std::time::Instant;
use iced::Task;
use super::{decode_region_audio, Editor, Message, StatusLevel};
use crate::engine::{EngineCommand, EngineEvent};
use crate::region::{Region, TakeFolder};
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn handle_engine_tick(&mut self) -> Task<Message> {
if let Some((ref msg, level, t)) = self.status_message {
if t.elapsed().as_secs() >= 5 {
self.last_status = Some((msg.clone(), level));
self.status_message = None;
}
}
if let Some(ref engine) = self.engine {
for event in engine.poll_events() {
match event {
EngineEvent::TransportPosition(pos) => {
self.current_position = pos;
let sample_pos = pos.to_samples_mapped(
&self.tempo_map, self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
for track in &mut self.tracks {
if track.automation_mode.reads() {
for lane in &track.automation_lanes {
if let Some(val) = lane.value_at(sample_pos) {
match &lane.target {
crate::automation::AutomationTarget::Volume => track.volume = val,
crate::automation::AutomationTarget::Pan => track.pan = val,
crate::automation::AutomationTarget::Mute => track.muted = val > 0.5,
crate::automation::AutomationTarget::ModuleParam { module_id, key } => {
self.module_params.values.insert((*module_id, key.clone()), val);
}
}
}
}
}
}
}
EngineEvent::Error(e) => {
debug_log!("engine error: {}", e);
self.status_message = Some((e, StatusLevel::Error, Instant::now()));
}
EngineEvent::BusCreated => {}
EngineEvent::GraphRebuilt => {}
EngineEvent::ModuleLoaded { bus_name, module_id, module_type, plugin_name, has_gui, gui_descriptor } => {
if self.routing.handle_module_loaded(&bus_name, module_id, module_type, plugin_name, &mut self.tracks) {
self.dirty = true;
}
self.module_gui.handle_module_loaded(module_id, has_gui, gui_descriptor);
}
EngineEvent::ContractViolation { module_id: _module_id, module_name: _module_name, avg_ns: _avg_ns, budget_ns: _budget_ns } => {
debug_log!("contract violation: module {}({}) {}ns / {}ns budget",
_module_name, _module_id, _avg_ns, _budget_ns);
}
EngineEvent::BufferAutoIncreased { new_size, latency_ms: _latency_ms, reason: _reason } => {
debug_log!("buffer auto-increased to {} ({:.1}ms): {}",
new_size, _latency_ms, _reason);
self.project_config.output_buffer_size = new_size as u32;
self.dirty = true;
}
EngineEvent::BufferNegotiation { module_id: _module_id, required_samples: _required_samples, required_ms: _required_ms, current_samples: _current_samples, current_ms: _current_ms } => {
debug_log!("buffer negotiation: module {} needs {} samples ({:.1}ms), current {} ({:.1}ms)",
_module_id, _required_samples, _required_ms, _current_samples, _current_ms);
}
EngineEvent::ModuleDisabled { module_id: _module_id, reason: _reason } => {
debug_log!("module {} disabled: {}", _module_id, _reason);
}
EngineEvent::AudioConfigResolved {
output_device: _output_device, input_device: _input_device, sample_rate: _sample_rate,
} => {
debug_log!("[audio] output='{}' input='{}' rate={}Hz",
_output_device,
if _input_device.is_empty() { "none" } else { &_input_device },
_sample_rate);
}
EngineEvent::RecordingComplete {
bus_name, file_path, start_sample,
length_samples, start_time, duration,
} => {
let region = Region::with_audio(
start_time,
duration,
file_path.clone(),
start_sample,
length_samples,
);
let region_id = region.id;
let abs_path = self.project_path.join(&file_path);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
self.waveform_cache.insert(
region_id,
WaveformPeaks::from_stereo(&audio_l, &audio_r),
);
engine.send(EngineCommand::LoadRegionAudio {
bus_name: bus_name.clone(),
region_id,
start_sample,
audio_l,
audio_r,
fade_in_samples: 0,
fade_out_samples: 0,
});
}
for track in &mut self.tracks {
if track.bus_name == bus_name {
track.regions.push(region);
self.dirty = true;
break;
}
}
}
EngineEvent::ModuleParamDescriptors { module_id, descriptors } => {
for desc in &descriptors {
let default_val = match &desc.kind {
oxforge::mdk::ParamKind::Float { default, .. } => *default,
oxforge::mdk::ParamKind::Bool { default } => if *default { 1.0 } else { 0.0 },
oxforge::mdk::ParamKind::Choice { default, .. } => *default as f32,
oxforge::mdk::ParamKind::Int { default, .. } => *default as f32,
};
self.module_params.values
.entry((module_id, desc.key.clone()))
.or_insert(default_val);
}
self.module_params.descriptors.insert(module_id, descriptors);
}
EngineEvent::ModuleParamChanged { module_id, key, value } => {
self.module_params.values.insert((module_id, key), value);
}
EngineEvent::PluginsDiscovered { plugins } => {
debug_log!("[plugins] discovered {} plugins", plugins.len());
self.discovered_plugins = plugins;
}
EngineEvent::TakeRecordingComplete { bus_name, takes } => {
let mut region_ids = Vec::new();
for (i, take) in takes.iter().enumerate() {
let region = Region::with_audio(
take.start_time,
take.duration,
take.file_path.clone(),
take.start_sample,
take.length_samples,
);
let region_id = region.id;
region_ids.push(region_id);
let abs_path = self.project_path.join(&take.file_path);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
self.waveform_cache.insert(
region_id,
WaveformPeaks::from_stereo(&audio_l, &audio_r),
);
if i == takes.len() - 1 {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: bus_name.clone(),
region_id,
start_sample: take.start_sample,
audio_l,
audio_r,
fade_in_samples: 0,
fade_out_samples: 0,
});
}
}
for track in &mut self.tracks {
if track.bus_name == bus_name {
track.regions.push(region);
break;
}
}
}
if region_ids.len() > 1 {
let folder = TakeFolder::new(region_ids);
for track in &mut self.tracks {
if track.bus_name == bus_name {
track.take_folders.push(folder);
self.dirty = true;
break;
}
}
}
}
EngineEvent::MeterUpdate { bus_peaks, master_peak } => {
for (name, l, r) in bus_peaks {
self.meter_levels.insert(name, (l, r));
}
self.master_meter = master_peak;
}
EngineEvent::ModuleGuiDescriptorReady { module_id, descriptor } => {
self.module_gui.handle_gui_descriptor_ready(module_id, descriptor);
}
EngineEvent::ModuleGuiReady => {}
EngineEvent::ModuleErrorReport { .. } => {}
}
}
}
let gui_tasks = self.module_gui.tick(
self.engine.as_ref(),
&mut self.module_params,
&self.routing.module_names,
);
if !gui_tasks.is_empty() {
return Task::batch(gui_tasks);
}
Task::none()
}
}

View File

@ -0,0 +1,144 @@
use iced::widget::{button, column, container, pick_list, row, text, text_input, checkbox, Space};
use iced::{Alignment, Background, Color, Element, Length, Theme};
use super::{Editor, Message, ModalState};
use crate::export::{self, ExportConfig, ExportFormat};
use crate::gui::theme as ui_theme;
impl Editor {
pub(crate) fn handle_export(&mut self, message: Message) {
match message {
Message::ShowExportDialog => {
self.modal_state = Some(ModalState::ExportDialog(ExportConfig {
sample_rate: self.project_config.sample_rate,
..ExportConfig::default()
}));
}
Message::ExportFormatSelected(fmt) => {
if let Some(ModalState::ExportDialog(ref mut cfg)) = self.modal_state {
cfg.format = fmt;
}
}
Message::ExportBitDepthSelected(bd) => {
if let Some(ModalState::ExportDialog(ref mut cfg)) = self.modal_state {
cfg.bit_depth = bd;
}
}
Message::ExportNormalizeToggled => {
if let Some(ModalState::ExportDialog(ref mut cfg)) = self.modal_state {
cfg.normalize = !cfg.normalize;
}
}
Message::ExportFilenameChanged(name) => {
if let Some(ModalState::ExportDialog(ref mut cfg)) = self.modal_state {
cfg.filename = name;
}
}
Message::ExportConfirm => {
if let Some(ModalState::ExportDialog(ref cfg)) = self.modal_state {
self.perform_export(cfg.clone());
}
self.modal_state = None;
}
_ => {}
}
}
pub(crate) fn perform_export(&self, config: ExportConfig) {
let bounced = export::bounce_offline(
&self.tracks,
&self.project_path,
self.tempo,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let (mut mix_l, mut mix_r) = match bounced {
Some(data) => data,
None => {
debug_log!("nothing to export");
return;
}
};
if config.normalize {
export::normalize(&mut mix_l, &mut mix_r);
}
let export_dir = self.project_path.join("exports");
let _ = std::fs::create_dir_all(&export_dir);
let filename = format!("{}.{}", config.filename, config.format.extension());
let path = export_dir.join(&filename);
let result = match config.format {
ExportFormat::Wav => export::export_wav(
&path, &mix_l, &mix_r, config.sample_rate, config.bit_depth,
),
ExportFormat::Flac => export::export_flac(
&path, &mix_l, &mix_r, config.sample_rate, config.bit_depth,
),
ExportFormat::Xtc => export::export_xtc(
&path, &mix_l, &mix_r, config.sample_rate, config.bit_depth,
self.analysis_fft_size as u32,
),
};
match result {
Ok(()) => debug_log!("[export] wrote {}", path.display()),
Err(_e) => debug_log!("[export] failed: {}", _e),
}
}
pub(crate) fn export_dialog_view(&self, config: &ExportConfig) -> Element<'_, Message> {
let format_picker = pick_list(
ExportFormat::ALL.as_slice(),
Some(config.format),
Message::ExportFormatSelected,
);
let bit_depths: Vec<u16> = vec![16, 24, 32];
let bit_depth_picker = pick_list(
bit_depths,
Some(config.bit_depth),
Message::ExportBitDepthSelected,
);
let normalize_check = checkbox("Normalize", config.normalize)
.on_toggle(|_| Message::ExportNormalizeToggled);
let filename_input = text_input("filename", &config.filename)
.on_input(Message::ExportFilenameChanged)
.width(200);
let export_btn = button(text("Export").size(ui_theme::TS_MD))
.on_press(Message::ExportConfirm);
let cancel_btn = button(text("Cancel").size(ui_theme::TS_MD))
.on_press(Message::CloseModal);
container(
column![
text("Export / Bounce").size(ui_theme::TS_XL),
Space::new(0, ui_theme::SP_LG),
row![text("Format:").size(ui_theme::TS_MD).width(80), format_picker].spacing(ui_theme::SP_MD).align_y(Alignment::Center),
row![text("Bit Depth:").size(ui_theme::TS_MD).width(80), bit_depth_picker].spacing(ui_theme::SP_MD).align_y(Alignment::Center),
row![text("Filename:").size(ui_theme::TS_MD).width(80), filename_input].spacing(ui_theme::SP_MD).align_y(Alignment::Center),
normalize_check,
Space::new(0, ui_theme::SP_LG),
row![cancel_btn, Space::new(Length::Fill, 0), export_btn].spacing(ui_theme::SP_MD),
]
.spacing(ui_theme::SP_MD)
.padding(ui_theme::SP_XXL)
.width(400),
)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(Color::from_rgb8(0x38, 0x31, 0x2A))),
border: iced::Border {
color: Color::from_rgb8(0x58, 0x4E, 0x44),
width: 1.0,
radius: 10.0.into(),
},
..container::Style::default()
})
.into()
}
}

View File

@ -0,0 +1,96 @@
use super::{Editor, Message};
use crate::engine::EngineCommand;
use crate::region::Region;
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn handle_freeze(&mut self, message: Message) {
if let Message::FreezeTrack(track_index) = message {
if let Some(track) = self.tracks.get(track_index) {
if track.frozen {
let module_ids: Vec<u32> = track.module_chain.clone();
for mid in &module_ids {
self.routing.disabled_modules.remove(mid);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetModuleDisabled {
module_id: *mid,
disabled: false,
});
}
}
if let Some(track) = self.tracks.get_mut(track_index) {
track.frozen = false;
track.frozen_file = None;
self.mark_dirty();
}
} else {
let bus_name = track.bus_name.clone();
if let Some((audio_l, audio_r, start_sample, length_samples)) =
crate::export::bounce_track(track, &self.project_path)
{
let audio_dir = self.project_path.join("audio");
let _ = std::fs::create_dir_all(&audio_dir);
let safe = bus_name.replace(|c: char| !c.is_alphanumeric() && c != '_', "_");
let filename = format!("{}_frozen.xtc", safe);
let file_path = audio_dir.join(&filename);
let relative_path = format!("audio/{}", filename);
let sr = self.project_config.sample_rate;
let encoder = crate::codec::XtcEncoder::new(sr, 24, 2048);
let imag_l = vec![0.0f32; audio_l.len()];
let imag_r = vec![0.0f32; audio_r.len()];
if encoder.encode_to_file(&file_path, &audio_l, &audio_r, &imag_l, &imag_r).is_ok() {
let start_time = crate::timing::MusicalTime::from_samples_mapped(
start_sample, &self.tempo_map, sr, self.time_signature_numerator as u32,
);
let end_time = crate::timing::MusicalTime::from_samples_mapped(
start_sample + length_samples, &self.tempo_map, sr, self.time_signature_numerator as u32,
);
let duration = end_time - start_time;
let frozen_region = Region::with_audio(
start_time, duration, relative_path.clone(), start_sample, length_samples,
);
let region_id = frozen_region.id;
self.waveform_cache.insert(
region_id,
WaveformPeaks::from_stereo(&audio_l, &audio_r),
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: bus_name.clone(),
region_id,
start_sample,
audio_l,
audio_r,
fade_in_samples: 0,
fade_out_samples: 0,
});
}
if let Some(track) = self.tracks.get_mut(track_index) {
track.regions.push(frozen_region);
track.frozen = true;
track.frozen_file = Some(relative_path);
let module_ids: Vec<u32> = track.module_chain.clone();
for mid in &module_ids {
self.routing.disabled_modules.insert(*mid);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetModuleDisabled {
module_id: *mid,
disabled: true,
});
}
}
self.mark_dirty();
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,88 @@
use super::{Editor, Message};
use crate::engine::EngineCommand;
impl Editor {
pub(crate) fn handle_groups(&mut self, message: Message) {
match message {
Message::CreateGroup => {
let idx = self.groups.len();
let group = crate::track::TrackGroup::new(
format!("Group {}", idx + 1),
idx + 6,
);
self.groups.push(group);
self.mark_dirty();
}
Message::DeleteGroup(group_id) => {
for track in &mut self.tracks {
if track.group_id == Some(group_id) {
track.group_id = None;
}
}
self.groups.retain(|g| g.id != group_id);
self.mark_dirty();
}
Message::SetGroupVolume { group_id, volume } => {
if let Some(group) = self.groups.iter_mut().find(|g| g.id == group_id) {
group.volume = volume;
}
self.sync_group_volumes(group_id);
self.mark_dirty();
}
Message::SetGroupMute(group_id) => {
if let Some(group) = self.groups.iter_mut().find(|g| g.id == group_id) {
group.muted = !group.muted;
}
self.sync_group_mutes(group_id);
self.mark_dirty();
}
Message::SetGroupSolo(group_id) => {
if let Some(group) = self.groups.iter_mut().find(|g| g.id == group_id) {
group.soloed = !group.soloed;
}
self.mark_dirty();
}
Message::AssignTrackToGroup { track_index, group_id } => {
if let Some(track) = self.tracks.get_mut(track_index) {
track.group_id = group_id;
self.mark_dirty();
}
}
_ => {}
}
}
pub(crate) fn sync_group_volumes(&self, group_id: uuid::Uuid) {
let group_vol = self.groups.iter()
.find(|g| g.id == group_id)
.map(|g| g.volume)
.unwrap_or(1.0);
if let Some(ref engine) = self.engine {
for track in &self.tracks {
if track.group_id == Some(group_id) {
engine.send(EngineCommand::SetBusVolume {
bus_name: track.bus_name.clone(),
volume: track.volume * group_vol,
});
}
}
}
}
pub(crate) fn sync_group_mutes(&self, group_id: uuid::Uuid) {
let group_muted = self.groups.iter()
.find(|g| g.id == group_id)
.map(|g| g.muted)
.unwrap_or(false);
if let Some(ref engine) = self.engine {
for track in &self.tracks {
if track.group_id == Some(group_id) {
engine.send(EngineCommand::SetBusMute {
bus_name: track.bus_name.clone(),
muted: track.muted || group_muted,
});
}
}
}
}
}

View File

@ -0,0 +1,20 @@
use std::path::Path;
pub(crate) fn decode_region_audio(path: &Path, project_rate: u32) -> Option<(Vec<f32>, Vec<f32>)> {
let decoder = crate::codec::XtcDecoder::open(path).ok()?;
let (audio_l, audio_r) = decoder.decode_real(path).ok()?;
if decoder.sample_rate != project_rate && decoder.sample_rate > 0 {
let l = crate::engine::resample::resample_mono(&audio_l, decoder.sample_rate, project_rate);
let r = crate::engine::resample::resample_mono(&audio_r, decoder.sample_rate, project_rate);
Some((l, r))
} else {
Some((audio_l, audio_r))
}
}
pub(crate) fn apply_flex(audio_l: Vec<f32>, audio_r: Vec<f32>, rate: f32) -> (Vec<f32>, Vec<f32>) {
if (rate - 1.0).abs() < 0.001 {
return (audio_l, audio_r);
}
crate::engine::resample::stretch_stereo(&audio_l, &audio_r, rate)
}

View File

@ -0,0 +1,258 @@
use std::path::PathBuf;
use iced::widget::scrollable;
use iced::Task;
use super::{
apply_flex, decode_region_audio, BottomPanelMode, Editor, Message, ModuleParamState,
Tool,
};
use crate::clipboard::Clipboard;
use crate::config::ProjectConfig;
use crate::engine::session_player::SessionPlayerConfig;
use crate::engine::{EngineCommand, EngineConfig, EngineHandle};
use crate::gui::editor::score;
use crate::gui::icons::IconSet;
use crate::gui::theme as ui_theme;
use crate::history::History;
use crate::module_gui_manager::ModuleGuiManager;
use crate::routing::RoutingManager;
use crate::timing::MusicalTime;
use crate::waveform::{WaveformCache, WaveformPeaks};
impl Editor {
pub fn new(project_path: PathBuf) -> (Self, Task<Message>) {
let config_path = project_path.join("project.toml");
let project_config: ProjectConfig = std::fs::read_to_string(&config_path)
.ok()
.and_then(|content| toml::from_str(&content).ok())
.unwrap_or_default();
let tempo = project_config.tempo;
let mut tempo_map = crate::timing::TempoMap::new(tempo);
for pt in &project_config.tempo_points {
tempo_map.insert_point(pt.sample_pos, pt.tempo);
}
let ts_num = project_config.time_signature_numerator;
let ts_den = project_config.time_signature_denominator;
let engine_config = EngineConfig {
sample_rate: project_config.sample_rate,
output_buffer_size: project_config.output_buffer_size,
input_buffer_size: project_config.input_buffer_size,
output_device: project_config.audio_device.clone(),
input_device: project_config.audio_input_device.clone(),
auto_oversample: project_config.auto_oversample,
auto_undersample: project_config.auto_undersample,
hilbert_fft_size: 2048,
};
let engine = Some(EngineHandle::spawn(&engine_config));
let tracks = project_config.tracks.clone();
let track_count = tracks.len();
let markers = project_config.markers.clone();
let groups = project_config.groups.clone();
let next_marker_id = markers.iter().map(|m| m.id).max().unwrap_or(0) + 1;
let mut waveform_cache = WaveformCache::new();
if let Some(ref engine_handle) = engine {
for track in &tracks {
engine_handle.send(EngineCommand::CreateBus {
name: track.bus_name.clone(),
is_midi: track.track_type == crate::track::TrackType::Midi,
});
engine_handle.send(EngineCommand::SetBusVolume {
bus_name: track.bus_name.clone(), volume: track.volume,
});
engine_handle.send(EngineCommand::SetBusPan {
bus_name: track.bus_name.clone(), pan: track.pan,
});
if track.muted {
engine_handle.send(EngineCommand::SetBusMute {
bus_name: track.bus_name.clone(), muted: true,
});
}
if track.soloed {
engine_handle.send(EngineCommand::SetBusSolo {
bus_name: track.bus_name.clone(), soloed: true,
});
}
if track.record_armed {
engine_handle.send(EngineCommand::ArmTrack {
bus_name: track.bus_name.clone(),
});
}
let visible_ids: std::collections::HashSet<uuid::Uuid> =
track.visible_regions().iter().map(|r| r.id).collect();
for region in &track.regions {
if let Some(ref audio_file) = region.audio_file {
let abs_path = project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, project_config.sample_rate) {
waveform_cache.insert(
region.id,
WaveformPeaks::from_stereo(&audio_l, &audio_r),
);
if visible_ids.contains(&region.id) {
let (sl, sr) = apply_flex(audio_l, audio_r, region.playback_rate);
engine_handle.send(EngineCommand::LoadRegionAudio {
bus_name: track.bus_name.clone(),
region_id: region.id,
start_sample: region.start_sample,
audio_l: sl,
audio_r: sr,
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
if !region.midi_notes.is_empty() && visible_ids.contains(&region.id) {
let start_beat = region.start_time.to_total_beats(project_config.time_signature_numerator as u32);
let notes: Vec<oxforge::mdk::MidiPlaybackNote> = region.midi_notes.iter().map(|n| {
oxforge::mdk::MidiPlaybackNote {
start_tick: n.start_tick,
duration_ticks: n.duration_ticks,
note: n.note,
velocity: n.velocity,
channel: n.channel,
}
}).collect();
engine_handle.send(EngineCommand::LoadMidiRegion {
bus_name: track.bus_name.clone(),
region_id: region.id,
start_beat,
notes,
});
}
}
for send in &track.sends {
if send.enabled {
engine_handle.send(EngineCommand::SetSend {
source_bus: track.bus_name.clone(),
aux_bus: send.aux_bus_name.clone(),
level: send.level,
});
}
}
{
let flag = match track.automation_mode {
crate::automation::AutomationMode::Off => crate::engine::AutomationModeFlag::Off,
crate::automation::AutomationMode::Read => crate::engine::AutomationModeFlag::Read,
crate::automation::AutomationMode::Write => crate::engine::AutomationModeFlag::Write,
crate::automation::AutomationMode::Touch => crate::engine::AutomationModeFlag::Touch,
crate::automation::AutomationMode::Latch => crate::engine::AutomationModeFlag::Latch,
};
engine_handle.send(EngineCommand::SetAutomationMode {
bus_name: track.bus_name.clone(),
mode: flag,
});
}
for lane in &track.automation_lanes {
let target = match &lane.target {
crate::automation::AutomationTarget::Volume => crate::engine::AutomationTarget::Volume,
crate::automation::AutomationTarget::Pan => crate::engine::AutomationTarget::Pan,
crate::automation::AutomationTarget::Mute => crate::engine::AutomationTarget::Mute,
crate::automation::AutomationTarget::ModuleParam { module_id, key } => {
crate::engine::AutomationTarget::ModuleParam { module_id: *module_id, key: key.clone() }
}
};
let points: Vec<(u64, f32)> = lane.points.iter().map(|p| (p.sample_pos, p.value)).collect();
engine_handle.send(EngineCommand::SetAutomationData {
bus_name: track.bus_name.clone(),
target,
points,
});
}
}
if !tempo_map.points.is_empty() {
let points: Vec<(u64, f32)> = tempo_map.points.iter()
.map(|p| (p.sample_pos, p.tempo))
.collect();
engine_handle.send(EngineCommand::SetTempoCurve { points });
}
engine_handle.send(EngineCommand::ScanPlugins);
}
(
Self {
project_path,
project_config,
tracks,
modal_state: None,
engine,
dirty: false,
transport: crate::engine::TransportState::Stopped,
record_armed: false,
current_position: MusicalTime::new(1, 1, 0),
tempo,
tempo_map,
show_tempo_lane: false,
time_signature_numerator: ts_num,
time_signature_denominator: ts_den,
active_tool: Tool::Pointer,
show_inspector: false,
show_bottom_panel: false,
bottom_panel_mode: BottomPanelMode::Editor,
header_width: 200.0,
inspector_width: ui_theme::INSPECTOR_WIDTH,
bottom_panel_height: 250.0,
resize_dragging: false,
resize_last_y: 0.0,
tracklist_resize_dragging: false,
tracklist_resize_last_x: 0.0,
inspector_resize_dragging: false,
inspector_resize_last_x: 0.0,
lcd_editing: false,
lcd_bar_input: String::new(),
lcd_beat_input: String::new(),
lcd_tick_input: String::new(),
track_list_scrollable_id: scrollable::Id::unique(),
timeline_scrollable_id: scrollable::Id::unique(),
scroll_offset_y: 0.0,
scroll_source: None,
master_volume: 1.0,
master_pan: 0.0,
cycle_enabled: false,
cycle_start_bar: 1,
cycle_end_bar: 5,
metronome_enabled: false,
count_in_enabled: false,
punch_enabled: false,
selected_track: None,
track_count,
h_zoom: 100.0,
v_zoom: 1.0,
routing: RoutingManager::new(),
analysis_fft_size: 2048,
waveform_cache,
clipboard: Clipboard::new(),
markers,
next_marker_id,
history: History::new(),
status_message: None,
last_status: None,
groups,
active_clips: std::collections::HashSet::new(),
score_note_duration: score::ScoreNoteDuration::Quarter,
icons: IconSet::load(),
meter_levels: std::collections::HashMap::new(),
master_meter: (0.0, 0.0),
session_player_config: SessionPlayerConfig::default(),
session_player_bars: 4,
discovered_plugins: Vec::new(),
spatial_mode: crate::engine::atmos::SpatialRenderMode::default(),
mono_lane: crate::engine::atmos::MonoLane::default(),
module_params: ModuleParamState::new(),
pattern_length: 16,
module_gui: ModuleGuiManager::new(),
inspector_signal_open: true,
inspector_sends_open: true,
inspector_automation_open: false,
inspector_spatial_open: false,
inspector_analysis_open: false,
show_network_view: false,
},
Task::none(),
)
}
}

View File

@ -0,0 +1,162 @@
use iced::Task;
use super::{Editor, Message};
use crate::gui::theme as ui_theme;
use crate::engine::EngineCommand;
use crate::timing::MusicalTime;
impl Editor {
pub(crate) fn handle_layout(&mut self, message: Message) -> Task<Message> {
match message {
Message::ToolSelected(tool) => self.active_tool = tool,
Message::ToggleInspector => self.show_inspector = !self.show_inspector,
Message::ToggleBottomPanel => self.show_bottom_panel = !self.show_bottom_panel,
Message::ToggleTempoLane => self.show_tempo_lane = !self.show_tempo_lane,
Message::SetBottomPanelMode(mode) => {
if self.bottom_panel_mode == mode && self.show_bottom_panel {
self.show_bottom_panel = false;
} else {
self.bottom_panel_mode = mode;
self.show_bottom_panel = true;
}
}
Message::ResizeHandlePressed => {
self.resize_dragging = true;
debug_log!("resize: drag start, height={:.0}", self.bottom_panel_height);
}
Message::ResizeHandleMoved(point) => {
if self.resize_dragging {
let delta = self.resize_last_y - point.y;
self.bottom_panel_height = (self.bottom_panel_height + delta)
.clamp(ui_theme::BOTTOM_PANEL_MIN, ui_theme::BOTTOM_PANEL_MAX);
}
self.resize_last_y = point.y;
}
Message::ResizeHandleReleased => {
self.resize_dragging = false;
debug_log!("resize: drag end, height={:.0}", self.bottom_panel_height);
}
Message::TrackListResizePressed => {
self.tracklist_resize_dragging = true;
}
Message::TrackListResizeMoved(point) => {
if self.tracklist_resize_dragging {
let delta = point.x - self.tracklist_resize_last_x;
self.header_width = (self.header_width + delta)
.clamp(ui_theme::TRACKLIST_WIDTH_MIN, ui_theme::TRACKLIST_WIDTH_MAX);
}
self.tracklist_resize_last_x = point.x;
}
Message::TrackListResizeReleased => {
self.tracklist_resize_dragging = false;
}
Message::InspectorResizePressed => {
self.inspector_resize_dragging = true;
}
Message::InspectorResizeMoved(point) => {
if self.inspector_resize_dragging {
let delta = point.x - self.inspector_resize_last_x;
self.inspector_width = (self.inspector_width + delta)
.clamp(ui_theme::INSPECTOR_WIDTH_MIN, ui_theme::INSPECTOR_WIDTH_MAX);
}
self.inspector_resize_last_x = point.x;
}
Message::InspectorResizeReleased => {
self.inspector_resize_dragging = false;
}
Message::EscapePressed => {
if self.lcd_editing {
self.lcd_editing = false;
} else {
self.modal_state = None;
}
}
Message::CloseModal => {
self.modal_state = None;
}
Message::LcdClicked => {
self.lcd_editing = true;
self.lcd_bar_input = self.current_position.bar.to_string();
self.lcd_beat_input = self.current_position.beat.to_string();
self.lcd_tick_input = self.current_position.tick.to_string();
}
Message::LcdBarChanged(s) => { self.lcd_bar_input = s; }
Message::LcdBeatChanged(s) => { self.lcd_beat_input = s; }
Message::LcdTickChanged(s) => { self.lcd_tick_input = s; }
Message::LcdConfirm => {
let bar = self.lcd_bar_input.parse::<u32>().unwrap_or(self.current_position.bar).max(1);
let beat = self.lcd_beat_input.parse::<u32>().unwrap_or(self.current_position.beat).clamp(1, self.time_signature_numerator as u32);
let tick = self.lcd_tick_input.parse::<u32>().unwrap_or(self.current_position.tick).min(959);
let pos = MusicalTime::new(bar, beat, tick);
self.current_position = pos;
let sample_pos = pos.to_samples_mapped(
&self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::Seek { sample_pos });
}
self.lcd_editing = false;
}
Message::ZoomToFit => {
if !self.tracks.is_empty() {
let max_sample = self.tracks.iter()
.flat_map(|t| t.regions.iter())
.map(|r| r.start_sample + r.length_samples)
.max()
.unwrap_or(self.project_config.sample_rate as u64 * 60);
let total_beats = max_sample as f32 / (self.project_config.sample_rate as f32 * 60.0 / self.tempo);
let target_width = 1200.0;
self.h_zoom = (target_width / total_beats.max(1.0)).clamp(10.0, 1000.0);
}
}
Message::ZoomH(factor) => {
self.h_zoom = (self.h_zoom * factor).clamp(10.0, 1000.0);
}
Message::ZoomV(factor) => {
self.v_zoom = (self.v_zoom * factor).clamp(0.3, 5.0);
}
_ => {}
}
Task::none()
}
pub(crate) fn handle_scroll(&mut self, message: Message) -> Task<Message> {
use iced::widget::scrollable;
match message {
Message::TrackListScrolled(viewport) => {
if self.scroll_source == Some(super::ScrollSource::Timeline) {
self.scroll_source = None;
return Task::none();
}
self.scroll_source = Some(super::ScrollSource::TrackList);
self.scroll_offset_y = viewport.absolute_offset().y;
return scrollable::scroll_to(
self.timeline_scrollable_id.clone(),
scrollable::AbsoluteOffset {
x: 0.0,
y: self.scroll_offset_y,
},
);
}
Message::TimelineScrolled(viewport) => {
if self.scroll_source == Some(super::ScrollSource::TrackList) {
self.scroll_source = None;
return Task::none();
}
self.scroll_source = Some(super::ScrollSource::Timeline);
self.scroll_offset_y = viewport.absolute_offset().y;
return scrollable::scroll_to(
self.track_list_scrollable_id.clone(),
scrollable::AbsoluteOffset {
x: 0.0,
y: self.scroll_offset_y,
},
);
}
_ => {}
}
Task::none()
}
}

View File

@ -0,0 +1,36 @@
use super::{Editor, Message};
use crate::engine::EngineCommand;
impl Editor {
pub(crate) fn handle_markers(&mut self, message: Message) {
match message {
Message::AddMarker(position) => {
let id = self.next_marker_id;
self.next_marker_id += 1;
let name = format!("Marker {}", id);
self.markers.push(crate::timing::Marker { id, name, position });
self.markers.sort_by(|a, b| a.position.cmp(&b.position));
self.mark_dirty();
}
Message::DeleteMarker(id) => {
self.markers.retain(|m| m.id != id);
self.mark_dirty();
}
Message::JumpToMarker(id) => {
if let Some(marker) = self.markers.iter().find(|m| m.id == id) {
let pos = marker.position;
self.current_position = pos;
let sample_pos = pos.to_samples_mapped(
&self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::Seek { sample_pos });
}
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,292 @@
use super::{decode_region_audio, Editor, Message};
use crate::engine::EngineCommand;
use crate::region::Region;
use crate::timing::MusicalTime;
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn handle_midi(&mut self, message: Message) {
match message {
Message::AddMidiNote { track_index, region_id, note } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
region.midi_notes.push(note);
self.mark_dirty();
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
self.sync_midi_region_to_engine(track, region);
}
}
}
Message::RemoveMidiNote { track_index, region_id, note_index } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
if note_index < region.midi_notes.len() {
region.midi_notes.remove(note_index);
self.mark_dirty();
}
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
self.sync_midi_region_to_engine(track, region);
}
}
}
Message::MoveMidiNote { track_index, region_id, note_index, start_tick, note } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
if let Some(mn) = region.midi_notes.get_mut(note_index) {
mn.start_tick = start_tick;
mn.note = note;
self.dirty = true;
}
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
self.sync_midi_region_to_engine(track, region);
}
}
}
Message::SetNoteVelocity { track_index, region_id, note_index, velocity } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
if let Some(mn) = region.midi_notes.get_mut(note_index) {
mn.velocity = velocity;
self.dirty = true;
}
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
self.sync_midi_region_to_engine(track, region);
}
}
}
Message::ResizeMidiNote { track_index, region_id, note_index, duration_ticks } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
if let Some(mn) = region.midi_notes.get_mut(note_index) {
mn.duration_ticks = duration_ticks;
self.dirty = true;
}
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
self.sync_midi_region_to_engine(track, region);
}
}
}
Message::SetPatternLength(len) => {
self.pattern_length = len;
}
Message::SetScoreNoteDuration(dur) => {
self.score_note_duration = dur;
}
Message::QuantizeMidiNotes { track_index, region_id, grid_ticks } => {
if grid_ticks > 0 {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
for mn in &mut region.midi_notes {
let half = grid_ticks / 2;
mn.start_tick = ((mn.start_tick + half) / grid_ticks) * grid_ticks;
}
self.mark_dirty();
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
self.sync_midi_region_to_engine(track, region);
}
}
}
}
_ => {}
}
}
pub(crate) fn quantize_selected(&mut self) {
use crate::timing::TICKS_PER_BEAT;
let grid_ticks = TICKS_PER_BEAT as u64 / 4;
let sample_rate = self.project_config.sample_rate;
let tempo = self.tempo;
let bpb = self.time_signature_numerator as u32;
let mut targets: Vec<(usize, uuid::Uuid, bool)> = Vec::new();
for (ti, track) in self.tracks.iter().enumerate() {
for region in &track.regions {
if region.selected {
targets.push((ti, region.id, region.is_midi()));
}
}
}
for (track_index, region_id, is_midi) in targets {
if is_midi {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
let half = grid_ticks / 2;
for mn in &mut region.midi_notes {
mn.start_tick = ((mn.start_tick + half) / grid_ticks) * grid_ticks;
}
}
}
if let Some(track) = self.tracks.get(track_index) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
self.sync_midi_region_to_engine(track, region);
}
}
} else {
self.audio_quantize_region(track_index, region_id, grid_ticks, sample_rate, tempo, bpb);
}
}
}
pub(crate) fn audio_quantize_region(
&mut self,
track_index: usize,
region_id: uuid::Uuid,
grid_ticks: u64,
sample_rate: u32,
tempo: f32,
beats_per_bar: u32,
) {
let track = match self.tracks.get(track_index) {
Some(t) => t,
None => return,
};
let region = match track.regions.iter().find(|r| r.id == region_id) {
Some(r) => r,
None => return,
};
let audio_file = match &region.audio_file {
Some(f) => f.clone(),
None => return,
};
let abs_path = self.project_path.join(&audio_file);
let (audio_l, audio_r) = match decode_region_audio(&abs_path, sample_rate) {
Some(a) => a,
None => return,
};
let region_start = region.start_sample as usize;
let region_end = (region_start + region.length_samples as usize).min(audio_l.len());
if region_end <= region_start {
return;
}
let slice_l = &audio_l[region_start..region_end];
let slice_r = &audio_r[region_start.min(audio_r.len())..region_end.min(audio_r.len())];
let onsets = crate::engine::onset::detect_onsets(slice_l, slice_r, sample_rate);
if onsets.is_empty() {
return;
}
let original_region = region.clone();
let beats_per_second = tempo as f64 / 60.0;
let samples_per_beat = sample_rate as f64 / beats_per_second;
let samples_per_tick = samples_per_beat / crate::timing::TICKS_PER_BEAT as f64;
let grid_samples = (grid_ticks as f64 * samples_per_tick) as u64;
let mut split_points: Vec<u64> = onsets
.iter()
.map(|&o| region.start_sample + o as u64)
.filter(|&s| s > region.start_sample && s < region.start_sample + region.length_samples)
.collect();
split_points.sort();
split_points.dedup();
let mut boundaries = vec![region.start_sample];
boundaries.extend_from_slice(&split_points);
boundaries.push(region.start_sample + region.length_samples);
let mut new_regions: Vec<Region> = Vec::new();
for i in 0..boundaries.len() - 1 {
let seg_start = boundaries[i];
let seg_end = boundaries[i + 1];
let seg_len = seg_end - seg_start;
if seg_len == 0 {
continue;
}
let snapped = if grid_samples > 0 {
let half = grid_samples / 2;
((seg_start + half) / grid_samples) * grid_samples
} else {
seg_start
};
let snapped_time = MusicalTime::from_samples_mapped(snapped, &self.tempo_map, sample_rate, beats_per_bar);
let dur_time = MusicalTime::from_samples_mapped(seg_len, &self.tempo_map, sample_rate, beats_per_bar);
let new_region = Region {
id: uuid::Uuid::new_v4(),
start_time: snapped_time,
duration: dur_time,
audio_file: Some(audio_file.clone()),
start_sample: seg_start,
length_samples: seg_len,
selected: false,
fade_in_samples: if i == 0 { original_region.fade_in_samples } else { 0 },
fade_out_samples: if i == boundaries.len() - 2 { original_region.fade_out_samples } else { 0 },
midi_notes: Vec::new(),
playback_rate: original_region.playback_rate,
};
let s = seg_start as usize;
let e = seg_end as usize;
if s < audio_l.len() && e <= audio_l.len() {
let sl = &audio_l[s..e];
let sr = &audio_r[s.min(audio_r.len())..e.min(audio_r.len())];
self.waveform_cache.insert(new_region.id, WaveformPeaks::from_stereo(sl, sr));
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[track_index].bus_name.clone(),
region_id: new_region.id,
start_sample: snapped,
audio_l: sl.to_vec(),
audio_r: sr.to_vec(),
fade_in_samples: new_region.fade_in_samples,
fade_out_samples: new_region.fade_out_samples,
});
}
}
new_regions.push(new_region);
}
if new_regions.is_empty() {
return;
}
if let Some(track) = self.tracks.get_mut(track_index) {
track.regions.retain(|r| r.id != region_id);
}
self.waveform_cache.remove(&region_id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id });
}
if let Some(track) = self.tracks.get_mut(track_index) {
for r in &new_regions {
track.regions.push(r.clone());
}
}
self.history.push(crate::history::EditCommand::AudioQuantize {
track_index,
original_region,
result_regions: new_regions,
});
}
}

675
au-o2-gui/src/editor/mod.rs Normal file
View File

@ -0,0 +1,675 @@
mod transport;
mod markers;
mod layout;
mod tracks;
mod edit_actions;
mod timeline_events;
mod modules;
mod module_gui;
mod export;
mod sends;
mod automation;
mod midi;
mod regions;
mod takes;
mod freeze;
mod groups;
mod spatial;
mod stems;
mod clip_launcher;
mod session_player;
mod engine_tick;
mod undo;
mod redo;
mod clipboard;
mod tempo_detect;
mod helpers;
mod session;
mod init;
mod view;
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Instant;
pub(crate) use helpers::{decode_region_audio, apply_flex};
use crate::modules::plugin_host::PluginInfo;
use iced::widget::scrollable;
use iced::window;
use iced::{Element, Task};
use crate::behaviors;
use crate::clipboard::Clipboard;
use crate::config::ProjectConfig;
use crate::engine::session_player::{PlayerStyle, ScaleType, SessionPlayerConfig};
use crate::engine::atmos::ObjectPosition;
use crate::engine::{EngineCommand, EngineHandle, TransportState};
use crate::export::{ExportConfig, ExportFormat};
use crate::gui::editor::{new_track_wizard, score, timeline, track_header};
use crate::gui::icons::IconSet;
use crate::module_gui_manager::ModuleGuiManager;
use crate::routing::RoutingManager;
use crate::history::History;
use crate::timing::MusicalTime;
use crate::track::Track;
use crate::waveform::WaveformCache;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Tool {
Pointer,
Eraser,
Scissors,
Zoom,
}
impl Tool {
pub const ALL: [Tool; 4] = [
Tool::Pointer,
Tool::Eraser,
Tool::Scissors,
Tool::Zoom,
];
pub fn hint(&self) -> &'static str {
match self {
Tool::Pointer => "Pointer",
Tool::Eraser => "Eraser",
Tool::Scissors => "Scissors",
Tool::Zoom => "Zoom",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BottomPanelMode {
Editor,
Mixer,
StepSequencer,
ScoreEditor,
ClipLauncher,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatusLevel {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScrollSource {
TrackList,
Timeline,
}
#[derive(Debug, Clone)]
pub enum ModalState {
NewTrackWizard(new_track_wizard::State),
ExportDialog(ExportConfig),
}
pub struct ModuleParamState {
pub expanded: Option<u32>,
pub descriptors: HashMap<u32, Vec<oxforge::mdk::ParameterDescriptor>>,
pub values: HashMap<(u32, String), f32>,
}
impl ModuleParamState {
pub fn new() -> Self {
Self {
expanded: None,
descriptors: HashMap::new(),
values: HashMap::new(),
}
}
pub fn remove_module(&mut self, module_id: u32) {
self.descriptors.remove(&module_id);
self.values.retain(|(id, _), _| *id != module_id);
if self.expanded == Some(module_id) {
self.expanded = None;
}
}
}
pub struct Editor {
pub(crate) project_path: PathBuf,
pub(crate) project_config: ProjectConfig,
pub(crate) tracks: Vec<Track>,
pub(crate) modal_state: Option<ModalState>,
pub(crate) engine: Option<EngineHandle>,
pub(crate) dirty: bool,
pub(crate) transport: TransportState,
pub(crate) record_armed: bool,
pub(crate) current_position: MusicalTime,
pub(crate) tempo: f32,
pub(crate) tempo_map: crate::timing::TempoMap,
pub(crate) show_tempo_lane: bool,
pub(crate) time_signature_numerator: u8,
pub(crate) time_signature_denominator: u8,
pub(crate) active_tool: Tool,
pub(crate) show_inspector: bool,
pub(crate) show_bottom_panel: bool,
pub(crate) bottom_panel_mode: BottomPanelMode,
pub(crate) header_width: f32,
pub(crate) inspector_width: f32,
pub(crate) bottom_panel_height: f32,
pub(crate) resize_dragging: bool,
pub(crate) resize_last_y: f32,
pub(crate) tracklist_resize_dragging: bool,
pub(crate) tracklist_resize_last_x: f32,
pub(crate) inspector_resize_dragging: bool,
pub(crate) inspector_resize_last_x: f32,
pub(crate) lcd_editing: bool,
pub(crate) lcd_bar_input: String,
pub(crate) lcd_beat_input: String,
pub(crate) lcd_tick_input: String,
pub(crate) track_list_scrollable_id: scrollable::Id,
pub(crate) timeline_scrollable_id: scrollable::Id,
pub(crate) scroll_offset_y: f32,
pub(crate) scroll_source: Option<ScrollSource>,
pub(crate) cycle_enabled: bool,
pub(crate) cycle_start_bar: u32,
pub(crate) cycle_end_bar: u32,
pub(crate) metronome_enabled: bool,
pub(crate) count_in_enabled: bool,
pub(crate) punch_enabled: bool,
pub(crate) master_volume: f32,
pub(crate) master_pan: f32,
pub(crate) selected_track: Option<usize>,
pub(crate) track_count: usize,
pub(crate) h_zoom: f32,
pub(crate) v_zoom: f32,
pub(crate) routing: RoutingManager,
pub(crate) analysis_fft_size: usize,
pub(crate) waveform_cache: WaveformCache,
pub(crate) clipboard: Clipboard,
pub(crate) markers: Vec<crate::timing::Marker>,
pub(crate) next_marker_id: u32,
pub(crate) history: History,
pub(crate) status_message: Option<(String, StatusLevel, Instant)>,
pub(crate) last_status: Option<(String, StatusLevel)>,
pub(crate) groups: Vec<crate::track::TrackGroup>,
pub(crate) active_clips: std::collections::HashSet<uuid::Uuid>,
pub(crate) score_note_duration: score::ScoreNoteDuration,
pub(crate) icons: IconSet,
pub(crate) meter_levels: HashMap<String, (f32, f32)>,
pub(crate) master_meter: (f32, f32),
pub(crate) session_player_config: SessionPlayerConfig,
pub(crate) session_player_bars: u32,
pub(crate) discovered_plugins: Vec<PluginInfo>,
pub(crate) spatial_mode: crate::engine::atmos::SpatialRenderMode,
pub(crate) mono_lane: crate::engine::atmos::MonoLane,
pub module_params: ModuleParamState,
pub(crate) pattern_length: usize,
pub(crate) module_gui: ModuleGuiManager,
pub inspector_signal_open: bool,
pub inspector_sends_open: bool,
pub inspector_automation_open: bool,
pub inspector_spatial_open: bool,
pub inspector_analysis_open: bool,
pub show_network_view: bool,
}
#[derive(Debug, Clone)]
pub enum Message {
PlayPressed,
StopPressed,
RecordPressed,
RewindPressed,
CycleToggled,
MetronomeToggled,
CountInToggled,
PunchToggled,
AddMarker(MusicalTime),
DeleteMarker(u32),
JumpToMarker(u32),
MasterVolumeChanged(f32),
MasterPanChanged(f32),
ToolSelected(Tool),
ToggleInspector,
ToggleBottomPanel,
ToggleTempoLane,
SetBottomPanelMode(BottomPanelMode),
ResizeHandlePressed,
ResizeHandleMoved(iced::Point),
ResizeHandleReleased,
TrackListResizePressed,
TrackListResizeMoved(iced::Point),
TrackListResizeReleased,
InspectorResizePressed,
InspectorResizeMoved(iced::Point),
InspectorResizeReleased,
ShowNewTrackWizard,
NewTrackWizard(new_track_wizard::Message),
TrackHeader(usize, track_header::Message),
Timeline(timeline::Message),
TrackListScrolled(scrollable::Viewport),
TimelineScrolled(scrollable::Viewport),
EscapePressed,
CloseModal,
EditAction(behaviors::Action),
ZoomH(f32),
ZoomV(f32),
AddModuleToTrack(usize, String),
RemoveModuleFromTrack(usize, u32),
ToggleModuleDisabled(usize, u32),
MoveModuleUp(usize, u32),
MoveModuleDown(usize, u32),
ShowModulePicker(usize),
HideModulePicker,
LoadPluginOnTrack(usize, PathBuf),
SetAnalysisFftSize(usize),
ShowExportDialog,
ExportFormatSelected(ExportFormat),
ExportBitDepthSelected(u16),
ExportNormalizeToggled,
ExportFilenameChanged(String),
ExportConfirm,
ShowSendPicker(usize),
HideSendPicker,
AddSend { track_index: usize, aux_bus_name: String },
RemoveSend { track_index: usize, send_index: usize },
SetSendLevel { track_index: usize, send_index: usize, level: f32 },
SetTrackAutomationMode(usize, crate::automation::AutomationMode),
AddAutomationLane(usize, crate::automation::AutomationTarget),
AddAutomationPoint { track_index: usize, lane_index: usize, sample_pos: u64, value: f32 },
RemoveAutomationPoint { track_index: usize, lane_index: usize, point_index: usize },
MoveAutomationPoint { track_index: usize, lane_index: usize, point_index: usize, sample_pos: u64, value: f32 },
AddTempoPoint { sample_pos: u64, tempo: f32 },
RemoveTempoPoint(usize),
MoveTempoPoint { index: usize, sample_pos: u64, tempo: f32 },
AddMidiNote { track_index: usize, region_id: uuid::Uuid, note: crate::region::MidiNote },
RemoveMidiNote { track_index: usize, region_id: uuid::Uuid, note_index: usize },
MoveMidiNote { track_index: usize, region_id: uuid::Uuid, note_index: usize, start_tick: u64, note: u8 },
SetNoteVelocity { track_index: usize, region_id: uuid::Uuid, note_index: usize, velocity: u8 },
ResizeMidiNote { track_index: usize, region_id: uuid::Uuid, note_index: usize, duration_ticks: u64 },
SetPatternLength(usize),
SetScoreNoteDuration(score::ScoreNoteDuration),
QuantizeMidiNotes { track_index: usize, region_id: uuid::Uuid, grid_ticks: u64 },
SetRegionPlaybackRate { track_index: usize, region_id: uuid::Uuid, rate: f32 },
SetActiveTake { track_index: usize, folder_id: uuid::Uuid, take_index: usize },
DeleteTake { track_index: usize, folder_id: uuid::Uuid, take_index: usize },
FreezeTrack(usize),
CreateGroup,
DeleteGroup(uuid::Uuid),
SetGroupVolume { group_id: uuid::Uuid, volume: f32 },
SetGroupMute(uuid::Uuid),
SetGroupSolo(uuid::Uuid),
AssignTrackToGroup { track_index: usize, group_id: Option<uuid::Uuid> },
DetectTempo,
DetectTempoCurve,
SetSpatialMode(crate::engine::atmos::SpatialRenderMode),
SetMonoLane(crate::engine::atmos::MonoLane),
SetTrackSpatialPosition { track_index: usize, x: f32, y: f32 },
SetTrackSpatialElevation { track_index: usize, z: f32 },
SetTrackObjectSize { track_index: usize, size: f32 },
SplitStems { track_index: usize, region_id: uuid::Uuid },
TriggerClip { track_index: usize, region_id: uuid::Uuid },
StopClip { region_id: uuid::Uuid },
TriggerScene(usize),
StopAllClips,
SetSessionPlayerStyle(PlayerStyle),
SetSessionPlayerRoot(u8),
SetSessionPlayerScale(ScaleType),
SetSessionPlayerDensity(f32),
SetSessionPlayerSwing(f32),
SetSessionPlayerBars(u32),
GenerateSessionPattern,
ExpandModule(u32),
CollapseModule,
SetModuleParam { module_id: u32, key: String, value: f32 },
SelectTrackAndExpandModule { track_index: usize, module_id: u32 },
OpenModuleGui(u32),
CloseModuleGui(u32),
FramebufferMouseDown(u32),
FramebufferMouseUp(u32),
FramebufferResize(u32, u32, u32),
ToggleInspectorSignal,
ToggleInspectorSends,
ToggleInspectorAutomation,
ToggleInspectorSpatial,
ToggleInspectorAnalysis,
ToggleNetworkView,
CreatePortConnection { from_module: u32, from_port: u32, to_module: u32, to_port: u32 },
DeletePortConnection { from_module: u32, from_port: u32, to_module: u32, to_port: u32 },
SetTrackColor(usize, crate::track::TrackColor),
SetMonitorMode(usize, crate::track::MonitorMode),
LcdClicked,
LcdBarChanged(String),
LcdBeatChanged(String),
LcdTickChanged(String),
LcdConfirm,
ZoomToFit,
EngineTick,
}
impl Editor {
pub fn update(&mut self, message: Message) -> Task<Message> {
match message {
// Transport
Message::PlayPressed
| Message::StopPressed
| Message::RecordPressed
| Message::RewindPressed
| Message::CycleToggled
| Message::MetronomeToggled
| Message::CountInToggled
| Message::PunchToggled
| Message::MasterVolumeChanged(_)
| Message::MasterPanChanged(_) => return self.handle_transport(message),
// Markers
Message::AddMarker(_)
| Message::DeleteMarker(_)
| Message::JumpToMarker(_) => self.handle_markers(message),
// Layout / panels
Message::ToolSelected(_)
| Message::ToggleInspector
| Message::ToggleBottomPanel
| Message::ToggleTempoLane
| Message::SetBottomPanelMode(_)
| Message::ResizeHandlePressed
| Message::ResizeHandleMoved(_)
| Message::ResizeHandleReleased
| Message::TrackListResizePressed
| Message::TrackListResizeMoved(_)
| Message::TrackListResizeReleased
| Message::InspectorResizePressed
| Message::InspectorResizeMoved(_)
| Message::InspectorResizeReleased
| Message::EscapePressed
| Message::CloseModal
| Message::LcdClicked
| Message::LcdBarChanged(_)
| Message::LcdBeatChanged(_)
| Message::LcdTickChanged(_)
| Message::LcdConfirm
| Message::ZoomToFit
| Message::ZoomH(_)
| Message::ZoomV(_) => return self.handle_layout(message),
// Scroll sync
Message::TrackListScrolled(_)
| Message::TimelineScrolled(_) => return self.handle_scroll(message),
// Track management
Message::ShowNewTrackWizard
| Message::NewTrackWizard(_)
| Message::TrackHeader(_, _)
| Message::SetTrackColor(_, _)
| Message::SetMonitorMode(_, _) => return self.handle_tracks(message),
// Edit actions
Message::EditAction(_) => return self.handle_edit_actions(message),
// Timeline events
Message::Timeline(_) => return self.handle_timeline(message),
// Modules
Message::AddModuleToTrack(_, _)
| Message::RemoveModuleFromTrack(_, _)
| Message::ToggleModuleDisabled(_, _)
| Message::MoveModuleUp(_, _)
| Message::MoveModuleDown(_, _)
| Message::ShowModulePicker(_)
| Message::HideModulePicker
| Message::LoadPluginOnTrack(_, _)
| Message::SetAnalysisFftSize(_)
| Message::ExpandModule(_)
| Message::CollapseModule
| Message::SetModuleParam { .. }
| Message::SelectTrackAndExpandModule { .. } => return self.handle_modules(message),
// Module GUI
Message::OpenModuleGui(_)
| Message::CloseModuleGui(_)
| Message::FramebufferMouseDown(_)
| Message::FramebufferMouseUp(_)
| Message::FramebufferResize(_, _, _) => return self.handle_module_gui(message),
// Export
Message::ShowExportDialog
| Message::ExportFormatSelected(_)
| Message::ExportBitDepthSelected(_)
| Message::ExportNormalizeToggled
| Message::ExportFilenameChanged(_)
| Message::ExportConfirm => self.handle_export(message),
// Sends
Message::ShowSendPicker(_)
| Message::HideSendPicker
| Message::AddSend { .. }
| Message::RemoveSend { .. }
| Message::SetSendLevel { .. } => self.handle_sends(message),
// Automation
Message::SetTrackAutomationMode(_, _)
| Message::AddAutomationLane(_, _)
| Message::AddAutomationPoint { .. }
| Message::RemoveAutomationPoint { .. }
| Message::MoveAutomationPoint { .. }
| Message::AddTempoPoint { .. }
| Message::RemoveTempoPoint(_)
| Message::MoveTempoPoint { .. } => self.handle_automation(message),
// MIDI
Message::AddMidiNote { .. }
| Message::RemoveMidiNote { .. }
| Message::MoveMidiNote { .. }
| Message::SetNoteVelocity { .. }
| Message::ResizeMidiNote { .. }
| Message::SetPatternLength(_)
| Message::SetScoreNoteDuration(_)
| Message::QuantizeMidiNotes { .. } => self.handle_midi(message),
// Regions / flex
Message::SetRegionPlaybackRate { .. } => self.handle_regions(message),
// Takes
Message::SetActiveTake { .. }
| Message::DeleteTake { .. } => self.handle_takes(message),
// Freeze
Message::FreezeTrack(_) => self.handle_freeze(message),
// Groups
Message::CreateGroup
| Message::DeleteGroup(_)
| Message::SetGroupVolume { .. }
| Message::SetGroupMute(_)
| Message::SetGroupSolo(_)
| Message::AssignTrackToGroup { .. } => self.handle_groups(message),
// Tempo detection
Message::DetectTempo
| Message::DetectTempoCurve => self.handle_tempo_detect(message),
// Spatial
Message::SetSpatialMode(_)
| Message::SetMonoLane(_)
| Message::SetTrackSpatialPosition { .. }
| Message::SetTrackSpatialElevation { .. }
| Message::SetTrackObjectSize { .. } => self.handle_spatial(message),
// Stems
Message::SplitStems { .. } => self.handle_stems(message),
// Clip launcher
Message::TriggerClip { .. }
| Message::StopClip { .. }
| Message::TriggerScene(_)
| Message::StopAllClips => return self.handle_clip_launcher(message),
// Session player
Message::SetSessionPlayerStyle(_)
| Message::SetSessionPlayerRoot(_)
| Message::SetSessionPlayerScale(_)
| Message::SetSessionPlayerDensity(_)
| Message::SetSessionPlayerSwing(_)
| Message::SetSessionPlayerBars(_)
| Message::GenerateSessionPattern => self.handle_session_player(message),
// Inspector toggles
Message::ToggleInspectorSignal
| Message::ToggleInspectorSends
| Message::ToggleInspectorAutomation
| Message::ToggleInspectorSpatial
| Message::ToggleInspectorAnalysis => self.handle_inspector_toggles(message),
// Port network
Message::ToggleNetworkView => {
self.show_network_view = !self.show_network_view;
}
Message::CreatePortConnection { from_module, from_port, to_module, to_port } => {
debug_log!(
"port connect: {}:{} -> {}:{}",
from_module, from_port, to_module, to_port,
);
self.set_status(format!(
"Connection: module {} port {} -> module {} port {}",
from_module, from_port, to_module, to_port,
));
}
Message::DeletePortConnection { from_module, from_port, to_module, to_port } => {
debug_log!(
"port disconnect: {}:{} -> {}:{}",
from_module, from_port, to_module, to_port,
);
self.set_status(format!(
"Disconnected: module {} port {} -> module {} port {}",
from_module, from_port, to_module, to_port,
));
}
// Engine tick
Message::EngineTick => return self.handle_engine_tick(),
}
Task::none()
}
fn handle_inspector_toggles(&mut self, message: Message) {
match message {
Message::ToggleInspectorSignal => { self.inspector_signal_open = !self.inspector_signal_open; }
Message::ToggleInspectorSends => { self.inspector_sends_open = !self.inspector_sends_open; }
Message::ToggleInspectorAutomation => { self.inspector_automation_open = !self.inspector_automation_open; }
Message::ToggleInspectorSpatial => { self.inspector_spatial_open = !self.inspector_spatial_open; }
Message::ToggleInspectorAnalysis => { self.inspector_analysis_open = !self.inspector_analysis_open; }
_ => {}
}
}
pub fn module_for_window(&self, window_id: window::Id) -> Option<u32> {
self.module_gui.module_for_window(window_id)
}
pub fn module_window_view(&self, window_id: window::Id) -> Option<Element<'_, Message>> {
self.module_gui.module_window_view(window_id)
}
pub fn module_window_title(&self, window_id: window::Id) -> Option<String> {
self.module_gui.module_window_title(window_id)
}
pub fn close_module_window_by_id(&mut self, window_id: window::Id) -> Task<Message> {
self.module_gui.close_module_window_by_id(window_id, self.engine.as_ref())
}
pub fn tracks_ref(&self) -> &[Track] {
&self.tracks
}
pub fn set_status(&mut self, msg: String) {
self.last_status = None;
self.status_message = Some((msg, StatusLevel::Info, Instant::now()));
}
pub fn is_dirty(&self) -> bool {
self.dirty
}
pub fn project_name(&self) -> &str {
&self.project_config.name
}
pub fn has_engine(&self) -> bool {
self.engine.is_some()
}
pub(crate) fn mark_dirty(&mut self) {
if !self.dirty {
debug_log!("project marked dirty");
}
self.dirty = true;
}
pub(crate) fn send_spatial_update(engine: &EngineHandle, track: &crate::track::Track) {
engine.send(EngineCommand::SetObjectPosition {
bus_name: track.bus_name.clone(),
position: ObjectPosition {
x: track.spatial_x,
y: track.spatial_y,
z: track.spatial_z,
size: track.object_size,
},
});
}
}

View File

@ -0,0 +1,29 @@
use iced::Task;
use super::{Editor, Message};
impl Editor {
pub(crate) fn handle_module_gui(&mut self, message: Message) -> Task<Message> {
match message {
Message::OpenModuleGui(module_id) => {
return self.module_gui.handle_open_gui(
module_id, self.engine.as_ref(), &self.module_params, &self.routing.module_names,
);
}
Message::CloseModuleGui(module_id) => {
return self.module_gui.handle_close_gui(module_id, self.engine.as_ref());
}
Message::FramebufferMouseDown(module_id) => {
self.module_gui.handle_framebuffer_mouse_down(module_id);
}
Message::FramebufferMouseUp(module_id) => {
self.module_gui.handle_framebuffer_mouse_up(module_id);
}
Message::FramebufferResize(module_id, width, height) => {
self.module_gui.handle_framebuffer_resize(module_id, width, height);
}
_ => {}
}
Task::none()
}
}

View File

@ -0,0 +1,82 @@
use iced::Task;
use super::{Editor, Message};
use crate::engine::EngineCommand;
impl Editor {
pub(crate) fn handle_modules(&mut self, message: Message) -> Task<Message> {
match message {
Message::AddModuleToTrack(track_idx, module_type) => {
self.routing.handle_add_module(track_idx, module_type, &self.tracks, self.engine.as_ref());
}
Message::RemoveModuleFromTrack(track_idx, module_id) => {
let module_gui = &mut self.module_gui;
if self.routing.handle_remove_module(
track_idx, module_id, &mut self.tracks, self.engine.as_ref(),
&mut self.module_params,
&mut |mid| module_gui.handle_module_removed(mid),
) {
self.mark_dirty();
}
}
Message::ToggleModuleDisabled(track_idx, module_id) => {
if self.routing.handle_toggle_disabled(track_idx, module_id, &self.tracks, self.engine.as_ref()) {
self.mark_dirty();
}
}
Message::MoveModuleUp(track_idx, module_id) => {
if self.routing.handle_move_module_up(track_idx, module_id, &mut self.tracks, self.engine.as_ref()) {
self.mark_dirty();
}
}
Message::MoveModuleDown(track_idx, module_id) => {
if self.routing.handle_move_module_down(track_idx, module_id, &mut self.tracks, self.engine.as_ref()) {
self.mark_dirty();
}
}
Message::ShowModulePicker(track_idx) => {
self.routing.module_picker_track = Some(track_idx);
}
Message::HideModulePicker => {
self.routing.module_picker_track = None;
}
Message::LoadPluginOnTrack(track_idx, plugin_path) => {
self.routing.handle_load_plugin(track_idx, plugin_path, &self.tracks, self.engine.as_ref());
}
Message::SetAnalysisFftSize(size) => {
self.analysis_fft_size = size;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetHilbertFftSize { size });
}
}
Message::ExpandModule(module_id) => {
if self.module_params.expanded == Some(module_id) {
self.module_params.expanded = None;
} else {
self.module_params.expanded = Some(module_id);
if !self.module_params.descriptors.contains_key(&module_id) {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::QueryModuleParams { module_id });
}
}
}
}
Message::CollapseModule => {
self.module_params.expanded = None;
}
Message::SetModuleParam { module_id, key, value } => {
self.module_params.values.insert((module_id, key.clone()), value);
self.module_gui.write_param(module_id, &key, value);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetParam { module_id, key, value });
}
}
Message::SelectTrackAndExpandModule { track_index, module_id } => {
self.selected_track = Some(track_index);
return self.update(Message::ExpandModule(module_id));
}
_ => {}
}
Task::none()
}
}

View File

@ -0,0 +1,338 @@
use super::{decode_region_audio, Editor};
use crate::engine::EngineCommand;
use crate::history::EditCommand;
use crate::region::Region;
use crate::timing::MusicalTime;
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn perform_redo(&mut self) {
let cmd = match self.history.pop_redo() {
Some(c) => c,
None => return,
};
match cmd {
EditCommand::MoveRegion { track_index, region_id, old_start, new_start, old_start_sample, new_start_sample } => {
debug_log!("redo MoveRegion: region={} track={}", region_id, track_index);
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
region.start_time = new_start;
region.start_sample = new_start_sample;
}
}
self.history.push(EditCommand::MoveRegion {
track_index, region_id, old_start, new_start, old_start_sample, new_start_sample,
});
}
EditCommand::MoveRegionAcrossTracks { region_id, old_track, new_track, old_start, new_start, old_start_sample, new_start_sample } => {
debug_log!("redo MoveRegionAcrossTracks: region={} track {}->{}",
region_id, old_track, new_track);
if old_track < self.tracks.len() {
if let Some(pos) = self.tracks[old_track].regions.iter().position(|r| r.id == region_id) {
let mut region = self.tracks[old_track].regions.remove(pos);
region.start_time = new_start;
region.start_sample = new_start_sample;
if new_track < self.tracks.len() {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id });
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Ok(decoder) = crate::codec::XtcDecoder::open(&abs_path) {
if let Ok((audio_l, audio_r)) = decoder.decode_real(&abs_path) {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[new_track].bus_name.clone(),
region_id,
start_sample: new_start_sample,
audio_l,
audio_r,
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
}
self.tracks[new_track].regions.push(region);
}
}
}
self.history.push(EditCommand::MoveRegionAcrossTracks {
region_id, old_track, new_track, old_start, new_start, old_start_sample, new_start_sample,
});
}
EditCommand::DeleteRegion { track_index, region } => {
debug_log!("redo DeleteRegion: region={} track={}", region.id, track_index);
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(pos) = track.regions.iter().position(|r| r.id == region.id) {
track.regions.remove(pos);
self.waveform_cache.remove(&region.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: region.id });
}
}
}
self.history.push(EditCommand::DeleteRegion { track_index, region });
}
EditCommand::SplitRegion { track_index, original_id, original_region, left_id, right_id, split_sample } => {
debug_log!("redo SplitRegion: original={} track={} split_sample={}",
original_id, track_index, split_sample);
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(pos) = track.regions.iter().position(|r| r.id == original_id) {
let region = track.regions.remove(pos);
let region_start = region.start_sample;
let region_end = region_start + region.length_samples;
let left_samples = split_sample - region_start;
let right_samples = region_end - split_sample;
let left_duration = MusicalTime::from_samples_mapped(
left_samples, &self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let right_start_time = MusicalTime::from_samples_mapped(
split_sample, &self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let right_duration = MusicalTime::from_samples_mapped(
right_samples, &self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let left_region = Region {
id: left_id,
start_time: region.start_time,
duration: left_duration,
audio_file: region.audio_file.clone(),
start_sample: region_start,
length_samples: left_samples,
selected: false,
fade_in_samples: region.fade_in_samples,
fade_out_samples: 0,
midi_notes: Vec::new(),
playback_rate: region.playback_rate,
};
let right_region = Region {
id: right_id,
start_time: right_start_time,
duration: right_duration,
audio_file: region.audio_file.clone(),
start_sample: split_sample,
length_samples: right_samples,
selected: false,
fade_in_samples: 0,
fade_out_samples: region.fade_out_samples,
midi_notes: Vec::new(),
playback_rate: region.playback_rate,
};
self.waveform_cache.remove(&original_id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: original_id });
}
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Ok(decoder) = crate::codec::XtcDecoder::open(&abs_path) {
if let Ok((audio_l, audio_r)) = decoder.decode_real(&abs_path) {
let offset_l = (region_start as usize).min(audio_l.len());
let end_l = (split_sample as usize).min(audio_l.len());
let end_r = (region_end as usize).min(audio_l.len());
if end_l > offset_l {
self.waveform_cache.insert(
left_id,
WaveformPeaks::from_stereo(
&audio_l[offset_l..end_l],
&audio_r[offset_l..end_l.min(audio_r.len())],
),
);
if let Some(ref engine) = self.engine {
let bus = track.bus_name.clone();
engine.send(EngineCommand::LoadRegionAudio {
bus_name: bus,
region_id: left_id,
start_sample: region_start,
audio_l: audio_l[offset_l..end_l].to_vec(),
audio_r: audio_r[offset_l..end_l.min(audio_r.len())].to_vec(),
fade_in_samples: region.fade_in_samples,
fade_out_samples: 0,
});
}
}
if end_r > end_l {
self.waveform_cache.insert(
right_id,
WaveformPeaks::from_stereo(
&audio_l[end_l..end_r],
&audio_r[end_l..end_r.min(audio_r.len())],
),
);
if let Some(ref engine) = self.engine {
let bus = track.bus_name.clone();
engine.send(EngineCommand::LoadRegionAudio {
bus_name: bus,
region_id: right_id,
start_sample: split_sample,
audio_l: audio_l[end_l..end_r].to_vec(),
audio_r: audio_r[end_l..end_r.min(audio_r.len())].to_vec(),
fade_in_samples: 0,
fade_out_samples: region.fade_out_samples,
});
}
}
}
}
}
track.regions.insert(pos, right_region);
track.regions.insert(pos, left_region);
}
}
self.history.push(EditCommand::SplitRegion {
track_index, original_id, original_region, left_id, right_id, split_sample,
});
}
EditCommand::DeleteTrack { index, track } => {
debug_log!("redo DeleteTrack: index={}", index);
if index < self.tracks.len() {
let removed = self.tracks.remove(index);
for region in &removed.regions {
self.waveform_cache.remove(&region.id);
}
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::RemoveBus { name: removed.bus_name.clone() });
}
}
self.history.push(EditCommand::DeleteTrack { index, track });
}
EditCommand::CreateTrack { index } => {
debug_log!("redo CreateTrack: index={}", index);
self.history.push(EditCommand::CreateTrack { index });
}
EditCommand::DuplicateTrack { source_index, new_index } => {
debug_log!("redo DuplicateTrack: source={} new={}", source_index, new_index);
if source_index < self.tracks.len() {
let source = &self.tracks[source_index];
let mut dup = source.clone();
dup.id = uuid::Uuid::new_v4();
dup.name = format!("{} Copy", dup.name);
dup.bus_name = format!("track_{}", dup.id.as_simple());
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::CreateBus {
name: dup.bus_name.clone(),
is_midi: dup.track_type == crate::track::TrackType::Midi,
});
}
let insert_at = new_index.min(self.tracks.len());
self.tracks.insert(insert_at, dup);
self.track_count += 1;
}
self.history.push(EditCommand::DuplicateTrack { source_index, new_index });
}
EditCommand::PasteRegions { entries } => {
debug_log!("redo PasteRegions: {} regions", entries.len());
let entries_clone = entries.clone();
for (track_index, region) in &entries {
if *track_index < self.tracks.len() {
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Ok(decoder) = crate::codec::XtcDecoder::open(&abs_path) {
if let Ok((audio_l, audio_r)) = decoder.decode_real(&abs_path) {
self.waveform_cache.insert(
region.id,
WaveformPeaks::from_stereo(&audio_l, &audio_r),
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[*track_index].bus_name.clone(),
region_id: region.id,
start_sample: region.start_sample,
audio_l,
audio_r,
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
}
self.tracks[*track_index].regions.push(region.clone());
}
}
self.history.push(EditCommand::PasteRegions { entries: entries_clone });
}
EditCommand::CutRegions { entries } => {
debug_log!("redo CutRegions: {} regions", entries.len());
let entries_clone = entries.clone();
for (track_index, region) in &entries {
if *track_index < self.tracks.len() {
self.tracks[*track_index].regions.retain(|r| r.id != region.id);
self.waveform_cache.remove(&region.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: region.id });
}
}
}
self.history.push(EditCommand::CutRegions { entries: entries_clone });
}
EditCommand::AudioQuantize { track_index, original_region, result_regions } => {
if track_index < self.tracks.len() {
self.tracks[track_index].regions.retain(|r| r.id != original_region.id);
self.waveform_cache.remove(&original_region.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: original_region.id });
}
if let Some(ref audio_file) = original_region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
for r in &result_regions {
let s = (r.start_sample as usize).min(audio_l.len());
let e = (s + r.length_samples as usize).min(audio_l.len());
let sl = &audio_l[s..e];
let sr = &audio_r[s.min(audio_r.len())..e.min(audio_r.len())];
self.waveform_cache.insert(r.id, WaveformPeaks::from_stereo(sl, sr));
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[track_index].bus_name.clone(),
region_id: r.id,
start_sample: r.start_sample,
audio_l: sl.to_vec(),
audio_r: sr.to_vec(),
fade_in_samples: r.fade_in_samples,
fade_out_samples: r.fade_out_samples,
});
}
}
}
}
for r in &result_regions {
self.tracks[track_index].regions.push(r.clone());
}
}
self.history.push(EditCommand::AudioQuantize {
track_index,
original_region,
result_regions,
});
}
EditCommand::SetTempo { old_tempo, new_tempo, old_tempo_map, new_tempo_map } => {
self.tempo = new_tempo;
self.project_config.tempo = new_tempo;
self.tempo_map = new_tempo_map.clone();
self.sync_tempo_to_engine();
self.history.push(EditCommand::SetTempo {
old_tempo,
new_tempo,
old_tempo_map,
new_tempo_map,
});
}
EditCommand::SplitStems { track_indices } => {
self.history.push(EditCommand::SplitStems { track_indices });
}
}
}
}

View File

@ -0,0 +1,241 @@
use super::{apply_flex, decode_region_audio, Editor, Message};
use crate::engine::EngineCommand;
use crate::history::EditCommand;
use crate::region::Region;
use crate::timing::MusicalTime;
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn handle_regions(&mut self, message: Message) {
if let Message::SetRegionPlaybackRate { track_index, region_id, rate } = message {
let clamped = rate.clamp(0.25, 4.0);
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
region.playback_rate = clamped;
self.mark_dirty();
}
}
if let (Some(track), Some(engine)) = (self.tracks.get(track_index), &self.engine) {
if let Some(region) = track.regions.iter().find(|r| r.id == region_id) {
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
let (sl, sr) = apply_flex(audio_l, audio_r, clamped);
engine.send(EngineCommand::LoadRegionAudio {
bus_name: track.bus_name.clone(),
region_id: region.id,
start_sample: region.start_sample,
audio_l: sl,
audio_r: sr,
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
}
}
}
pub(crate) fn delete_selected(&mut self) {
let mut deleted_any_region = false;
for ti in 0..self.tracks.len() {
let mut ri = 0;
while ri < self.tracks[ti].regions.len() {
if self.tracks[ti].regions[ri].selected {
let region = self.tracks[ti].regions.remove(ri);
self.waveform_cache.remove(&region.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: region.id });
}
self.history.push(EditCommand::DeleteRegion {
track_index: ti,
region,
});
deleted_any_region = true;
} else {
ri += 1;
}
}
}
if !deleted_any_region {
if let Some(i) = self.selected_track {
if i < self.tracks.len() {
let removed = self.tracks.remove(i);
for region in &removed.regions {
self.waveform_cache.remove(&region.id);
}
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::RemoveBus {
name: removed.bus_name.clone(),
});
}
self.history.push(EditCommand::DeleteTrack {
index: i,
track: removed,
});
self.selected_track = None;
}
}
}
}
pub(crate) fn delete_region(&mut self, track_index: usize, region_id: uuid::Uuid) {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(pos) = track.regions.iter().position(|r| r.id == region_id) {
let region = track.regions.remove(pos);
self.waveform_cache.remove(&region.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id });
}
self.history.push(EditCommand::DeleteRegion {
track_index,
region,
});
}
}
}
pub(crate) fn split_region(&mut self, track_index: usize, region_id: uuid::Uuid, split_sample: u64) {
let track = match self.tracks.get_mut(track_index) {
Some(t) => t,
None => return,
};
let region_idx = match track.regions.iter().position(|r| r.id == region_id) {
Some(i) => i,
None => return,
};
let region = &track.regions[region_idx];
let region_start_sample = region.start_sample;
let region_end_sample = region_start_sample + region.length_samples;
if split_sample <= region_start_sample || split_sample >= region_end_sample {
return;
}
let left_samples = split_sample - region_start_sample;
let right_samples = region_end_sample - split_sample;
let left_duration = MusicalTime::from_samples_mapped(
left_samples, &self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let right_start = MusicalTime::from_samples_mapped(
split_sample, &self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let right_duration = MusicalTime::from_samples_mapped(
right_samples, &self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
let original_region = region.clone();
let audio_file = region.audio_file.clone();
let left_id = uuid::Uuid::new_v4();
let left_region = Region {
id: left_id,
start_time: original_region.start_time,
duration: left_duration,
audio_file: audio_file.clone(),
start_sample: region_start_sample,
length_samples: left_samples,
selected: false,
fade_in_samples: original_region.fade_in_samples,
fade_out_samples: 0,
midi_notes: Vec::new(),
playback_rate: original_region.playback_rate,
};
let right_id = uuid::Uuid::new_v4();
let right_region = Region {
id: right_id,
start_time: right_start,
duration: right_duration,
audio_file: audio_file.clone(),
start_sample: split_sample,
length_samples: right_samples,
selected: false,
fade_in_samples: 0,
fade_out_samples: original_region.fade_out_samples,
midi_notes: Vec::new(),
playback_rate: original_region.playback_rate,
};
if let Some(ref audio_file) = audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
let offset_l = (region_start_sample as usize).min(audio_l.len());
let end_l = (split_sample as usize).min(audio_l.len());
let end_r = (region_end_sample as usize).min(audio_l.len());
if end_l > offset_l {
self.waveform_cache.insert(
left_id,
WaveformPeaks::from_stereo(
&audio_l[offset_l..end_l],
&audio_r[offset_l..end_l.min(audio_r.len())],
),
);
}
if end_r > end_l {
self.waveform_cache.insert(
right_id,
WaveformPeaks::from_stereo(
&audio_l[end_l..end_r],
&audio_r[end_l..end_r.min(audio_r.len())],
),
);
}
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id });
let bus_name = track.bus_name.clone();
if end_l > offset_l {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: bus_name.clone(),
region_id: left_id,
start_sample: region_start_sample,
audio_l: audio_l[offset_l..end_l].to_vec(),
audio_r: audio_r[offset_l..end_l.min(audio_r.len())].to_vec(),
fade_in_samples: original_region.fade_in_samples,
fade_out_samples: 0,
});
}
if end_r > end_l {
engine.send(EngineCommand::LoadRegionAudio {
bus_name,
region_id: right_id,
start_sample: split_sample,
audio_l: audio_l[end_l..end_r].to_vec(),
audio_r: audio_r[end_l..end_r.min(audio_r.len())].to_vec(),
fade_in_samples: 0,
fade_out_samples: original_region.fade_out_samples,
});
}
}
}
}
self.waveform_cache.remove(&region_id);
let track = &mut self.tracks[track_index];
track.regions.remove(region_idx);
track.regions.insert(region_idx, right_region);
track.regions.insert(region_idx, left_region);
self.history.push(EditCommand::SplitRegion {
track_index,
original_id: region_id,
original_region,
left_id,
right_id,
split_sample,
});
}
}

View File

@ -0,0 +1,30 @@
use super::{Editor, Message};
impl Editor {
pub(crate) fn handle_sends(&mut self, message: Message) {
match message {
Message::ShowSendPicker(track_idx) => {
self.routing.send_picker_track = Some(track_idx);
}
Message::HideSendPicker => {
self.routing.send_picker_track = None;
}
Message::AddSend { track_index, aux_bus_name } => {
if self.routing.handle_add_send(track_index, aux_bus_name, &mut self.tracks, self.engine.as_ref()) {
self.dirty = true;
}
}
Message::RemoveSend { track_index, send_index } => {
if self.routing.handle_remove_send(track_index, send_index, &mut self.tracks, self.engine.as_ref()) {
self.dirty = true;
}
}
Message::SetSendLevel { track_index, send_index, level } => {
if self.routing.handle_set_send_level(track_index, send_index, level, &mut self.tracks, self.engine.as_ref()) {
self.dirty = true;
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,69 @@
use std::path::PathBuf;
use super::Editor;
impl Editor {
pub(crate) fn sync_config(&mut self) {
self.project_config.tracks = self.tracks.clone();
self.project_config.markers = self.markers.clone();
self.project_config.tempo = self.tempo;
self.project_config.tempo_points = self.tempo_map.points.clone();
self.project_config.time_signature_numerator = self.time_signature_numerator;
self.project_config.time_signature_denominator = self.time_signature_denominator;
self.project_config.groups = self.groups.clone();
}
pub fn save_project(&mut self) {
debug_log!("save_project: {}", self.project_path.display());
self.sync_config();
let config_path = self.project_path.join("project.toml");
match toml::to_string_pretty(&self.project_config) {
Ok(content) => {
match std::fs::write(&config_path, content) {
Ok(()) => {
self.dirty = false;
debug_log!(" saved, dirty cleared");
}
Err(_e) => {
debug_log!(" ERROR writing project.toml: {}", _e);
}
}
}
Err(_e) => {
debug_log!(" ERROR serializing project: {}", _e);
}
}
}
#[allow(dead_code)]
pub fn save_project_as(&mut self, new_path: PathBuf) {
debug_log!("save_project_as: {} -> {}", self.project_path.display(), new_path.display());
if let Err(_e) = std::fs::create_dir_all(&new_path) {
debug_log!(" ERROR creating directory: {}", _e);
return;
}
if let Ok(entries) = std::fs::read_dir(&self.project_path) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("xtc") && path.is_file() {
let dest = new_path.join(path.file_name().unwrap());
if let Err(_e) = std::fs::copy(&path, &dest) {
debug_log!(" ERROR copying {}: {}", path.display(), _e);
}
}
}
}
let new_name = new_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Untitled")
.to_string();
self.project_path = new_path;
self.project_config.name = new_name;
self.save_project();
}
}

View File

@ -0,0 +1,86 @@
use super::{Editor, Message};
use crate::engine::session_player;
use crate::region::Region;
use crate::timing::MusicalTime;
impl Editor {
pub(crate) fn handle_session_player(&mut self, message: Message) {
match message {
Message::SetSessionPlayerStyle(style) => {
self.session_player_config.style = style;
}
Message::SetSessionPlayerRoot(root) => {
self.session_player_config.root_note = root;
}
Message::SetSessionPlayerScale(scale) => {
self.session_player_config.scale = scale;
}
Message::SetSessionPlayerDensity(d) => {
self.session_player_config.density = d;
}
Message::SetSessionPlayerSwing(s) => {
self.session_player_config.swing = s;
}
Message::SetSessionPlayerBars(b) => {
self.session_player_bars = b;
}
Message::GenerateSessionPattern => {
self.generate_session_pattern();
}
_ => {}
}
}
fn generate_session_pattern(&mut self) {
let Some(track_idx) = self.selected_track else { return };
let Some(track) = self.tracks.get(track_idx) else { return };
if track.track_type != crate::track::TrackType::Midi { return }
let beats_per_bar = self.time_signature_numerator as u32;
let notes = session_player::generate_pattern(
&self.session_player_config,
self.session_player_bars,
beats_per_bar,
);
let ticks_per_beat = crate::timing::TICKS_PER_BEAT as f64;
let midi_notes: Vec<crate::region::MidiNote> = notes.iter().map(|n| {
crate::region::MidiNote {
start_tick: (n.beat_offset * ticks_per_beat) as u64,
duration_ticks: (n.duration_beats * ticks_per_beat) as u64,
note: n.note,
velocity: n.velocity,
channel: 0,
}
}).collect();
let total_beats = self.session_player_bars as f64 * beats_per_bar as f64;
let total_ticks = (total_beats * ticks_per_beat) as u64;
let ticks_per_bar = beats_per_bar as u64 * crate::timing::TICKS_PER_BEAT as u64;
let duration_bars = (total_ticks / ticks_per_bar) as u32;
let start = self.current_position;
let duration = MusicalTime::new(duration_bars, 1, 0);
let start_sample = start.to_samples_mapped(&self.tempo_map, self.project_config.sample_rate, beats_per_bar);
let midi_events = session_player::notes_to_midi_events(
&notes, self.tempo as f64, self.project_config.sample_rate,
);
let length_samples = midi_events.last()
.map(|(pos, _, _, _)| *pos)
.unwrap_or_else(|| duration.to_samples_mapped(&self.tempo_map, self.project_config.sample_rate, beats_per_bar));
let region = Region::with_midi(
start, duration, start_sample, length_samples, midi_notes,
);
let track = self.tracks.get_mut(track_idx).unwrap();
track.regions.push(region);
self.mark_dirty();
let track = &self.tracks[track_idx];
let region = track.regions.last().unwrap();
self.sync_midi_region_to_engine(track, region);
self.session_player_config.seed = self.session_player_config.seed.wrapping_add(1);
}
}

View File

@ -0,0 +1,61 @@
use super::{Editor, Message};
use crate::engine::EngineCommand;
impl Editor {
pub(crate) fn handle_spatial(&mut self, message: Message) {
match message {
Message::SetSpatialMode(mode) => {
self.spatial_mode = mode;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetSpatialMode(mode));
}
}
Message::SetMonoLane(lane) => {
self.mono_lane = lane;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetMonoLane(lane));
}
}
Message::SetTrackSpatialPosition { track_index, x, y } => {
if let Some(track) = self.tracks.get_mut(track_index) {
track.spatial_x = x.clamp(-1.0, 1.0);
track.spatial_y = y.clamp(-1.0, 1.0);
let pan = x.clamp(-1.0, 1.0);
track.pan = pan;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetBusPan {
bus_name: track.bus_name.clone(),
pan,
});
let depth_atten = 1.0 - y.clamp(0.0, 1.0) * 0.5;
engine.send(EngineCommand::SetBusVolume {
bus_name: track.bus_name.clone(),
volume: track.volume * depth_atten,
});
Self::send_spatial_update(engine, track);
}
self.mark_dirty();
}
}
Message::SetTrackSpatialElevation { track_index, z } => {
if let Some(track) = self.tracks.get_mut(track_index) {
track.spatial_z = z.clamp(-1.0, 1.0);
if let Some(ref engine) = self.engine {
Self::send_spatial_update(engine, track);
}
self.mark_dirty();
}
}
Message::SetTrackObjectSize { track_index, size } => {
if let Some(track) = self.tracks.get_mut(track_index) {
track.object_size = size.clamp(0.0, 1.0);
if let Some(ref engine) = self.engine {
Self::send_spatial_update(engine, track);
}
self.mark_dirty();
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,129 @@
use std::time::Instant;
use super::{decode_region_audio, Editor, Message, StatusLevel};
use crate::engine::EngineCommand;
use crate::history::EditCommand;
use crate::region::Region;
use crate::track::Track;
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn handle_stems(&mut self, message: Message) {
if let Message::SplitStems { track_index, region_id } = message {
self.split_stems_from_region(track_index, region_id);
}
}
fn split_stems_from_region(&mut self, track_index: usize, region_id: uuid::Uuid) {
let sample_rate = self.project_config.sample_rate;
let (audio_file, start_sample, length_samples, start_time, duration, playback_rate, region_label) = {
let track = match self.tracks.get(track_index) {
Some(t) => t,
None => return,
};
let region = match track.regions.iter().find(|r| r.id == region_id) {
Some(r) => r,
None => return,
};
if region.is_midi() || region.audio_file.is_none() {
self.status_message = Some(("Cannot split stems from MIDI region".into(), StatusLevel::Warning, Instant::now()));
return;
}
let label = region.audio_file.as_ref()
.and_then(|f| std::path::Path::new(f).file_stem())
.and_then(|s| s.to_str())
.map(|s| s.to_string())
.unwrap_or_else(|| track.name.clone());
(
region.audio_file.clone().unwrap(),
region.start_sample,
region.length_samples,
region.start_time,
region.duration,
region.playback_rate,
label,
)
};
self.status_message = Some(("Splitting stems\u{2026}".into(), StatusLevel::Info, Instant::now()));
let abs_path = self.project_path.join(&audio_file);
let (audio_l, audio_r) = match decode_region_audio(&abs_path, sample_rate) {
Some(a) => a,
None => {
self.status_message = Some(("Stem split failed: could not decode audio".into(), StatusLevel::Error, Instant::now()));
return;
}
};
let s = (start_sample as usize).min(audio_l.len());
let e = (s + length_samples as usize).min(audio_l.len());
let slice_l = &audio_l[s..e];
let slice_r = &audio_r[s.min(audio_r.len())..e.min(audio_r.len())];
let stems = crate::engine::stems::split_stems(slice_l, slice_r);
let stem_names = ["Bass", "Drums", "Vocals", "Other"];
let stem_data = [stems.bass, stems.drums, stems.vocals, stems.other];
let mut created_indices = Vec::with_capacity(4);
for (name, (stem_l, stem_r)) in stem_names.iter().zip(stem_data.into_iter()) {
let config = crate::track::TrackConfig {
name: format!("{} - {}", region_label, name),
track_type: crate::track::TrackType::Audio,
};
let track = Track::new(config, self.track_count);
self.track_count += 1;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::CreateBus { name: track.bus_name.clone(), is_midi: false });
engine.send(EngineCommand::SetBusVolume {
bus_name: track.bus_name.clone(),
volume: track.volume,
});
}
let region = Region {
id: uuid::Uuid::new_v4(),
start_time,
duration,
audio_file: Some(audio_file.clone()),
start_sample,
length_samples,
selected: false,
fade_in_samples: 0,
fade_out_samples: 0,
midi_notes: Vec::new(),
playback_rate,
};
self.waveform_cache.insert(region.id, WaveformPeaks::from_stereo(&stem_l, &stem_r));
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: track.bus_name.clone(),
region_id: region.id,
start_sample,
audio_l: stem_l,
audio_r: stem_r,
fade_in_samples: 0,
fade_out_samples: 0,
});
}
let mut track = track;
track.regions.push(region);
let idx = self.tracks.len();
self.tracks.push(track);
created_indices.push(idx);
}
self.history.push(EditCommand::SplitStems { track_indices: created_indices });
self.mark_dirty();
self.status_message = Some((
format!("Stems split: 4 tracks created ({} - Bass/Drums/Vocals/Other)", region_label),
StatusLevel::Info,
Instant::now(),
));
}
}

View File

@ -0,0 +1,67 @@
use super::{decode_region_audio, Editor, Message};
use crate::engine::EngineCommand;
impl Editor {
pub(crate) fn handle_takes(&mut self, message: Message) {
match message {
Message::SetActiveTake { track_index, folder_id, take_index } => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(folder) = track.take_folders.iter_mut().find(|f| f.id == folder_id) {
if take_index < folder.take_ids.len() {
if let Some(prev_id) = folder.active_take_id() {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: prev_id });
}
}
folder.active_index = take_index;
let new_id = folder.take_ids[take_index];
if let Some(region) = track.regions.iter().find(|r| r.id == new_id) {
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: track.bus_name.clone(),
region_id: new_id,
start_sample: region.start_sample,
audio_l,
audio_r,
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
}
self.mark_dirty();
}
}
}
}
Message::DeleteTake { track_index, folder_id, take_index } => {
let mut removed_id = None;
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(folder) = track.take_folders.iter_mut().find(|f| f.id == folder_id) {
if take_index < folder.take_ids.len() {
let rid = folder.take_ids.remove(take_index);
track.regions.retain(|r| r.id != rid);
if folder.active_index >= folder.take_ids.len() && !folder.take_ids.is_empty() {
folder.active_index = folder.take_ids.len() - 1;
}
removed_id = Some(rid);
}
}
track.take_folders.retain(|f| f.take_ids.len() > 1);
}
if let Some(rid) = removed_id {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: rid });
}
self.waveform_cache.remove(&rid);
self.mark_dirty();
}
}
_ => {}
}
}
}

View File

@ -0,0 +1,112 @@
use std::time::Instant;
use super::{decode_region_audio, Editor, Message, StatusLevel};
use crate::history::EditCommand;
impl Editor {
pub(crate) fn handle_tempo_detect(&mut self, message: Message) {
match message {
Message::DetectTempo => {
self.detect_tempo_from_regions(false);
}
Message::DetectTempoCurve => {
self.detect_tempo_from_regions(true);
}
_ => {}
}
}
fn detect_tempo_from_regions(&mut self, detect_curve: bool) {
let sample_rate = self.project_config.sample_rate;
let mut all_l: Vec<f32> = Vec::new();
let mut all_r: Vec<f32> = Vec::new();
for track in &self.tracks {
for region in &track.regions {
if region.is_midi() { continue; }
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, sample_rate) {
let s = (region.start_sample as usize).min(audio_l.len());
let e = (s + region.length_samples as usize).min(audio_l.len());
let needed = region.start_sample as usize + (e - s);
if all_l.len() < needed {
all_l.resize(needed, 0.0);
all_r.resize(needed, 0.0);
}
for i in s..e {
let dst = region.start_sample as usize + (i - s);
if dst < all_l.len() {
all_l[dst] += audio_l[i];
all_r[dst] += audio_r[i.min(audio_r.len().saturating_sub(1))];
}
}
}
}
}
}
if all_l.is_empty() {
self.status_message = Some(("No audio to analyze".into(), StatusLevel::Warning, Instant::now()));
return;
}
let old_tempo = self.tempo;
let old_tempo_map = self.tempo_map.clone();
if detect_curve {
let curve = crate::engine::onset::estimate_tempo_curve(
&all_l, &all_r, sample_rate, 4.0,
);
if curve.is_empty() {
self.status_message = Some(("Could not detect tempo".into(), StatusLevel::Warning, Instant::now()));
return;
}
self.tempo = curve[0].1;
self.tempo_map = crate::timing::TempoMap::new(self.tempo);
for &(sample_pos, tempo) in &curve {
self.tempo_map.insert_point(sample_pos, tempo);
}
self.project_config.tempo = self.tempo;
self.sync_tempo_to_engine();
self.mark_dirty();
self.history.push(EditCommand::SetTempo {
old_tempo,
new_tempo: self.tempo,
old_tempo_map,
new_tempo_map: self.tempo_map.clone(),
});
self.status_message = Some((
format!("Detected tempo curve: {} points, base {:.1} BPM (applied)", curve.len(), self.tempo),
StatusLevel::Info,
Instant::now(),
));
} else {
match crate::engine::onset::estimate_tempo(&all_l, &all_r, sample_rate) {
Some(bpm) => {
let rounded = (bpm * 10.0).round() / 10.0;
self.tempo = rounded;
self.project_config.tempo = rounded;
self.tempo_map = crate::timing::TempoMap::new(rounded);
self.sync_tempo_to_engine();
self.mark_dirty();
self.history.push(EditCommand::SetTempo {
old_tempo,
new_tempo: rounded,
old_tempo_map,
new_tempo_map: self.tempo_map.clone(),
});
self.status_message = Some((
format!("Detected tempo: {:.1} BPM (applied)", rounded),
StatusLevel::Info,
Instant::now(),
));
}
None => {
self.status_message = Some(("Could not detect tempo".into(), StatusLevel::Warning, Instant::now()));
}
}
}
}
}

View File

@ -0,0 +1,217 @@
use iced::Task;
use super::{decode_region_audio, Editor, Message};
use crate::engine::EngineCommand;
use crate::gui::editor::timeline;
use crate::history::EditCommand;
use crate::region::Region;
impl Editor {
pub(crate) fn handle_timeline(&mut self, message: Message) -> Task<Message> {
match message {
Message::Timeline(timeline::Message::RegionClicked { track_index, region_id, shift }) => {
if !shift {
for track in &mut self.tracks {
for region in &mut track.regions {
region.selected = false;
}
}
}
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
region.selected = !region.selected || !shift;
}
}
self.selected_track = Some(track_index);
for (idx, t) in self.tracks.iter_mut().enumerate() {
t.selected = idx == track_index;
}
}
Message::Timeline(timeline::Message::RegionDragging { region_id, track_index, new_start, new_start_sample }) => {
let mut found = None;
for (ti, track) in self.tracks.iter().enumerate() {
if let Some(ri) = track.regions.iter().position(|r| r.id == region_id) {
found = Some((ti, ri));
break;
}
}
if let Some((old_track_idx, region_idx)) = found {
if old_track_idx == track_index {
let region = &mut self.tracks[old_track_idx].regions[region_idx];
region.start_time = new_start;
region.start_sample = new_start_sample;
} else if track_index < self.tracks.len() {
let mut region = self.tracks[old_track_idx].regions.remove(region_idx);
region.start_time = new_start;
region.start_sample = new_start_sample;
self.tracks[track_index].regions.push(region);
}
}
}
Message::Timeline(timeline::Message::RegionDragEnd {
region_id, old_track, new_track, old_start, new_start, old_start_sample, new_start_sample,
}) => {
debug_log!("region drag end: region={} old_track={} new_track={}", region_id, old_track, new_track);
if old_track == new_track {
self.history.push(EditCommand::MoveRegion {
track_index: new_track,
region_id,
old_start,
new_start,
old_start_sample,
new_start_sample,
});
} else {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id });
if new_track < self.tracks.len() {
if let Some(region) = self.tracks[new_track].regions.iter().find(|r| r.id == region_id) {
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[new_track].bus_name.clone(),
region_id,
start_sample: new_start_sample,
audio_l,
audio_r,
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
}
}
self.history.push(EditCommand::MoveRegionAcrossTracks {
region_id,
old_track,
new_track,
old_start,
new_start,
old_start_sample,
new_start_sample,
});
}
self.mark_dirty();
}
Message::Timeline(timeline::Message::RegionSplit { track_index, region_id, split_sample }) => {
self.split_region(track_index, region_id, split_sample);
self.mark_dirty();
}
Message::Timeline(timeline::Message::RegionDelete { track_index, region_id }) => {
self.delete_region(track_index, region_id);
self.mark_dirty();
}
Message::Timeline(timeline::Message::DeselectAll) => {
for track in &mut self.tracks {
for region in &mut track.regions {
region.selected = false;
}
}
}
Message::Timeline(timeline::Message::AddMarker(pos)) => {
return self.update(Message::AddMarker(pos));
}
Message::Timeline(timeline::Message::DeleteMarker(id)) => {
return self.update(Message::DeleteMarker(id));
}
Message::Timeline(timeline::Message::JumpToMarker(id)) => {
return self.update(Message::JumpToMarker(id));
}
Message::Timeline(timeline::Message::SetCycleRange { start_bar, end_bar }) => {
self.cycle_start_bar = start_bar;
self.cycle_end_bar = end_bar;
self.cycle_enabled = true;
self.send_cycle_state();
if self.punch_enabled { self.send_punch_state(); }
}
Message::Timeline(timeline::Message::SetRegionFade { track_index, region_id, fade_in_samples, fade_out_samples }) => {
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
region.fade_in_samples = fade_in_samples;
region.fade_out_samples = fade_out_samples;
self.dirty = true;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetRegionFade {
region_id,
fade_in_samples,
fade_out_samples,
});
}
}
}
}
Message::Timeline(timeline::Message::AutomationPointAdded { track_index, lane_index, sample_pos, value }) => {
return self.update(Message::AddAutomationPoint { track_index, lane_index, sample_pos, value });
}
Message::Timeline(timeline::Message::AutomationPointMoved { track_index, lane_index, point_index, sample_pos, value }) => {
return self.update(Message::MoveAutomationPoint { track_index, lane_index, point_index, sample_pos, value });
}
Message::Timeline(timeline::Message::AutomationPointRemoved { track_index, lane_index, point_index }) => {
return self.update(Message::RemoveAutomationPoint { track_index, lane_index, point_index });
}
Message::Timeline(timeline::Message::TempoPointAdded { sample_pos, tempo }) => {
return self.update(Message::AddTempoPoint { sample_pos, tempo });
}
Message::Timeline(timeline::Message::TempoPointMoved { index, sample_pos, tempo }) => {
return self.update(Message::MoveTempoPoint { index, sample_pos, tempo });
}
Message::Timeline(timeline::Message::TempoPointRemoved { index }) => {
return self.update(Message::RemoveTempoPoint(index));
}
Message::Timeline(timeline::Message::ZoomChanged(h, v)) => {
self.h_zoom = h.clamp(10.0, 1000.0);
self.v_zoom = v.clamp(0.3, 5.0);
}
Message::Timeline(timeline::Message::PlayheadMoved(time)) => {
self.current_position = time;
let sample_pos = time.to_samples_mapped(
&self.tempo_map,
self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::Seek { sample_pos });
}
}
Message::Timeline(timeline::Message::CycleTake { track_index, folder_id }) => {
if let Some(track) = self.tracks.get(track_index) {
if let Some(folder) = track.take_folders.iter().find(|f| f.id == folder_id) {
let next = (folder.active_index + 1) % folder.take_ids.len();
return self.update(Message::SetActiveTake {
track_index,
folder_id,
take_index: next,
});
}
}
}
Message::Timeline(timeline::Message::SetActiveTake { track_index, folder_id, take_index }) => {
return self.update(Message::SetActiveTake { track_index, folder_id, take_index });
}
Message::Timeline(timeline::Message::DeleteTake { track_index, folder_id, take_index }) => {
return self.update(Message::DeleteTake { track_index, folder_id, take_index });
}
Message::Timeline(timeline::Message::CreateMidiRegion {
track_index, start_time, duration, start_sample, length_samples,
}) => {
if let Some(track) = self.tracks.get_mut(track_index) {
if track.track_type == crate::track::TrackType::Midi {
let region = Region::with_midi(
start_time, duration, start_sample, length_samples, Vec::new(),
);
track.regions.push(region);
self.mark_dirty();
}
}
}
Message::Timeline(timeline::Message::SetRegionPlaybackRate { track_index, region_id, rate }) => {
return self.update(Message::SetRegionPlaybackRate { track_index, region_id, rate });
}
_ => {}
}
Task::none()
}
}

View File

@ -0,0 +1,176 @@
use iced::Task;
use super::{Editor, Message, ModalState};
use crate::engine::{EngineCommand, TransportState};
use crate::gui::editor::{new_track_wizard, track_header};
use crate::history::EditCommand;
use crate::track::Track;
impl Editor {
pub(crate) fn handle_tracks(&mut self, message: Message) -> Task<Message> {
match message {
Message::ShowNewTrackWizard => {
self.modal_state =
Some(ModalState::NewTrackWizard(new_track_wizard::State::default()));
}
Message::NewTrackWizard(wizard_message) => {
if let Some(ModalState::NewTrackWizard(state)) = &mut self.modal_state {
match wizard_message {
new_track_wizard::Message::Cancel => self.modal_state = None,
new_track_wizard::Message::Create => {
let config = state.config.clone();
let track = Track::new(config, self.track_count);
self.track_count += 1;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::CreateBus {
name: track.bus_name.clone(),
is_midi: track.track_type == crate::track::TrackType::Midi,
});
engine.send(EngineCommand::SetBusVolume {
bus_name: track.bus_name.clone(),
volume: track.volume,
});
}
let idx = self.tracks.len();
self.tracks.push(track);
self.history.push(EditCommand::CreateTrack { index: idx });
self.modal_state = None;
}
new_track_wizard::Message::NameChanged(name) => state.config.name = name,
new_track_wizard::Message::TrackTypeSelected(track_type) => {
state.config.track_type = track_type
}
}
}
}
Message::TrackHeader(i, msg) => {
if let Some(track) = self.tracks.get_mut(i) {
match msg {
track_header::Message::MuteToggled => {
track.muted = !track.muted;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetBusMute {
bus_name: track.bus_name.clone(),
muted: track.muted,
});
}
self.mark_dirty();
}
track_header::Message::SoloToggled => {
track.soloed = !track.soloed;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetBusSolo {
bus_name: track.bus_name.clone(),
soloed: track.soloed,
});
}
self.mark_dirty();
}
track_header::Message::RecordArmToggled => {
track.record_armed = !track.record_armed;
if let Some(ref engine) = self.engine {
if track.record_armed {
engine.send(EngineCommand::ArmTrack {
bus_name: track.bus_name.clone(),
});
} else {
engine.send(EngineCommand::DisarmTrack {
bus_name: track.bus_name.clone(),
});
}
}
}
track_header::Message::VolumeChanged(vol) => {
track.volume = if (vol - 0.75).abs() < 0.02 { 0.75 } else { vol };
let group_vol = track.group_id
.and_then(|gid| self.groups.iter().find(|g| g.id == gid))
.map(|g| g.volume)
.unwrap_or(1.0);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetBusVolume {
bus_name: track.bus_name.clone(),
volume: track.volume * group_vol,
});
}
let should_record = self.transport == TransportState::Playing && track.automation_mode.writes();
let vol_val = track.volume;
self.mark_dirty();
if should_record {
let sample_pos = self.current_position.to_samples_mapped(
&self.tempo_map, self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
self.record_automation_point(i, crate::automation::AutomationTarget::Volume, sample_pos, vol_val);
}
}
track_header::Message::PanChanged(pan) => {
track.pan = if pan.abs() < 0.02 { 0.0 } else { pan };
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetBusPan {
bus_name: track.bus_name.clone(),
pan: track.pan,
});
}
let should_record = self.transport == TransportState::Playing && track.automation_mode.writes();
let pan_val = track.pan;
self.mark_dirty();
if should_record {
let sample_pos = self.current_position.to_samples_mapped(
&self.tempo_map, self.project_config.sample_rate,
self.time_signature_numerator as u32,
);
self.record_automation_point(i, crate::automation::AutomationTarget::Pan, sample_pos, pan_val);
}
}
track_header::Message::Delete => {
let removed = self.tracks.remove(i);
for region in &removed.regions {
self.waveform_cache.remove(&region.id);
}
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::RemoveBus {
name: removed.bus_name.clone(),
});
}
self.history.push(EditCommand::DeleteTrack {
index: i,
track: removed,
});
self.mark_dirty();
if self.selected_track == Some(i) {
self.selected_track = None;
} else if let Some(sel) = self.selected_track {
if sel > i {
self.selected_track = Some(sel - 1);
}
}
}
track_header::Message::Select => {
self.selected_track = Some(i);
for (idx, t) in self.tracks.iter_mut().enumerate() {
t.selected = idx == i;
}
}
track_header::Message::FreezeToggled => {
return self.update(Message::FreezeTrack(i));
}
}
}
}
Message::SetTrackColor(i, color) => {
if let Some(track) = self.tracks.get_mut(i) {
track.color = color;
self.mark_dirty();
}
}
Message::SetMonitorMode(i, mode) => {
if let Some(track) = self.tracks.get_mut(i) {
track.monitor_mode = mode;
self.mark_dirty();
}
}
_ => {}
}
Task::none()
}
}

View File

@ -0,0 +1,133 @@
use iced::Task;
use super::{Editor, Message};
use crate::engine::{EngineCommand, TransportState};
impl Editor {
pub(crate) fn handle_transport(&mut self, message: Message) -> Task<Message> {
match message {
Message::PlayPressed => {
if self.transport == TransportState::Playing {
self.transport = TransportState::Stopped;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetTransportState(TransportState::Stopped));
}
} else {
self.transport = TransportState::Playing;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetTransportState(TransportState::Playing));
}
}
}
Message::StopPressed => {
if self.record_armed {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::StopRecording);
}
self.record_armed = false;
}
self.transport = TransportState::Stopped;
self.current_position = crate::timing::MusicalTime::new(1, 1, 0);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetTransportState(TransportState::Stopped));
}
}
Message::RecordPressed => {
if self.record_armed {
self.record_armed = false;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::StopRecording);
}
return Task::none();
}
let has_armed_tracks = self.tracks.iter().any(|t| t.record_armed);
if !has_armed_tracks {
return Task::none();
}
self.record_armed = true;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::StartRecording {
project_path: self.project_path.clone(),
sample_rate: self.project_config.sample_rate,
bit_depth: 24,
fft_size: self.analysis_fft_size as u32,
});
if self.transport != TransportState::Playing {
self.transport = TransportState::Playing;
engine.send(EngineCommand::SetTransportState(TransportState::Playing));
}
}
}
Message::RewindPressed => {
self.current_position = crate::timing::MusicalTime::new(1, 1, 0);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::Seek { sample_pos: 0 });
}
}
Message::CycleToggled => {
self.cycle_enabled = !self.cycle_enabled;
self.send_cycle_state();
}
Message::MetronomeToggled => {
self.metronome_enabled = !self.metronome_enabled;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetMetronomeEnabled(self.metronome_enabled));
}
}
Message::CountInToggled => {
self.count_in_enabled = !self.count_in_enabled;
let bars = if self.count_in_enabled { 1 } else { 0 };
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetCountIn { bars });
}
}
Message::PunchToggled => {
self.punch_enabled = !self.punch_enabled;
self.send_punch_state();
}
Message::MasterVolumeChanged(vol) => {
self.master_volume = vol;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetMasterVolume(vol));
}
}
Message::MasterPanChanged(pan) => {
self.master_pan = pan;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetMasterPan(pan));
}
}
_ => {}
}
Task::none()
}
pub(crate) fn send_cycle_state(&self) {
let beats_per_bar = self.time_signature_numerator as f64;
let samples_per_beat = (60.0 / self.tempo as f64) * self.project_config.sample_rate as f64;
let start_sample = ((self.cycle_start_bar - 1) as f64 * beats_per_bar * samples_per_beat) as u64;
let end_sample = ((self.cycle_end_bar - 1) as f64 * beats_per_bar * samples_per_beat) as u64;
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::SetCycleState {
enabled: self.cycle_enabled,
start_sample,
end_sample,
});
}
}
pub(crate) fn send_punch_state(&self) {
let Some(ref engine) = self.engine else { return };
let beats_per_bar = self.time_signature_numerator as f64;
let samples_per_beat = (60.0 / self.tempo as f64) * self.project_config.sample_rate as f64;
let start = ((self.cycle_start_bar - 1) as f64 * beats_per_bar * samples_per_beat) as u64;
let end = ((self.cycle_end_bar - 1) as f64 * beats_per_bar * samples_per_beat) as u64;
engine.send(EngineCommand::SetPunch {
enabled: self.punch_enabled,
start_sample: start,
end_sample: end,
});
}
}

View File

@ -0,0 +1,315 @@
use super::{decode_region_audio, Editor};
use crate::engine::EngineCommand;
use crate::history::EditCommand;
use crate::waveform::WaveformPeaks;
impl Editor {
pub(crate) fn perform_undo(&mut self) {
let cmd = match self.history.pop_undo() {
Some(c) => c,
None => return,
};
match cmd {
EditCommand::MoveRegion { track_index, region_id, old_start, new_start, old_start_sample, new_start_sample } => {
debug_log!("undo MoveRegion: region={} track={}", region_id, track_index);
if let Some(track) = self.tracks.get_mut(track_index) {
if let Some(region) = track.regions.iter_mut().find(|r| r.id == region_id) {
region.start_time = old_start;
region.start_sample = old_start_sample;
}
}
self.history.push_redo(EditCommand::MoveRegion {
track_index, region_id,
old_start: new_start,
new_start: old_start,
old_start_sample: new_start_sample,
new_start_sample: old_start_sample,
});
}
EditCommand::MoveRegionAcrossTracks { region_id, old_track, new_track, old_start, new_start, old_start_sample, new_start_sample } => {
debug_log!("undo cross-track move: region={} track {}->{}",
region_id, new_track, old_track);
if new_track < self.tracks.len() {
if let Some(pos) = self.tracks[new_track].regions.iter().position(|r| r.id == region_id) {
let mut region = self.tracks[new_track].regions.remove(pos);
region.start_time = old_start;
region.start_sample = old_start_sample;
if old_track < self.tracks.len() {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id });
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[old_track].bus_name.clone(),
region_id,
start_sample: old_start_sample,
audio_l,
audio_r,
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
self.tracks[old_track].regions.push(region);
}
}
}
self.history.push_redo(EditCommand::MoveRegionAcrossTracks {
region_id,
old_track: new_track,
new_track: old_track,
old_start: new_start,
new_start: old_start,
old_start_sample: new_start_sample,
new_start_sample: old_start_sample,
});
}
EditCommand::DeleteRegion { track_index, region } => {
debug_log!("undo DeleteRegion: region={} track={}", region.id, track_index);
let region_clone = region.clone();
if track_index < self.tracks.len() {
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
let start = (region.start_sample as usize).min(audio_l.len());
let end = (start + region.length_samples as usize).min(audio_l.len());
let sl = &audio_l[start..end];
let sr = &audio_r[start.min(audio_r.len())..end.min(audio_r.len())];
self.waveform_cache.insert(
region.id,
WaveformPeaks::from_stereo(sl, sr),
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[track_index].bus_name.clone(),
region_id: region.id,
start_sample: region.start_sample,
audio_l: sl.to_vec(),
audio_r: sr.to_vec(),
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
self.tracks[track_index].regions.push(region);
}
self.history.push_redo(EditCommand::DeleteRegion {
track_index,
region: region_clone,
});
}
EditCommand::DeleteTrack { index, track } => {
debug_log!("undo DeleteTrack: index={}", index);
let track_clone = track.clone();
if index <= self.tracks.len() {
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::CreateBus {
name: track.bus_name.clone(),
is_midi: track.track_type == crate::track::TrackType::Midi,
});
}
self.tracks.insert(index, track);
}
self.history.push_redo(EditCommand::DeleteTrack {
index,
track: track_clone,
});
}
EditCommand::CreateTrack { index } => {
debug_log!("undo CreateTrack: index={}", index);
if index < self.tracks.len() {
let removed = self.tracks.remove(index);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::RemoveBus {
name: removed.bus_name.clone(),
});
}
self.history.push_redo(EditCommand::CreateTrack { index });
}
}
EditCommand::DuplicateTrack { source_index, new_index } => {
debug_log!("undo DuplicateTrack: source={} new={}", source_index, new_index);
if new_index < self.tracks.len() {
let removed = self.tracks.remove(new_index);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::RemoveBus {
name: removed.bus_name.clone(),
});
}
}
self.history.push_redo(EditCommand::DuplicateTrack { source_index, new_index });
}
EditCommand::SplitRegion { track_index, original_id, original_region, left_id, right_id, split_sample } => {
debug_log!("undo SplitRegion: original={} track={}", original_id, track_index);
if let Some(track) = self.tracks.get_mut(track_index) {
track.regions.retain(|r| r.id != left_id && r.id != right_id);
self.waveform_cache.remove(&left_id);
self.waveform_cache.remove(&right_id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: left_id });
engine.send(EngineCommand::UnloadRegionAudio { region_id: right_id });
}
let orig_clone = original_region.clone();
if let Some(ref audio_file) = original_region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Ok(decoder) = crate::codec::XtcDecoder::open(&abs_path) {
if let Ok((audio_l, audio_r)) = decoder.decode_real(&abs_path) {
let start = original_region.start_sample as usize;
let end = (start + original_region.length_samples as usize).min(audio_l.len());
let sl = &audio_l[start.min(audio_l.len())..end];
let sr = &audio_r[start.min(audio_r.len())..end.min(audio_r.len())];
self.waveform_cache.insert(
original_id,
WaveformPeaks::from_stereo(sl, sr),
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: track.bus_name.clone(),
region_id: original_id,
start_sample: original_region.start_sample,
audio_l: audio_l[start.min(audio_l.len())..end].to_vec(),
audio_r: audio_r[start.min(audio_r.len())..end.min(audio_r.len())].to_vec(),
fade_in_samples: original_region.fade_in_samples,
fade_out_samples: original_region.fade_out_samples,
});
}
}
}
}
track.regions.push(original_region);
self.history.push_redo(EditCommand::SplitRegion {
track_index,
original_id,
original_region: orig_clone,
left_id,
right_id,
split_sample,
});
}
}
EditCommand::PasteRegions { entries } => {
debug_log!("undo PasteRegions: {} regions", entries.len());
let entries_clone = entries.clone();
for (track_index, region) in &entries {
if *track_index < self.tracks.len() {
self.tracks[*track_index].regions.retain(|r| r.id != region.id);
self.waveform_cache.remove(&region.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: region.id });
}
}
}
self.history.push_redo(EditCommand::PasteRegions { entries: entries_clone });
}
EditCommand::CutRegions { entries } => {
debug_log!("undo CutRegions: {} regions", entries.len());
let entries_clone = entries.clone();
for (track_index, region) in &entries {
if *track_index < self.tracks.len() {
if let Some(ref audio_file) = region.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Ok(decoder) = crate::codec::XtcDecoder::open(&abs_path) {
if let Ok((audio_l, audio_r)) = decoder.decode_real(&abs_path) {
let start = (region.start_sample as usize).min(audio_l.len());
let end = (start + region.length_samples as usize).min(audio_l.len());
let sl = &audio_l[start..end];
let sr = &audio_r[start.min(audio_r.len())..end.min(audio_r.len())];
self.waveform_cache.insert(
region.id,
WaveformPeaks::from_stereo(sl, sr),
);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[*track_index].bus_name.clone(),
region_id: region.id,
start_sample: region.start_sample,
audio_l: sl.to_vec(),
audio_r: sr.to_vec(),
fade_in_samples: region.fade_in_samples,
fade_out_samples: region.fade_out_samples,
});
}
}
}
}
self.tracks[*track_index].regions.push(region.clone());
}
}
self.history.push_redo(EditCommand::CutRegions { entries: entries_clone });
}
EditCommand::AudioQuantize { track_index, original_region, result_regions } => {
if track_index < self.tracks.len() {
for r in &result_regions {
self.tracks[track_index].regions.retain(|rr| rr.id != r.id);
self.waveform_cache.remove(&r.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: r.id });
}
}
let orig = original_region.clone();
if let Some(ref audio_file) = orig.audio_file {
let abs_path = self.project_path.join(audio_file);
if let Some((audio_l, audio_r)) = decode_region_audio(&abs_path, self.project_config.sample_rate) {
let s = (orig.start_sample as usize).min(audio_l.len());
let e = (s + orig.length_samples as usize).min(audio_l.len());
let sl = &audio_l[s..e];
let sr = &audio_r[s.min(audio_r.len())..e.min(audio_r.len())];
self.waveform_cache.insert(orig.id, WaveformPeaks::from_stereo(sl, sr));
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::LoadRegionAudio {
bus_name: self.tracks[track_index].bus_name.clone(),
region_id: orig.id,
start_sample: orig.start_sample,
audio_l: sl.to_vec(),
audio_r: sr.to_vec(),
fade_in_samples: orig.fade_in_samples,
fade_out_samples: orig.fade_out_samples,
});
}
}
}
self.tracks[track_index].regions.push(orig);
}
self.history.push_redo(EditCommand::AudioQuantize {
track_index,
original_region,
result_regions,
});
}
EditCommand::SetTempo { old_tempo, new_tempo, old_tempo_map, new_tempo_map } => {
self.tempo = old_tempo;
self.project_config.tempo = old_tempo;
self.tempo_map = old_tempo_map.clone();
self.sync_tempo_to_engine();
self.history.push_redo(EditCommand::SetTempo {
old_tempo: new_tempo,
new_tempo: old_tempo,
old_tempo_map: new_tempo_map,
new_tempo_map: old_tempo_map,
});
}
EditCommand::SplitStems { track_indices } => {
for &idx in track_indices.iter().rev() {
if idx < self.tracks.len() {
let removed = self.tracks.remove(idx);
for r in &removed.regions {
self.waveform_cache.remove(&r.id);
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::UnloadRegionAudio { region_id: r.id });
}
}
if let Some(ref engine) = self.engine {
engine.send(EngineCommand::RemoveBus { name: removed.bus_name.clone() });
}
}
}
self.history.push_redo(EditCommand::SplitStems { track_indices });
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More