Files
livekit--agents/examples/avatar/hold_music.py
T
2026-07-13 13:39:38 +08:00

103 lines
2.8 KiB
Python

from __future__ import annotations
from collections.abc import AsyncIterator
import numpy as np
from livekit import rtc
_SAMPLE_RATE = 48000
_BLOCK = 4800
_ROOT_HZ = 174.61 # F3
_CHORD_SEMITONES = (0, 4, 7) # F major triad
_BEAT_S = 0.28
_NOTE_DUR_S = 0.34
_TAG_DELAY_S = 0.08
_TAG_DUR_S = 0.18
_TAG_AMP = 0.45
_TAIL_S = 0.85
_ATTACK_FRAC = 0.55
_RELEASE_FRAC = 0.10
_WOBBLE_HZ = 22.0
_WOBBLE_DEPTH = 0.05
_DETUNE_CENTS = 2.0
_AMP = 2500.0
def _asr_envelope(n: int) -> np.ndarray:
if n <= 1:
return np.zeros(n)
attack_n = max(1, int(n * _ATTACK_FRAC))
release_n = max(1, int(n * _RELEASE_FRAC))
sustain_n = max(0, n - attack_n - release_n)
env = np.empty(n, dtype=np.float64)
env[:attack_n] = np.linspace(0.0, 1.0, attack_n)
env[attack_n : attack_n + sustain_n] = 1.0
env[attack_n + sustain_n :] = np.linspace(1.0, 0.0, release_n)
if _WOBBLE_DEPTH > 0:
t = np.arange(n, dtype=np.float64) / _SAMPLE_RATE
env *= (
1.0 - _WOBBLE_DEPTH + _WOBBLE_DEPTH * (0.5 + 0.5 * np.cos(2 * np.pi * _WOBBLE_HZ * t))
)
return env
def _note(freq: float, dur_s: float, amp: float) -> np.ndarray:
n = int(dur_s * _SAMPLE_RATE)
t = np.arange(n, dtype=np.float64) / _SAMPLE_RATE
det = 2.0 ** (_DETUNE_CENTS / 1200.0)
voice = 0.5 * np.sin(2 * np.pi * freq * det * t)
voice += 0.5 * np.sin(2 * np.pi * freq / det * t)
return voice * _asr_envelope(n) * amp
def _semitone_freq(root_hz: float, semis: int) -> float:
return root_hz * (2.0 ** (semis / 12.0))
def _build_hold_loop() -> np.ndarray:
chord_notes = [_semitone_freq(_ROOT_HZ, s) for s in _CHORD_SEMITONES]
tag_freq = chord_notes[-1]
tag_onset = len(chord_notes) * _BEAT_S + _TAG_DELAY_S
total_n = int((tag_onset + _TAG_DUR_S + _TAIL_S) * _SAMPLE_RATE)
out = np.zeros(total_n, dtype=np.float64)
for i, freq in enumerate(chord_notes):
note = _note(freq, _NOTE_DUR_S, _AMP)
start = int(i * _BEAT_S * _SAMPLE_RATE)
end = min(total_n, start + note.shape[0])
out[start:end] += note[: end - start]
tag = _note(tag_freq, _TAG_DUR_S, _AMP * _TAG_AMP)
start = int(tag_onset * _SAMPLE_RATE)
end = min(total_n, start + tag.shape[0])
out[start:end] += tag[: end - start]
return np.clip(out, -32767.0, 32767.0).astype(np.int16)
_HOLD_LOOP: np.ndarray | None = None
def _get_hold_loop() -> np.ndarray:
global _HOLD_LOOP
if _HOLD_LOOP is None:
_HOLD_LOOP = _build_hold_loop()
return _HOLD_LOOP
async def hold_beats() -> AsyncIterator[rtc.AudioFrame]:
loop = _get_hold_loop()
loop_n = loop.shape[0]
t = 0
while True:
idx = (np.arange(t, t + _BLOCK) % loop_n).astype(np.int64)
chunk = loop[idx].tobytes()
t += _BLOCK
yield rtc.AudioFrame(chunk, _SAMPLE_RATE, 1, _BLOCK)