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