Skip to content
Merged
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
33 changes: 28 additions & 5 deletions src/arduino/app_utils/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class SineGenerator:
sample_rate (int): Audio sample rate in Hz.
attack (float): Attack time for amplitude smoothing in seconds.
release (float): Release time for amplitude smoothing in seconds.
glide (float): Glide time for frequency smoothing in seconds.
"""

def __init__(self, sample_rate: int):
Expand All @@ -31,6 +32,7 @@ def __init__(self, sample_rate: int):
# envelope parameters (attack/release in seconds)
self.attack = 0.01
self.release = 0.03
self.glide = 0.02

# reusable buffers
self._buf_N = 0
Expand Down Expand Up @@ -88,17 +90,19 @@ def set_state(self, state: dict) -> None:
if "freq_last" in state:
self._freq_last = float(state["freq_last"])

def set_envelope_params(self, attack: float, release: float) -> None:
def set_envelope_params(self, attack: float, release: float, glide: float) -> None:
"""Update attack and release envelope parameters.

Args:
attack (float): Attack time in seconds (time to rise to target
amplitude when increasing amplitude).
release (float): Release time in seconds (time to fall to target
amplitude when decreasing amplitude).
glide (float): Glide time in seconds (time to reach target frequency).
"""
self.attack = float(max(0.0, attack))
self.release = float(max(0.0, release))
self.glide = float(max(0.0, glide))

def generate_block(self, freq: float, amp_target: float, block_dur: float, master_volume: float):
"""Generate a block of float32 audio samples.
Expand Down Expand Up @@ -149,10 +153,29 @@ def generate_block(self, freq: float, amp_target: float, block_dur: float, maste
envelope[:] = np.linspace(amp_current, next_amp, N, dtype=np.float32)
amp_current = float(envelope[-1])

# oscillator
phase_incr = 2.0 * math.pi * float(freq) / float(self.sample_rate)
# frequency glide (portamento)
freq_current = float(self._freq_last)
freq_target = float(freq)
glide = float(self.glide)
phase_incs = self._buf_phase_incs[:N]
phase_incs.fill(phase_incr)

if glide > 0.0 and freq_current != freq_target:
# Apply glide smoothing over time
frac = min(1.0, block_dur / glide)
next_freq = freq_current + (freq_target - freq_current) * frac

# Linear interpolation within block
freq_ramp = np.linspace(freq_current, next_freq, N, dtype=np.float32)
phase_incs[:] = 2.0 * math.pi * freq_ramp / float(self.sample_rate)

freq_current = float(next_freq)
else:
# No glide or already at target
phase_incr = 2.0 * math.pi * freq_target / float(self.sample_rate)
phase_incs.fill(phase_incr)
freq_current = freq_target

# oscillator (phase accumulation)
np.cumsum(phase_incs, dtype=np.float32, out=phases)
phases += self._phase
self._phase = float(phases[-1] % (2.0 * math.pi))
Expand All @@ -168,6 +191,6 @@ def generate_block(self, freq: float, amp_target: float, block_dur: float, maste

# update state
self._amp_current = amp_current
self._freq_last = float(freq)
self._freq_last = freq_current

return samples