오늘도 파이썬과 동행하는 작곡가가 되어보기 시리즈입니다. 오늘은 단순한 음계에서 벗어나 초등시절 명작인 학교종을 연주(?) 해 볼게요.
그리고 기타 소리도 만들어 볼게요.
그리고 코드는 다음 주에 소개하겠지만 자꾸 소리가 Upgrade 되고 있어서 진짜로 내가 작곡가가 될 수도 있다는 희망을 다음 주까지 간직하기를 바라요
우주인이 연주하는 것 같죠?
그리고 다음은 앞 두 편의 코드인데, 외우려고도, 배우려고도 하지 말고, 골동품 보관하듯 잘 보관만 하세요. 아직까지는.... 여러분들이 작곡가가 되려 한다면 이 정도야 뭐 하루면....
# -*- coding: utf-8 -*-
"""
초보 작곡가를 위한 파이썬 코딩 2탄
- 피아노 톤으로 '학교종이 땡땡땡' 연주 WAV 생성
- 기타 톤(Karplus-Strong)으로 간단 작곡 WAV 생성
필요 패키지: numpy (없으면 pip install numpy)
"""
import wave, struct, random, math
import numpy as np
SR = 44100 # 샘플레이트
A4 = 440.0 # 표준 튜닝 (평균율)
EPS = 1e-9
# ---------------------------
# 1) 음고(주파수) 계산
# ---------------------------
NOTE_INDEX = {'도':0,'도#':1,'레':2,'레#':3,'미':4,'파':5,'파#':6,'솔':7,'솔#':8,'라':9,'라#':10,'시':11}
NAME_TO_EN = {'도':'C','레':'D','미':'E','파':'F','솔':'G','라':'A','시':'B'}
def freq_equal_temper(note_kr: str, octave: int, a4=440.0):
"""평균율: MIDI 번호로 변환 후 주파수 계산"""
# C4(도4)=261.63 기준: MIDI 60
semitone = NOTE_INDEX[note_kr]
# octave 기준 C계 산정
midi = 12*(octave+1) + semitone # MIDI 번호(참고: C4=60)
return a4 * (2.0 ** ((midi - 69)/12.0))
# 피타고라스(다장조 기준) 비율
PYTH_RATIO = {
'도': 1.0,
'레': 9/8,
'미': 81/64,
'파': 4/3,
'솔': 3/2,
'라': 27/16,
'시': 243/128
}
def freq_pythagorean(note_kr: str, octave: int, c4=261.63):
"""피타고라스 조율(다장조 기준) 단순 비율 사용"""
# 기준: C4(도4)=261.63
base = c4 * (2 ** (octave - 4)) # 옥타브 이동
ratio = PYTH_RATIO[note_kr]
return base * ratio
def get_freq(note_kr: str, octave: int, temperament='equal'):
return freq_equal_temper(note_kr, octave) if temperament=='equal' else freq_pythagorean(note_kr, octave)
# ---------------------------
# 2) ADSR & 합성기
# ---------------------------
def adsr_envelope(n_samples, attack=0.01, decay=0.12, sustain=0.6, release=0.12):
"""간단 ADSR(비율 기반)"""
if n_samples <= 0: return np.zeros(0)
a = int(n_samples*attack)
d = int(n_samples*decay)
r = int(n_samples*release)
s = max(0, n_samples - a - d - r)
env = np.zeros(n_samples)
# Attack: 0->1 선형
if a>0: env[:a] = np.linspace(0.0, 1.0, a, endpoint=False)
# Decay: 1->sustain
if d>0: env[a:a+d] = np.linspace(1.0, sustain, d, endpoint=False)
# Sustain
if s>0: env[a+d:a+d+s] = sustain
# Release: sustain->0
if r>0: env[a+d+s:] = np.linspace(sustain, 0.0, r, endpoint=True)
return env
def synth_piano(freq, duration_sec):
"""피아노 비슷한 톤: 배음 + 지수감쇠 + ADSR"""
n = max(1, int(SR*duration_sec))
t = np.arange(n)/SR
# 배음(하모닉) 간단 합성
y = (1.0*np.sin(2*math.pi*freq*t) +
0.6*np.sin(2*math.pi*2*freq*t) +
0.3*np.sin(2*math.pi*3*freq*t) +
0.2*np.sin(2*math.pi*4*freq*t))
# 지수형 감쇠 + ADSR
y *= np.exp(-3.0*t)
y *= adsr_envelope(n, attack=0.02, decay=0.15, sustain=0.5, release=0.15)
return y
def karplus_strong(freq, duration_sec, decay=0.996):
"""Karplus-Strong: 기타(현) 톤 근사"""
n_total = max(1, int(SR*duration_sec))
N = max(2, int(SR/max(freq, EPS))) # delay line 길이
# 초기 잡음 버퍼
buf = np.array([random.uniform(-1,1) for _ in range(N)], dtype=np.float32)
y = np.zeros(n_total, dtype=np.float32)
idx = 0
for i in range(n_total):
y[i] = buf[idx]
# 평균 후 감쇠
nxt = decay * 0.5 * (buf[idx] + buf[(idx+1) % N])
buf[idx] = nxt
idx = (idx + 1) % N
# 간단한 ADSR로 어택 강조
y = y.astype(np.float64)
y *= adsr_envelope(n_total, attack=0.005, decay=0.2, sustain=0.7, release=0.25)
return y
# ---------------------------
# 3) 렌더링 & 파일 저장
# ---------------------------
def write_wav(path, data):
"""[-1,1] float -> int16 WAV 저장"""
if data.size == 0: return
# 노멀라이즈(클리핑 방지)
peak = np.max(np.abs(data)) + EPS
data16 = np.int16((data/peak) * 32767)
with wave.open(path, 'wb') as wf:
wf.setnchannels(1)
wf.setsampwidth(2) # int16
wf.setframerate(SR)
wf.writeframes(data16.tobytes())
def beat_to_seconds(beats, bpm):
return (60.0/bpm)*beats
# ---------------------------
# 4) '학교종이 땡땡땡' 피아노 렌더
# ---------------------------
def render_school_bell_piano(bpm=120, temperament='equal'):
# 계이름(다장조, 옥타브는 노래 느낌상 4옥타브 중심)
melody = [
('솔',1),('솔',1),('라',1),('라',1),('솔',1),('솔',1),('미',2),
('솔',1),('솔',1),('미',1),('미',1),('레',2),
('솔',1),('솔',1),('라',1),('라',1),('솔',1),('솔',1),('미',2),
('솔',1),('미',1),('레',1),('미',1),('도',2)
]
track = np.zeros(0, dtype=np.float64)
for note_kr, beats in melody:
dur = beat_to_seconds(beats, bpm)
f = get_freq(note_kr, octave=4, temperament=temperament)
y = synth_piano(f, dur)
track = np.concatenate([track, y])
return track
# ---------------------------
# 5) 기타 톤으로 간단 작곡(스트럼 + 짧은 멜로디)
# ---------------------------
CHORD_NOTES = {
# 간단한 코드 보이싱(기타 느낌 범위)
'C': [('도',3), ('미',3), ('솔',3), ('도',4), ('미',4)],
'F': [('파',3), ('라',3), ('도',4), ('파',4)],
'G': [('솔',3), ('시',3), ('레',4), ('솔',4)]
}
def strum_chord(chord_name, duration_beats, bpm=100, temperament='equal', direction='down', gap_ms=18):
"""코드 스트럼: 각 음을 ms 간격으로 차례로 발음"""
notes = CHORD_NOTES[chord_name]
if direction=='up':
notes = list(reversed(notes))
base_dur = beat_to_seconds(duration_beats, bpm)
total = np.zeros(int(SR*base_dur)+SR//10, dtype=np.float64) # 여유 tail
for i, (note_kr, octv) in enumerate(notes):
f = get_freq(note_kr, octave=octv, temperament=temperament)
start = int((i * gap_ms/1000.0) * SR)
y = karplus_strong(f, base_dur*0.95) # tail 약간 짧게
end = start + y.size
if end > total.size:
# 크기 확장
pad = np.zeros(end - total.size, dtype=np.float64)
total = np.concatenate([total, pad])
total[start:end] += y
return total
def render_guitar_composition(bpm=100, temperament='equal'):
"""C–F–C–G–C 진행 + 짧은 멜로디(도-미-솔-라-솔)"""
prog = [('C',4), ('F',4), ('C',4), ('G',4), ('C',4)]
track = np.zeros(0, dtype=np.float64)
for ch, beats in prog:
part = strum_chord(ch, beats, bpm=bpm, temperament=temperament, direction='down', gap_ms=18)
track = np.concatenate([track, part])
# 간단한 톱라인 멜로디(펜타토닉 느낌) 오버레이
topline = [('도',4,1), ('미',4,1), ('솔',4,1), ('라',4,1), ('솔',4,2)]
mel = np.zeros(0, dtype=np.float64)
for note_kr, octv, beats in topline:
dur = beat_to_seconds(beats, bpm)
f = get_freq(note_kr, octave=octv, temperament=temperament)
y = karplus_strong(f, dur)
mel = np.concatenate([mel, y])
# 멜로디 길이를 코드 트랙에 맞추어 앞쪽에 얹기
mix = track.copy()
L = min(mix.size, mel.size)
mix[:L] += 0.9*mel[:L] # 멜로디 살짝 크게
return mix
# ---------------------------
# 6) 메인
# ---------------------------
if __name__ == "__main__":
# (A) 피아노로 '학교종이 땡땡땡'
piano_track = render_school_bell_piano(bpm=120, temperament='equal') # 'pythagorean'으로 바꿔도 OK
write_wav("school_bell_piano.wav", piano_track)
print("생성: school_bell_piano.wav")
# (B) 기타 톤으로 간단 작곡
guitar_track = render_guitar_composition(bpm=100, temperament='equal')
write_wav("guitar_intro_composition.wav", guitar_track)
print("생성: guitar_intro_composition.wav")