- Add --lfo flag for slow modulation (0.5Hz) - Add --fast-lfo flag for rhythmic modulation (5Hz) - Display frequency type (LFO/Audio) in output - More intuitive LFO usage for modulation applications Usage: uv run python scripts/demo_oscilloscope.py --lfo --waveform sine uv run python scripts/demo_oscilloscope.py --fast-lfo --waveform triangle
205 lines
5.9 KiB
Python
205 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Oscilloscope Demo - Real-time waveform visualization
|
|
|
|
This demonstrates a real oscilloscope-style display where:
|
|
1. A complete waveform is drawn on the canvas
|
|
2. The camera scrolls horizontally (time axis)
|
|
3. The "pen" traces the waveform vertically at the center
|
|
|
|
Think of it as:
|
|
- Canvas: Contains the waveform pattern (like a stamp)
|
|
- Camera: Moves left-to-right, revealing different parts of the waveform
|
|
- Pen: Always at center X, moves vertically with the signal value
|
|
|
|
Usage:
|
|
uv run python scripts/demo_oscilloscope.py --frequency 1.0 --speed 10
|
|
"""
|
|
|
|
import argparse
|
|
import math
|
|
import time
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
# Add mainline to path
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from engine.sensors.oscillator import OscillatorSensor, register_oscillator_sensor
|
|
|
|
|
|
def render_oscilloscope(
|
|
width: int,
|
|
height: int,
|
|
osc: OscillatorSensor,
|
|
frame: int,
|
|
) -> str:
|
|
"""Render an oscilloscope-style display."""
|
|
# Get current reading (0.0 to 1.0)
|
|
reading = osc.read()
|
|
current_value = reading.value if reading else 0.5
|
|
phase = osc._phase
|
|
frequency = osc.frequency
|
|
|
|
# Build visualization
|
|
lines = []
|
|
|
|
# Header with sensor info
|
|
header = (
|
|
f"Oscilloscope: {osc.name} | Wave: {osc.waveform} | "
|
|
f"Freq: {osc.frequency}Hz | Phase: {phase:.2f}"
|
|
)
|
|
lines.append(header)
|
|
lines.append("─" * width)
|
|
|
|
# Center line (zero reference)
|
|
center_row = height // 2
|
|
|
|
# Draw oscilloscope trace
|
|
waveform_fn = osc.WAVEFORMS[osc._waveform]
|
|
|
|
# Calculate time offset for scrolling
|
|
# The trace scrolls based on phase - this creates the time axis movement
|
|
# At frequency 1.0, the trace completes one full sweep per frequency cycle
|
|
time_offset = phase * frequency * 2.0
|
|
|
|
# Pre-calculate all sample values for this frame
|
|
# Each column represents a time point on the X axis
|
|
samples = []
|
|
for col in range(width):
|
|
# Time position for this column (0.0 to 1.0 across width)
|
|
col_fraction = col / width
|
|
# Combine with time offset for scrolling effect
|
|
time_pos = time_offset + col_fraction
|
|
|
|
# Sample the waveform at this time point
|
|
# Multiply by frequency to get correct number of cycles shown
|
|
sample_value = waveform_fn(time_pos * frequency * 2)
|
|
samples.append(sample_value)
|
|
|
|
# Draw the trace
|
|
# For each row, check which columns have their sample value in this row
|
|
for row in range(height - 3): # Reserve 3 lines for header/footer
|
|
# Calculate vertical position (0.0 at bottom, 1.0 at top)
|
|
row_pos = 1.0 - (row / (height - 4))
|
|
|
|
line_chars = []
|
|
for col in range(width):
|
|
sample = samples[col]
|
|
|
|
# Check if this sample falls in this row
|
|
tolerance = 1.0 / (height - 4)
|
|
if abs(sample - row_pos) < tolerance:
|
|
line_chars.append("█")
|
|
else:
|
|
line_chars.append(" ")
|
|
lines.append("".join(line_chars))
|
|
|
|
# Draw center indicator line
|
|
center_line = list(" " * width)
|
|
# Position the indicator based on current value
|
|
indicator_x = int((current_value) * (width - 1))
|
|
if 0 <= indicator_x < width:
|
|
center_line[indicator_x] = "◎"
|
|
lines.append("".join(center_line))
|
|
|
|
# Footer with current value
|
|
footer = f"Value: {current_value:.3f} | Frame: {frame} | Phase: {phase:.2f}"
|
|
lines.append(footer)
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
def demo_oscilloscope(
|
|
waveform: str = "sine",
|
|
frequency: float = 1.0,
|
|
frames: int = 0,
|
|
):
|
|
"""Run oscilloscope demo."""
|
|
# Determine if this is LFO range
|
|
is_lfo = frequency <= 20.0 and frequency >= 0.1
|
|
freq_type = "LFO" if is_lfo else "Audio"
|
|
|
|
print(f"Oscilloscope demo: {waveform} wave")
|
|
print(f"Frequency: {frequency}Hz ({freq_type} range)")
|
|
if frames > 0:
|
|
print(f"Running for {frames} frames")
|
|
else:
|
|
print("Press Ctrl+C to stop")
|
|
print()
|
|
|
|
# Create oscillator sensor
|
|
register_oscillator_sensor(
|
|
name="oscilloscope_osc", waveform=waveform, frequency=frequency
|
|
)
|
|
osc = OscillatorSensor(
|
|
name="oscilloscope_osc", waveform=waveform, frequency=frequency
|
|
)
|
|
osc.start()
|
|
|
|
# Run demo loop
|
|
try:
|
|
frame = 0
|
|
while frames == 0 or frame < frames:
|
|
# Render oscilloscope display
|
|
visualization = render_oscilloscope(80, 22, osc, frame)
|
|
|
|
# Print with ANSI escape codes to clear screen and move cursor
|
|
print("\033[H\033[J" + visualization)
|
|
|
|
time.sleep(1.0 / 60.0) # 60 FPS
|
|
frame += 1
|
|
|
|
except KeyboardInterrupt:
|
|
print("\n\nDemo stopped by user")
|
|
|
|
finally:
|
|
osc.stop()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description="Oscilloscope demo")
|
|
parser.add_argument(
|
|
"--waveform",
|
|
choices=["sine", "square", "sawtooth", "triangle", "noise"],
|
|
default="sine",
|
|
help="Waveform type",
|
|
)
|
|
parser.add_argument(
|
|
"--frequency",
|
|
type=float,
|
|
default=1.0,
|
|
help="Oscillator frequency in Hz (LFO: 0.1-20Hz, Audio: >20Hz)",
|
|
)
|
|
parser.add_argument(
|
|
"--lfo",
|
|
action="store_true",
|
|
help="Use LFO frequency (0.5Hz - slow modulation)",
|
|
)
|
|
parser.add_argument(
|
|
"--fast-lfo",
|
|
action="store_true",
|
|
help="Use fast LFO frequency (5Hz - rhythmic modulation)",
|
|
)
|
|
parser.add_argument(
|
|
"--frames",
|
|
type=int,
|
|
default=0,
|
|
help="Number of frames to render (0 = infinite until Ctrl+C)",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Determine frequency based on mode
|
|
frequency = args.frequency
|
|
if args.lfo:
|
|
frequency = 0.5 # Slow LFO for modulation
|
|
elif args.fast_lfo:
|
|
frequency = 5.0 # Fast LFO for rhythmic modulation
|
|
|
|
demo_oscilloscope(
|
|
waveform=args.waveform,
|
|
frequency=frequency,
|
|
frames=args.frames,
|
|
)
|