Skip to content

Commit e6bb292

Browse files
committed
WIP - Add CLI targets
1 parent 3f08f90 commit e6bb292

File tree

2 files changed

+240
-0
lines changed

2 files changed

+240
-0
lines changed

TemplateProject/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,20 @@ if(NOT IOS AND NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
144144
LINK iPlug2::CLI
145145
)
146146
iplug_configure_cli(${PROJECT_NAME}-cli ${PROJECT_NAME})
147+
148+
# ============================================================================
149+
# CLI Target (Command Line Interface) - NO IGraphics, for offline processing
150+
# ============================================================================
151+
add_executable(${PROJECT_NAME}-cli
152+
TemplateProject.cpp
153+
TemplateProject.h
154+
resources/resource.h
155+
)
156+
iplug_add_target(${PROJECT_NAME}-cli PUBLIC
157+
INCLUDE ${PROJECT_DIR} ${PROJECT_DIR}/resources
158+
LINK iPlug2::CLI
159+
)
160+
iplug_configure_cli(${PROJECT_NAME}-cli ${PROJECT_NAME})
147161
endif()
148162

149163
# ============================================================================
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Python harness for plotting impulse responses from iPlug2 CLI plugins.
4+
5+
Usage:
6+
python plot_impulse.py <cli_binary> [options]
7+
8+
Examples:
9+
# Basic impulse plot with default settings
10+
python plot_impulse.py ./build/out/MyPlugin
11+
12+
# Set gain to 50% and plot 1024 samples
13+
python plot_impulse.py ./build/out/MyPlugin --set 0 50 --length 1024
14+
15+
# Compare multiple gain settings
16+
python plot_impulse.py ./build/out/MyPlugin --sweep 0 0 100 5
17+
18+
# Save to PNG
19+
python plot_impulse.py ./build/out/MyPlugin --set 0 75 -o impulse.png
20+
"""
21+
22+
import argparse
23+
import subprocess
24+
import tempfile
25+
import os
26+
import sys
27+
from pathlib import Path
28+
29+
# Optional: matplotlib for plotting
30+
try:
31+
import matplotlib.pyplot as plt
32+
HAS_MATPLOTLIB = True
33+
except ImportError:
34+
HAS_MATPLOTLIB = False
35+
36+
37+
38+
39+
def run_impulse(cli_path, length=4096, params=None, sample_rate=44100):
40+
"""
41+
Run the CLI to generate an impulse response.
42+
43+
Args:
44+
cli_path: Path to the CLI binary
45+
length: Impulse response length in samples
46+
params: List of (index, value) tuples to set parameters
47+
sample_rate: Sample rate in Hz
48+
49+
Returns:
50+
List of float samples
51+
"""
52+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
53+
output_path = f.name
54+
55+
try:
56+
cmd = [cli_path, "--sr", str(sample_rate)]
57+
58+
# Add parameter settings
59+
if params:
60+
for idx, val in params:
61+
cmd.extend(["--set", str(idx), str(val)])
62+
63+
cmd.extend(["--impulse", str(length), "--output-txt", output_path])
64+
65+
result = subprocess.run(cmd, capture_output=True, text=True)
66+
67+
if result.returncode != 0:
68+
print(f"CLI error: {result.stderr}", file=sys.stderr)
69+
return None
70+
71+
# Read samples from output file
72+
samples = []
73+
with open(output_path, 'r') as f:
74+
for line in f:
75+
line = line.strip()
76+
if line:
77+
samples.append(float(line))
78+
79+
return samples
80+
81+
finally:
82+
if os.path.exists(output_path):
83+
os.unlink(output_path)
84+
85+
86+
def plot_impulse(samples, title="Impulse Response", sample_rate=44100, output_path=None):
87+
"""Plot an impulse response."""
88+
if not HAS_MATPLOTLIB:
89+
print("matplotlib not installed. Install with: pip install matplotlib")
90+
print("\nSample data (first 32 samples):")
91+
for i, s in enumerate(samples[:32]):
92+
print(f" [{i:4d}] {s:+.6f}")
93+
return None
94+
95+
n = len(samples)
96+
time_ms = [i * 1000.0 / sample_rate for i in range(n)]
97+
98+
fig, ax = plt.subplots(figsize=(10, 4))
99+
ax.plot(time_ms, samples, 'b-', linewidth=0.8)
100+
ax.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)
101+
ax.set_xlabel('Time (ms)')
102+
ax.set_ylabel('Amplitude')
103+
ax.set_title(title)
104+
ax.grid(True, alpha=0.3)
105+
106+
plt.tight_layout()
107+
108+
if output_path:
109+
plt.savefig(output_path, dpi=150, bbox_inches='tight')
110+
plt.close(fig)
111+
return output_path
112+
else:
113+
plt.show()
114+
return None
115+
116+
117+
def plot_comparison(results, sample_rate=44100, output_path=None):
118+
"""Plot multiple impulse responses for comparison."""
119+
if not HAS_MATPLOTLIB:
120+
print("matplotlib not installed for comparison plot")
121+
return None
122+
123+
fig, ax = plt.subplots(figsize=(10, 5))
124+
125+
for label, samples in results.items():
126+
n = len(samples)
127+
time_ms = [i * 1000.0 / sample_rate for i in range(n)]
128+
ax.plot(time_ms, samples, label=label, linewidth=0.8)
129+
130+
ax.axhline(y=0, color='gray', linestyle='-', linewidth=0.5)
131+
ax.set_xlabel('Time (ms)')
132+
ax.set_ylabel('Amplitude')
133+
ax.set_title('Impulse Response Comparison')
134+
ax.legend()
135+
ax.grid(True, alpha=0.3)
136+
137+
plt.tight_layout()
138+
139+
if output_path:
140+
plt.savefig(output_path, dpi=150, bbox_inches='tight')
141+
plt.close(fig)
142+
return output_path
143+
else:
144+
plt.show()
145+
return None
146+
147+
148+
def main():
149+
parser = argparse.ArgumentParser(
150+
description='Plot impulse responses from iPlug2 CLI plugins',
151+
formatter_class=argparse.RawDescriptionHelpFormatter,
152+
epilog=__doc__
153+
)
154+
parser.add_argument('cli', type=str, help='Path to CLI binary')
155+
parser.add_argument('--length', '-l', type=int, default=4096,
156+
help='Impulse response length in samples (default: 4096)')
157+
parser.add_argument('--sr', type=int, default=44100,
158+
help='Sample rate in Hz (default: 44100)')
159+
parser.add_argument('--set', nargs=2, action='append', metavar=('IDX', 'VAL'),
160+
help='Set parameter IDX to VAL (can be repeated)')
161+
parser.add_argument('--sweep', nargs=4, type=float, metavar=('IDX', 'START', 'END', 'STEPS'),
162+
help='Sweep parameter IDX from START to END in STEPS')
163+
parser.add_argument('--output', '-o', type=str,
164+
help='Save plot to PNG file instead of displaying')
165+
parser.add_argument('--no-plot', action='store_true',
166+
help='Print samples instead of plotting')
167+
168+
args = parser.parse_args()
169+
170+
cli_path = args.cli
171+
print(f"Using CLI: {cli_path}")
172+
173+
# Parse parameters
174+
params = []
175+
if args.set:
176+
for idx, val in args.set:
177+
params.append((int(idx), float(val)))
178+
179+
if args.sweep:
180+
# Parameter sweep mode
181+
param_idx = int(args.sweep[0])
182+
start, end, steps = args.sweep[1], args.sweep[2], int(args.sweep[3])
183+
184+
results = {}
185+
for i in range(steps + 1):
186+
val = start + (end - start) * i / steps
187+
sweep_params = params + [(param_idx, val)]
188+
samples = run_impulse(cli_path, args.length, sweep_params, args.sr)
189+
if samples:
190+
results[f"Param {param_idx}={val:.1f}"] = samples
191+
192+
if results:
193+
if args.no_plot or not HAS_MATPLOTLIB:
194+
for label, samples in results.items():
195+
print(f"\n{label}:")
196+
print(f" Peak: {max(abs(s) for s in samples):.6f}")
197+
print(f" Sum: {sum(samples):.6f}")
198+
else:
199+
out = plot_comparison(results, args.sr, args.output)
200+
if out:
201+
print(f"Saved plot to: {out}")
202+
else:
203+
# Single impulse mode
204+
samples = run_impulse(cli_path, args.length, params, args.sr)
205+
206+
if samples:
207+
if args.no_plot or not HAS_MATPLOTLIB:
208+
print(f"\nImpulse Response ({len(samples)} samples):")
209+
print(f" Peak amplitude: {max(abs(s) for s in samples):.6f}")
210+
print(f" DC sum: {sum(samples):.6f}")
211+
print("\nFirst 32 samples:")
212+
for i, s in enumerate(samples[:32]):
213+
bar = '#' * int(abs(s) * 50) if abs(s) > 0.001 else ''
214+
print(f" [{i:4d}] {s:+.6f} {bar}")
215+
else:
216+
title = "Impulse Response"
217+
if params:
218+
param_str = ', '.join(f"P{i}={v}" for i, v in params)
219+
title += f" ({param_str})"
220+
out = plot_impulse(samples, title, args.sr, args.output)
221+
if out:
222+
print(f"Saved plot to: {out}")
223+
224+
225+
if __name__ == '__main__':
226+
main()

0 commit comments

Comments
 (0)