715 lines
26 KiB
Python

"""
8-bit 스타일 사운드 이펙트 생성기 v2
고전 게임풍 보스레이드용 효과음 - 퀄리티 향상 버전
"""
import numpy as np
import wave
import os
SAMPLE_RATE = 44100
OUTPUT_DIR = os.path.dirname(os.path.abspath(__file__))
def quantize_8bit(samples, levels=64):
"""8비트 양자화 (레벨 조절 가능)"""
quantized = np.round(samples * levels) / levels
return np.clip(quantized, -1.0, 1.0)
def square_wave(freq, duration, duty=0.5):
t = np.linspace(0, duration, int(SAMPLE_RATE * duration), endpoint=False)
return np.where((t * freq) % 1.0 < duty, 1.0, -1.0)
def pulse_wave(freq, duration, duty=0.25):
"""펄스파 - NES 특유의 음색"""
return square_wave(freq, duration, duty=duty)
def triangle_wave(freq, duration):
t = np.linspace(0, duration, int(SAMPLE_RATE * duration), endpoint=False)
return 2 * np.abs(2 * (t * freq - np.floor(t * freq + 0.5))) - 1
def noise(duration):
n = int(SAMPLE_RATE * duration)
return np.random.uniform(-1, 1, n)
def periodic_noise(freq, duration):
"""주기적 노이즈 - NES 스타일 메탈릭 노이즈"""
t = np.linspace(0, duration, int(SAMPLE_RATE * duration), endpoint=False)
raw = np.random.uniform(-1, 1, int(SAMPLE_RATE * duration))
# 주파수에 맞춰 샘플 앤 홀드
hold_samples = max(1, int(SAMPLE_RATE / freq))
for i in range(0, len(raw), hold_samples):
raw[i:i + hold_samples] = raw[i]
return raw
def adsr(samples, attack=0.01, decay=0.1, sustain=0.7, release=0.1):
"""부드러운 ADSR 엔벨로프 (지수 커브)"""
n = len(samples)
env = np.zeros(n)
a = int(attack * SAMPLE_RATE)
d = int(decay * SAMPLE_RATE)
r = int(release * SAMPLE_RATE)
s_end = max(0, n - r)
# Attack (지수형 상승)
if a > 0:
a = min(a, n)
env[:a] = np.power(np.linspace(0, 1, a), 0.5)
# Decay (지수형 감쇠)
if d > 0:
d_end = min(a + d, s_end)
d_len = d_end - a
if d_len > 0:
env[a:d_end] = sustain + (1 - sustain) * np.power(np.linspace(1, 0, d_len), 1.5)
# Sustain
if a + d < s_end:
env[a + d:s_end] = sustain
# Release (지수형)
if r > 0 and s_end < n:
r_len = n - s_end
start_val = env[max(0, s_end - 1)] if s_end > 0 else sustain
env[s_end:] = start_val * np.power(np.linspace(1, 0, r_len), 2.0)
return samples * env
def pitch_sweep(start_freq, end_freq, duration, wave_type='square', exponential=True):
"""피치 스윕 (지수형/선형)"""
n = int(SAMPLE_RATE * duration)
t = np.linspace(0, duration, n, endpoint=False)
if exponential and start_freq > 0 and end_freq > 0:
freqs = start_freq * np.power(end_freq / start_freq, t / duration)
else:
freqs = np.linspace(start_freq, end_freq, n)
phase = np.cumsum(2 * np.pi * freqs / SAMPLE_RATE)
if wave_type == 'square':
return np.sign(np.sin(phase))
elif wave_type == 'triangle':
return 2 * np.abs(2 * (phase / (2 * np.pi) - np.floor(phase / (2 * np.pi) + 0.5))) - 1
else:
return np.sin(phase)
def delay_effect(samples, delay_time=0.08, feedback=0.4, mix_amount=0.3):
"""에코/딜레이"""
delay_samples = int(delay_time * SAMPLE_RATE)
result = samples.copy()
delayed = np.zeros(len(samples))
if delay_samples < len(samples):
delayed[delay_samples:] = samples[:-delay_samples] * feedback
# 2차 에코
if delay_samples * 2 < len(samples):
delayed[delay_samples * 2:] += samples[:-delay_samples * 2] * feedback * feedback
return result * (1 - mix_amount) + (result + delayed) * mix_amount
def chord(freqs, duration, wave_func=square_wave, volumes=None):
"""화음 생성"""
if volumes is None:
volumes = [1.0 / len(freqs)] * len(freqs)
result = np.zeros(int(SAMPLE_RATE * duration))
for f, v in zip(freqs, volumes):
result += wave_func(f, duration) * v
return np.clip(result, -1.0, 1.0)
def vibrato(freq, duration, vib_freq=6, vib_depth=0.02, wave_func=square_wave):
"""비브라토 (주파수 흔들림)"""
n = int(SAMPLE_RATE * duration)
t = np.linspace(0, duration, n, endpoint=False)
mod_freqs = freq * (1 + vib_depth * np.sin(2 * np.pi * vib_freq * t))
phase = np.cumsum(2 * np.pi * mod_freqs / SAMPLE_RATE)
return np.sign(np.sin(phase))
def concat(*arrays):
return np.concatenate(arrays)
def mix_layers(*layers):
"""볼륨 가중 믹스 - (array, volume) 튜플 리스트"""
max_len = max(len(a) for a, _ in layers)
result = np.zeros(max_len)
for arr, vol in layers:
result[:len(arr)] += arr * vol
return np.clip(result, -1.0, 1.0)
def silence(duration):
return np.zeros(int(SAMPLE_RATE * duration))
def save_wav(filename, samples, sample_rate=SAMPLE_RATE):
filepath = os.path.join(OUTPUT_DIR, filename)
samples = np.clip(samples * 0.85, -1.0, 1.0)
int_samples = (samples * 32767).astype(np.int16)
with wave.open(filepath, 'w') as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(sample_rate)
wf.writeframes(int_samples.tobytes())
print(f" Created: {filename} ({len(samples)/sample_rate:.2f}s)")
# ============================================================
# SFX 생성
# ============================================================
def gen_hit_normal():
"""일반 피격 - 펀치감 있는 타격"""
# 레이어 1: 강한 어택 (높은 사각파)
atk = square_wave(660, 0.025) * 0.9
atk = adsr(atk, attack=0.001, decay=0.02, sustain=0.0, release=0.005)
# 레이어 2: 노이즈 버스트 (타격 질감)
n = periodic_noise(2000, 0.04) * 0.7
n = adsr(n, attack=0.001, decay=0.025, sustain=0.0, release=0.015)
# 레이어 3: 바디 (피치 다운 펀치)
body = pitch_sweep(500, 80, 0.1, exponential=True)
body = adsr(body, attack=0.001, decay=0.06, sustain=0.15, release=0.04) * 0.6
# 레이어 4: 서브 베이스 쿵
sub = triangle_wave(60, 0.08) * 0.4
sub = adsr(sub, attack=0.001, decay=0.05, sustain=0.0, release=0.03)
result = mix_layers(
(atk, 1.0),
(n, 0.8),
(body, 0.7),
(sub, 0.5)
)
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.04, feedback=0.2, mix_amount=0.15)
save_wav("sfx_hit_normal.wav", result)
def gen_hit_critical():
"""크리티컬 - 더 강하고 화려한 타격"""
# 레이어 1: 이중 어택 (옥타브)
atk1 = square_wave(880, 0.02) * 0.9
atk2 = pulse_wave(1760, 0.02, duty=0.25) * 0.5
atk = mix_layers((atk1, 1.0), (atk2, 0.6))
atk = adsr(atk, attack=0.001, decay=0.015, sustain=0.0, release=0.005)
# 레이어 2: 크런치 노이즈
n = noise(0.06) * 0.8
n = adsr(n, attack=0.001, decay=0.04, sustain=0.1, release=0.02)
# 레이어 3: 피치 스윕 (화려한 하강)
sweep = pitch_sweep(2000, 150, 0.18, exponential=True)
sweep = adsr(sweep, attack=0.001, decay=0.12, sustain=0.1, release=0.06) * 0.5
# 레이어 4: 파워 코드 울림
ring = chord([220, 330, 440], 0.15)
ring = adsr(ring, attack=0.005, decay=0.08, sustain=0.15, release=0.06) * 0.35
# 레이어 5: 서브 베이스 임팩트
sub = pitch_sweep(120, 35, 0.12, exponential=True)
sub = adsr(sub, attack=0.001, decay=0.08, sustain=0.0, release=0.04) * 0.5
# 레이어 6: 메탈릭 링
metal = periodic_noise(4000, 0.1) * 0.3
metal = adsr(metal, attack=0.001, decay=0.06, sustain=0.05, release=0.04)
part1_len = max(len(atk), len(n))
part1 = mix_layers((atk, 1.0), (n, 0.7))
result = concat(
part1,
mix_layers((sweep, 0.8), (ring, 0.5), (sub, 0.6), (metal, 0.4))
)
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.06, feedback=0.3, mix_amount=0.2)
save_wav("sfx_hit_critical.wav", result)
def gen_hit_miss():
"""빗나감 - 공기를 가르는 휙"""
# 필터드 노이즈 스윕 (고→저)
swoosh = pitch_sweep(1600, 300, 0.2, wave_type='sin', exponential=True)
swoosh = adsr(swoosh, attack=0.01, decay=0.12, sustain=0.05, release=0.07) * 0.3
# 노이즈 레이어 (바람 질감)
n = noise(0.2) * 0.25
t_env = np.linspace(0, 1, len(n))
n = n * np.exp(-3 * t_env) # 지수 감쇠
n = adsr(n, attack=0.015, decay=0.1, sustain=0.05, release=0.05)
# 가벼운 톤
tone = triangle_wave(800, 0.05) * 0.15
tone = adsr(tone, attack=0.005, decay=0.03, sustain=0.0, release=0.02)
result = mix_layers((swoosh, 1.0), (n, 0.8), (tone, 0.5))
result = quantize_8bit(result, levels=64)
save_wav("sfx_hit_miss.wav", result)
def gen_boss_appear():
"""보스 등장 - 위압적인 경고 + 등장 임팩트"""
# 파트 1: 저음 럼블 빌드업
rumble = pitch_sweep(40, 100, 0.5, exponential=True)
rumble = adsr(rumble, attack=0.15, decay=0.1, sustain=0.7, release=0.1) * 0.5
rumble_noise = noise(0.5) * 0.15
rumble_noise = adsr(rumble_noise, attack=0.1, decay=0.2, sustain=0.3, release=0.1)
rumble_part = mix_layers((rumble, 1.0), (rumble_noise, 0.5))
# 파트 2: WARNING 사이렌 (3회, 점점 높아짐)
siren_parts = []
for i, freq in enumerate([440, 523, 660]):
beep = square_wave(freq, 0.12) * 0.6
# 하모닉스 추가
beep_h = pulse_wave(freq * 2, 0.12, duty=0.25) * 0.2
combined = mix_layers((beep, 1.0), (beep_h, 0.5))
combined = adsr(combined, attack=0.005, decay=0.03, sustain=0.5, release=0.03)
siren_parts.append(combined)
siren_parts.append(silence(0.06))
siren = concat(*siren_parts)
# 파트 3: 등장 임팩트 (강한 쿵)
impact_boom = pitch_sweep(250, 30, 0.3, exponential=True)
impact_boom = adsr(impact_boom, attack=0.001, decay=0.2, sustain=0.05, release=0.1) * 0.8
impact_noise = noise(0.2) * 0.5
impact_noise = adsr(impact_noise, attack=0.001, decay=0.12, sustain=0.0, release=0.08)
impact_ring = chord([55, 82.5, 110], 0.3)
impact_ring = adsr(impact_ring, attack=0.01, decay=0.2, sustain=0.1, release=0.1) * 0.3
impact = mix_layers(
(impact_boom, 1.0),
(impact_noise, 0.6),
(impact_ring, 0.4)
)
impact = delay_effect(impact, delay_time=0.1, feedback=0.35, mix_amount=0.25)
result = concat(rumble_part, siren, impact)
result = quantize_8bit(result, levels=64)
save_wav("sfx_boss_appear.wav", result)
def gen_boss_rage():
"""분노 모드 - 포효 + 파워업"""
dur = 0.7
# 레이어 1: 포효 (저→고 스윕 + 트레몰로)
roar = pitch_sweep(60, 400, dur * 0.6, exponential=True)
t_roar = np.linspace(0, dur * 0.6, len(roar))
roar = roar * (0.6 + 0.4 * np.sin(2 * np.pi * 12 * t_roar)) # 빠른 트레몰로
roar = adsr(roar, attack=0.03, decay=0.2, sustain=0.6, release=0.15) * 0.6
# 레이어 2: 디스토션 노이즈
n = periodic_noise(500, dur * 0.6) * 0.4
t_n = np.linspace(0, 1, len(n))
n = n * (0.3 + 0.7 * t_n) # 점점 커짐
n = adsr(n, attack=0.05, decay=0.15, sustain=0.5, release=0.2)
# 레이어 3: 파워업 상승음
powerup = pitch_sweep(200, 1200, dur * 0.5, exponential=True)
powerup = adsr(powerup, attack=0.05, decay=0.15, sustain=0.4, release=0.15) * 0.3
# 마무리: 강한 임팩트 + 드론
end_impact = pitch_sweep(200, 50, 0.15, exponential=True)
end_impact = adsr(end_impact, attack=0.001, decay=0.1, sustain=0.0, release=0.05) * 0.7
end_drone = vibrato(80, 0.2, vib_freq=8, vib_depth=0.05)
end_drone = adsr(end_drone, attack=0.01, decay=0.1, sustain=0.3, release=0.08) * 0.35
main = mix_layers((roar, 1.0), (n, 0.6), (powerup, 0.5))
ending = mix_layers((end_impact, 1.0), (end_drone, 0.6))
result = concat(main, ending)
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.08, feedback=0.3, mix_amount=0.2)
save_wav("sfx_boss_rage.wav", result)
def gen_boss_death():
"""보스 사망 - 다단 폭발 + 소멸"""
# 폭발 1 (첫 번째 강한 폭발)
exp1_boom = pitch_sweep(300, 25, 0.25, exponential=True)
exp1_boom = adsr(exp1_boom, attack=0.001, decay=0.18, sustain=0.05, release=0.07) * 0.8
exp1_noise = noise(0.25) * 0.7
exp1_noise = adsr(exp1_noise, attack=0.001, decay=0.15, sustain=0.1, release=0.1)
exp1 = mix_layers((exp1_boom, 1.0), (exp1_noise, 0.7))
# 폭발 2 (연쇄 폭발)
exp2_boom = pitch_sweep(400, 40, 0.2, exponential=True)
exp2_boom = adsr(exp2_boom, attack=0.001, decay=0.12, sustain=0.0, release=0.08) * 0.6
exp2_noise = noise(0.15) * 0.5
exp2_noise = adsr(exp2_noise, attack=0.001, decay=0.1, sustain=0.0, release=0.05)
exp2 = mix_layers((exp2_boom, 1.0), (exp2_noise, 0.6))
# 소멸 (피치 다운 + 디졸브)
dissolve = pitch_sweep(1000, 60, 0.6, exponential=True)
dissolve = adsr(dissolve, attack=0.01, decay=0.35, sustain=0.15, release=0.25) * 0.4
# 잔향 삼각파
reverb_tone = triangle_wave(80, 0.4) * 0.25
reverb_tone = adsr(reverb_tone, attack=0.05, decay=0.25, sustain=0.05, release=0.1)
# 마지막 "퐁" (소멸 완료)
final_pop = pitch_sweep(600, 200, 0.1, exponential=True)
final_pop = adsr(final_pop, attack=0.001, decay=0.06, sustain=0.0, release=0.04) * 0.3
dissolve_part = mix_layers((dissolve, 1.0), (reverb_tone, 0.5))
dissolve_part = delay_effect(dissolve_part, delay_time=0.12, feedback=0.4, mix_amount=0.3)
result = concat(exp1, silence(0.05), exp2, dissolve_part, final_pop)
result = quantize_8bit(result, levels=64)
save_wav("sfx_boss_death.wav", result)
def gen_victory():
"""승리 팡파레 - 포켓몬 스타일 승리 징글"""
def note(freq, dur, vol=0.5, duty=0.5):
w = square_wave(freq, dur, duty=duty) * vol
# 옥타브 위 하모닉스
h = pulse_wave(freq * 2, dur, duty=0.25) * vol * 0.15
combined = mix_layers((w, 1.0), (h, 0.5))
return adsr(combined, attack=0.005, decay=0.03, sustain=0.7, release=0.03)
p = silence(0.025) # 노트 간 갭
# 도입부: 팡! (짧은 팡파레 코드)
fanfare = chord([523, 659, 784], 0.08)
fanfare = adsr(fanfare, attack=0.001, decay=0.04, sustain=0.4, release=0.03) * 0.6
# 멜로디: C-E-G (빠르게) → C5 (길게)
c4 = note(523, 0.1, 0.55)
e4 = note(659, 0.1, 0.55)
g4 = note(784, 0.1, 0.55)
c5_long = note(1047, 0.25, 0.6)
# 후반: G5-E5-C5 (아르페지오 마무리)
g5 = note(1568, 0.07, 0.35, duty=0.25)
e5 = note(1319, 0.07, 0.35, duty=0.25)
c5_end = note(1047, 0.07, 0.35, duty=0.25)
# 마지막 코드 (C 메이저 울림)
final = chord([523, 659, 784, 1047], 0.35)
final = adsr(final, attack=0.005, decay=0.1, sustain=0.5, release=0.2) * 0.5
# 베이스 라인 (삼각파)
bass1 = triangle_wave(131, 0.1) * 0.25 # C3
bass1 = adsr(bass1, attack=0.005, decay=0.05, sustain=0.3, release=0.02)
bass2 = triangle_wave(165, 0.1) * 0.25 # E3
bass2 = adsr(bass2, attack=0.005, decay=0.05, sustain=0.3, release=0.02)
bass3 = triangle_wave(196, 0.1) * 0.25 # G3
bass3 = adsr(bass3, attack=0.005, decay=0.05, sustain=0.3, release=0.02)
bass_final = triangle_wave(131, 0.35) * 0.3 # C3 길게
bass_final = adsr(bass_final, attack=0.005, decay=0.1, sustain=0.4, release=0.2)
# 멜로디 조합
melody = concat(fanfare, p, c4, p, e4, p, g4, p, c5_long, p, g5, e5, c5_end, p, final)
# 베이스 조합 (멜로디에 맞춤)
bass = concat(
silence(len(fanfare) / SAMPLE_RATE + 0.025),
bass1, p, bass2, p, bass3, p,
silence(len(c5_long) / SAMPLE_RATE + 0.025),
silence((len(g5) + len(e5) + len(c5_end)) / SAMPLE_RATE + 0.025),
bass_final
)
# 길이 맞추기
max_len = max(len(melody), len(bass))
if len(melody) < max_len:
melody = np.pad(melody, (0, max_len - len(melody)))
if len(bass) < max_len:
bass = np.pad(bass, (0, max_len - len(bass)))
result = mix_layers((melody, 1.0), (bass, 0.6))
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.07, feedback=0.25, mix_amount=0.15)
save_wav("sfx_victory.wav", result)
def gen_damage_popup():
"""데미지 팝업 - 기분 좋은 팝"""
# 메인 팝 (피치 업)
pop = pitch_sweep(800, 1400, 0.05, exponential=True)
pop = adsr(pop, attack=0.001, decay=0.03, sustain=0.1, release=0.02) * 0.6
# 하모닉
pop_h = pitch_sweep(1600, 2800, 0.04, wave_type='sin', exponential=True)
pop_h = adsr(pop_h, attack=0.001, decay=0.02, sustain=0.0, release=0.02) * 0.2
# 노이즈 틱
tick = periodic_noise(6000, 0.02) * 0.25
tick = adsr(tick, attack=0.001, decay=0.01, sustain=0.0, release=0.01)
# 잔향 톤
ring = triangle_wave(1200, 0.06) * 0.15
ring = adsr(ring, attack=0.005, decay=0.03, sustain=0.05, release=0.02)
result = mix_layers((pop, 1.0), (pop_h, 0.6), (tick, 0.5), (ring, 0.4))
result = quantize_8bit(result, levels=64)
save_wav("sfx_damage_popup.wav", result)
def gen_phase_transition():
"""페이즈 전환 - 텐션 빌드업 → 임팩트"""
# 파트 1: 빌드업 (0.5초)
# 상승 스윕
rising = pitch_sweep(80, 1200, 0.5, exponential=True)
rising = adsr(rising, attack=0.05, decay=0.05, sustain=0.9, release=0.05) * 0.4
# 빌드업 노이즈 (점점 커짐)
build_noise = noise(0.5) * 0.3
t_bn = np.linspace(0, 1, len(build_noise))
build_noise = build_noise * np.power(t_bn, 2) # 지수적 증가
# 빌드업 트레몰로 (점점 빨라짐)
trem_freq = np.linspace(4, 30, len(rising)) # 4Hz → 30Hz로 가속
t_trem = np.linspace(0, 0.5, len(rising))
trem = square_wave(300, 0.5) * 0.3
trem_env = 0.5 + 0.5 * np.sin(np.cumsum(2 * np.pi * trem_freq / SAMPLE_RATE))
trem = trem * trem_env
trem = adsr(trem, attack=0.1, decay=0.1, sustain=0.8, release=0.05)
buildup = mix_layers((rising, 0.8), (build_noise, 0.5), (trem, 0.4))
# 파트 2: 임팩트 (전환 순간)
impact = pitch_sweep(500, 50, 0.2, exponential=True)
impact = adsr(impact, attack=0.001, decay=0.12, sustain=0.05, release=0.08) * 0.8
impact_noise = noise(0.15) * 0.5
impact_noise = adsr(impact_noise, attack=0.001, decay=0.08, sustain=0.0, release=0.07)
# 임팩트 코드
impact_chord = chord([55, 82.5, 110, 165], 0.2)
impact_chord = adsr(impact_chord, attack=0.001, decay=0.12, sustain=0.1, release=0.08) * 0.4
impact_part = mix_layers(
(impact, 1.0),
(impact_noise, 0.6),
(impact_chord, 0.5)
)
# 파트 3: 잔향 드론
drone = vibrato(65, 0.3, vib_freq=6, vib_depth=0.03)
drone = adsr(drone, attack=0.01, decay=0.2, sustain=0.15, release=0.1) * 0.3
impact_with_echo = delay_effect(impact_part, delay_time=0.1, feedback=0.35, mix_amount=0.25)
result = concat(buildup, impact_with_echo, drone)
result = quantize_8bit(result, levels=64)
save_wav("sfx_phase_transition.wav", result)
def gen_boss_breath():
"""보스 브레스 발사 - 에너지 차징 → 발사"""
# 차징 (고주파 수렴)
charge = pitch_sweep(1500, 400, 0.3, exponential=True)
t_ch = np.linspace(0, 1, len(charge))
charge = charge * (0.3 + 0.7 * np.power(t_ch, 0.5))
charge = adsr(charge, attack=0.05, decay=0.05, sustain=0.9, release=0.02) * 0.4
charge_noise = periodic_noise(3000, 0.3) * 0.2
charge_noise = charge_noise * np.power(t_ch, 1.5)
charge_part = mix_layers((charge, 1.0), (charge_noise, 0.5))
# 발사 (폭발적 방출)
blast = pitch_sweep(300, 60, 0.4, exponential=True)
blast = adsr(blast, attack=0.001, decay=0.25, sustain=0.1, release=0.15) * 0.8
blast_noise = noise(0.35) * 0.6
blast_noise = adsr(blast_noise, attack=0.001, decay=0.2, sustain=0.1, release=0.15)
sizzle = periodic_noise(5000, 0.3) * 0.3
sizzle = adsr(sizzle, attack=0.001, decay=0.15, sustain=0.1, release=0.15)
blast_part = mix_layers((blast, 1.0), (blast_noise, 0.6), (sizzle, 0.4))
blast_part = delay_effect(blast_part, delay_time=0.06, feedback=0.3, mix_amount=0.2)
result = concat(charge_part, blast_part)
result = quantize_8bit(result, levels=64)
save_wav("sfx_boss_breath.wav", result)
def gen_boss_slam():
"""보스 내려찍기 - 무거운 임팩트"""
windup = pitch_sweep(200, 600, 0.08, exponential=True)
windup = adsr(windup, attack=0.01, decay=0.04, sustain=0.3, release=0.03) * 0.3
impact = pitch_sweep(150, 20, 0.25, exponential=True)
impact = adsr(impact, attack=0.001, decay=0.18, sustain=0.05, release=0.07) * 0.9
sub = triangle_wave(35, 0.2) * 0.6
sub = adsr(sub, attack=0.001, decay=0.15, sustain=0.0, release=0.05)
crumble = noise(0.3) * 0.7
crumble = adsr(crumble, attack=0.001, decay=0.15, sustain=0.15, release=0.15)
debris1 = pitch_sweep(400, 150, 0.06, exponential=True) * 0.2
debris1 = adsr(debris1, attack=0.001, decay=0.04, sustain=0.0, release=0.02)
debris2 = pitch_sweep(500, 200, 0.05, exponential=True) * 0.15
debris2 = adsr(debris2, attack=0.001, decay=0.03, sustain=0.0, release=0.02)
impact_part = mix_layers((impact, 1.0), (sub, 0.7), (crumble, 0.5))
impact_part = delay_effect(impact_part, delay_time=0.08, feedback=0.35, mix_amount=0.25)
result = concat(windup, impact_part, silence(0.05), debris1, silence(0.03), debris2)
result = quantize_8bit(result, levels=64)
save_wav("sfx_boss_slam.wav", result)
def gen_boss_charge():
"""보스 차징 예고 - 위협적인 경고음"""
beep1 = square_wave(880, 0.06) * 0.5
beep1 = adsr(beep1, attack=0.003, decay=0.02, sustain=0.4, release=0.02)
beep2 = square_wave(880, 0.06) * 0.5
beep2 = adsr(beep2, attack=0.003, decay=0.02, sustain=0.4, release=0.02)
charge = pitch_sweep(100, 900, 0.4, exponential=True)
t_ch = np.linspace(0, 1, len(charge))
trem_freq = np.linspace(4, 25, len(charge))
trem_env = 0.5 + 0.5 * np.sin(np.cumsum(2 * np.pi * trem_freq / SAMPLE_RATE))
charge = charge * trem_env
charge = adsr(charge, attack=0.05, decay=0.05, sustain=0.8, release=0.05) * 0.5
ch_noise = periodic_noise(2000, 0.4) * 0.25
ch_noise = ch_noise * np.power(t_ch, 1.5)
hi = pulse_wave(1200, 0.4, duty=0.125) * 0.15
hi = hi * np.power(t_ch, 2)
hi = adsr(hi, attack=0.1, decay=0.1, sustain=0.7, release=0.05)
charge_part = mix_layers((charge, 1.0), (ch_noise, 0.5), (hi, 0.4))
result = concat(beep1, silence(0.04), beep2, silence(0.06), charge_part)
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.05, feedback=0.2, mix_amount=0.15)
save_wav("sfx_boss_charge.wav", result)
def gen_player_hit():
"""아바타 피격 - 플레이어가 맞는 소리"""
shock = pitch_sweep(1800, 400, 0.08, exponential=True)
shock = adsr(shock, attack=0.001, decay=0.05, sustain=0.1, release=0.03) * 0.6
hit = square_wave(300, 0.03) * 0.7
hit = adsr(hit, attack=0.001, decay=0.02, sustain=0.0, release=0.01)
n = noise(0.06) * 0.5
n = adsr(n, attack=0.001, decay=0.04, sustain=0.0, release=0.02)
wobble = vibrato(250, 0.12, vib_freq=20, vib_depth=0.08)
wobble = adsr(wobble, attack=0.01, decay=0.06, sustain=0.15, release=0.05) * 0.3
sub = triangle_wave(80, 0.06) * 0.3
sub = adsr(sub, attack=0.001, decay=0.04, sustain=0.0, release=0.02)
first = mix_layers((shock, 0.8), (hit, 1.0), (n, 0.6), (sub, 0.5))
result = concat(first, wobble)
result = quantize_8bit(result, levels=64)
save_wav("sfx_player_hit.wav", result)
def gen_player_death():
"""아바타 사망 - 슬픈 하강음"""
def sad_note(freq, dur):
w = square_wave(freq, dur) * 0.45
h = triangle_wave(freq, dur) * 0.2
combined = mix_layers((w, 1.0), (h, 0.5))
return adsr(combined, attack=0.005, decay=0.05, sustain=0.5, release=0.05)
p = silence(0.03)
n1 = sad_note(784, 0.15) # G5
n2 = sad_note(659, 0.15) # E5
n3 = sad_note(523, 0.15) # C5
n4 = sad_note(415, 0.35) # Ab4
melody = concat(n1, p, n2, p, n3, p, n4)
bass = pitch_sweep(200, 60, 0.8, wave_type='triangle', exponential=True)
bass = adsr(bass, attack=0.01, decay=0.4, sustain=0.2, release=0.3) * 0.25
max_len = max(len(melody), len(bass))
if len(melody) < max_len:
melody = np.pad(melody, (0, max_len - len(melody)))
if len(bass) < max_len:
bass = np.pad(bass, (0, max_len - len(bass)))
result = mix_layers((melody, 1.0), (bass, 0.5))
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.1, feedback=0.4, mix_amount=0.3)
save_wav("sfx_player_death.wav", result)
def gen_game_over():
"""게임 오버 - 무거운 패배 징글"""
doom1 = chord([196, 233, 294], 0.25) # Gm
doom1 = adsr(doom1, attack=0.01, decay=0.08, sustain=0.6, release=0.06) * 0.5
doom2 = chord([175, 220, 262], 0.25) # F
doom2 = adsr(doom2, attack=0.01, decay=0.08, sustain=0.6, release=0.06) * 0.5
doom3 = chord([165, 196, 247], 0.25) # Em
doom3 = adsr(doom3, attack=0.01, decay=0.08, sustain=0.6, release=0.06) * 0.5
final = chord([131, 156, 196], 0.6) # Cm
final = adsr(final, attack=0.01, decay=0.2, sustain=0.4, release=0.35) * 0.55
p = silence(0.04)
melody = concat(doom1, p, doom2, p, doom3, p, final)
bass_drone = pitch_sweep(80, 40, 1.5, wave_type='triangle', exponential=True)
bass_drone = adsr(bass_drone, attack=0.05, decay=0.5, sustain=0.3, release=0.5) * 0.3
dark_noise = noise(1.5) * 0.1
dark_noise = adsr(dark_noise, attack=0.1, decay=0.5, sustain=0.15, release=0.5)
max_len = max(len(melody), len(bass_drone), len(dark_noise))
if len(melody) < max_len:
melody = np.pad(melody, (0, max_len - len(melody)))
if len(bass_drone) < max_len:
bass_drone = np.pad(bass_drone, (0, max_len - len(bass_drone)))
if len(dark_noise) < max_len:
dark_noise = np.pad(dark_noise, (0, max_len - len(dark_noise)))
result = mix_layers((melody, 1.0), (bass_drone, 0.5), (dark_noise, 0.4))
result = quantize_8bit(result, levels=64)
result = delay_effect(result, delay_time=0.12, feedback=0.4, mix_amount=0.3)
save_wav("sfx_game_over.wav", result)
# ============================================================
if __name__ == "__main__":
print("Generating 8-bit SFX v2 (enhanced quality)...")
print(f"Output: {OUTPUT_DIR}\n")
gen_hit_normal()
gen_hit_critical()
gen_hit_miss()
gen_boss_appear()
gen_boss_rage()
gen_boss_death()
gen_victory()
gen_damage_popup()
gen_phase_transition()
gen_boss_breath()
gen_boss_slam()
gen_boss_charge()
gen_player_hit()
gen_player_death()
gen_game_over()
print("\nDone! 15 SFX files generated (v2).")