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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,20 @@ When adding or removing dependencies:

---

## Pre-commit checklist

**Always run all three of these commands and confirm they pass before every `git commit`:**

```sh
uv run ruff format . # auto-format — re-stage any files it changes
uv run ruff check . # lint — must report "All checks passed!"
uv run pytest --tb=short -q # tests — must report 0 failures, 0 errors
```

Do not commit if any of these steps fails. Fix the issue first, then re-run all three.

---

## Git workflow

```
Expand Down
48 changes: 46 additions & 2 deletions MultiChannelWavMixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import pyloudnorm as pyln
import soundfile as sf
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from PIL import Image
from pydub import AudioSegment

from mixer_utils import (
Expand All @@ -34,6 +35,12 @@
ctk.set_default_color_theme("blue")

# ─── Constants ─────────────────────────────────────────────────────────────────
# Resolve where bundled/source assets (Pics/) live.
if getattr(sys, "frozen", False):
_BUNDLE_DIR = sys._MEIPASS
else:
_BUNDLE_DIR = os.path.dirname(os.path.abspath(__file__))

# Resolve paths that must work both from source and inside a .app bundle.
if getattr(sys, "frozen", False):
# Running inside PyInstaller bundle — store user data in Application Support
Expand Down Expand Up @@ -515,11 +522,21 @@ def set_output_folder(inFilePath=None):
lbl_output_folder.configure(text=display)


def _load_ctk_image(filename: str, size: tuple[int, int]) -> ctk.CTkImage | None:
"""Load a PNG from Pics/ as a CTkImage, returning None on failure."""
path = os.path.join(_BUNDLE_DIR, "Pics", filename)
try:
img = Image.open(path)
return ctk.CTkImage(light_image=img, dark_image=img, size=size)
except Exception:
return None


# ─── Main window ───────────────────────────────────────────────────────────────
root = ctk.CTk()
root.title("Multichannel WAV Mixer")
root.geometry("1060x560")
root.minsize(900, 420)
root.geometry("1060x628")
root.minsize(900, 490)


def bring_to_front(event=None):
Expand All @@ -529,6 +546,33 @@ def bring_to_front(event=None):

root.bind("<FocusIn>", bring_to_front)

# ─── Header ───────────────────────────────────────────────────────────────────
header = ctk.CTkFrame(root, corner_radius=0, height=66, fg_color="#090c15")
header.pack(side="top", fill="x")
header.pack_propagate(False)

_logo_img = _load_ctk_image("Logo.png", (76, 53))
if _logo_img:
ctk.CTkLabel(header, image=_logo_img, text="").pack(side="left", padx=(16, 10), pady=6)

_title_frame = ctk.CTkFrame(header, fg_color="transparent")
_title_frame.pack(side="left", pady=10)
ctk.CTkLabel(
_title_frame,
text="Multichannel WAV Mixer",
font=ctk.CTkFont(size=19, weight="bold"),
text_color="#daeef8",
).pack(anchor="w")
ctk.CTkLabel(
_title_frame,
text="RME Durec · Stereo Mixdown",
font=ctk.CTkFont(size=11),
text_color="#4a7a92",
).pack(anchor="w")

# Accent line separating header from toolbar
ctk.CTkFrame(root, corner_radius=0, height=2, fg_color="#1e5f7a").pack(side="top", fill="x")

# ─── State variables ───────────────────────────────────────────────────────────
file_paths: tuple = ()
tracks: list = []
Expand Down
7 changes: 6 additions & 1 deletion MultiChannelWavMixer.spec
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ block_cipher = None
# ── Data files to bundle ────────────────────────────────────────────────────────
datas = []

# Application images (logo, header background)
import glob as _glob
datas += [(_f, "Pics") for _f in _glob.glob("Pics/*.png")]

# customtkinter — themes, images, assets
datas += collect_data_files("customtkinter")

Expand Down Expand Up @@ -111,6 +115,7 @@ exe = EXE(
target_arch=None, # native arch; use 'universal2' for fat binary
codesign_identity=None,
entitlements_file=None,
icon="Pics/AppIcon.ico" if sys.platform == "win32" else None,
)

coll = COLLECT(
Expand All @@ -129,7 +134,7 @@ if sys.platform == "darwin":
app = BUNDLE(
coll,
name="MultiChannelWavMixer.app",
icon=None, # set to "path/to/icon.icns" when available
icon="Pics/AppIcon.icns",
bundle_identifier="com.macbuchi.multichannelwavmixer",
info_plist={
"CFBundleShortVersionString": "1.0.0",
Expand Down
Binary file added Pics/AppIcon.icns
Binary file not shown.
Binary file added Pics/AppIcon.ico
Binary file not shown.
Binary file added Pics/App_Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Pics/BG_Header.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions Pics/BG_Header.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Pics/Background.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 61 additions & 0 deletions Pics/Background.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added Pics/Logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
58 changes: 58 additions & 0 deletions Pics/Logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions tests/test_app_smoke.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
tests/test_app_smoke.py
-----------------------
Subprocess smoke-test for MultiChannelWavMixer.py.

Launches the GUI app as a child process, waits 6 seconds, verifies it is
still alive (i.e. no import error or immediate crash), then terminates it.

Skipped on platforms where the GUI backend is unavailable (Linux CI).
The pytest job in CI only runs on macos-latest, so this will always execute
there while remaining a safe no-op elsewhere.
"""

from __future__ import annotations

import os
import subprocess
import sys
import time
from pathlib import Path

import pytest

# Root of the repository — one level above this tests/ dir.
_REPO_ROOT = Path(__file__).parent.parent
_APP_SCRIPT = _REPO_ROOT / "MultiChannelWavMixer.py"

# Seconds the app must stay alive to be considered healthy.
_ALIVE_SECS = 6


@pytest.mark.skipif(
sys.platform not in ("darwin", "win32"),
reason="GUI smoke test requires a platform with a native display (macOS / Windows)",
)
def test_app_starts_and_stays_alive() -> None:
"""Launch the app and confirm it does not exit within _ALIVE_SECS seconds."""
proc = subprocess.Popen(
[sys.executable, str(_APP_SCRIPT)],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=str(_REPO_ROOT),
env={**os.environ},
)

time.sleep(_ALIVE_SECS)
exit_code = proc.poll()

if exit_code is None:
# Still running — healthy.
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
return

# Process exited before we got a chance to check — collect output and fail.
_, stderr = proc.communicate(timeout=5)
error_text = stderr.decode(errors="replace").strip()
pytest.fail(
f"App exited after {_ALIVE_SECS}s with code {exit_code}.\nstderr:\n{error_text[:2000]}"
)
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.