SecretSanta / gen-roulette-sfx.py
mooncake030's picture
hello
719b8f9
import wave
import numpy as np
# 參數設定
sample_rate = 44100 # 取樣率 (Hz)
duration = 0.5 # 音效長度 (秒)
num_impacts = 1 # 撞擊次數,可自行調整
# 建立空的音訊 buffer
num_samples = int(sample_rate * duration)
audio = np.zeros(num_samples, dtype=np.float32)
# 產生單一撞擊聲的函式
def generate_impact(sample_rate, max_length_ms=5):
"""
回傳一個短促的撞擊聲波形 (numpy 1D array)。
使用白噪音 * 指數衰減包絡,模擬鋼珠撞擊的高頻 'click'。
"""
length_ms = np.random.uniform(2, max_length_ms) # 2~5ms 的撞擊長度
length_samples = int(sample_rate * length_ms / 1000.0)
t = np.arange(length_samples) / sample_rate
# 指數衰減包絡,衰減越快越「硬」
decay_rate = np.random.uniform(600, 1200) # 衰減速率隨機
envelope = np.exp(-decay_rate * t)
# 高頻噪音,模擬金屬撞擊的尖銳感
noise = np.random.randn(length_samples)
impact = noise * envelope
# 控制單一撞擊的音量
impact *= np.random.uniform(0.4, 0.9)
return impact
# 產生多個撞擊,時間點分佈在 0.5 秒內
# 為了模擬鋼珠逐漸減速,可以讓後面時間的撞擊較密集
times = np.linspace(0.0, duration, num_impacts + 2)[1:-1] # 避開完全 0 和完全結尾
# 稍微往後擠,形成「一開始快、後面密」的感覺
times = times**1.4 * (duration / (times[-1] ** 1.4))
for impact_time in times:
impact = generate_impact(sample_rate)
start_idx = int(impact_time * sample_rate)
end_idx = start_idx + len(impact)
if start_idx >= num_samples:
continue
if end_idx > num_samples:
impact = impact[: num_samples - start_idx]
end_idx = num_samples
audio[start_idx:end_idx] += impact
# 簡單做一下整體衰減包絡,避免尾端太突兀
t_all = np.linspace(0, duration, num_samples, endpoint=False)
global_env = np.exp(-t_all * 4.0) # 4 可調大一點讓衰減更快
audio *= global_env
# 避免削波:正規化到 -1.0 ~ 1.0 區間內,並留一點安全裕度
max_val = np.max(np.abs(audio))
if max_val > 0:
audio = audio / max_val * 0.9
# 轉成 16-bit PCM 並輸出為 WAV 檔
output_name = "roulette_ball.wav"
with wave.open(output_name, "w") as wf:
wf.setnchannels(1) # 單聲道
wf.setsampwidth(2) # 16-bit
wf.setframerate(sample_rate)
audio_int16 = (audio * 32767).astype(np.int16)
wf.writeframes(audio_int16.tobytes())
print(f"已輸出音效檔:{output_name}")