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}")