목표 길이까지 루프로 자동 생성하며, 연결부를 150ms 동안 Equal-Power 크로스페이드로 자연스럽게 잇고 마지막 2초 동안 Fade Out
마감합니다.
# ==========================================
# 8초 원본 → 정확히 15초 완성
# 마지막 2초 Fade Out
# ==========================================
TARGET_SECONDS = 18 # 최종 완성 목표 길이 (초)
PROMPT_SECONDS = 8 # 이전 오디오에서 참고할 뒷부분 길이 (초)
FADE_OUT_SECONDS = 2 # 마지막 마감 페이드아웃 길이 (초)
# 한 번에 요청할 생성 길이 (목표보다 조금 넉넉하게 생성)
GENERATE_SECONDS = 8
# 두 음원이 만나는 접합부 크로스페이드 구간 (150ms = 0.15초)
CROSSFADE_MS = 150
# MusicGen 모델의 초당 토큰 생성 수 (약 50 tokens/sec)
TOKENS_PER_SECOND = 50
def crossfade_join(a, b, sr, crossfade_ms=150):
"""
[함수] 두 오디오 a와 b를 150ms 동안 Equal-Power(cos/sin) 곡선으로 겹쳐 노이즈 없이 부드럽게 연결
"""
fade = int(sr * crossfade_ms / 1000)
fade = min(fade, len(a), len(b))
if fade <= 0:
return np.concatenate([a, b])
# Equal-power에 가까운 부드러운 fade 곡선 생성
x = np.linspace(0, np.pi / 2, fade)
fade_out = np.cos(x)
fade_in = np.sin(x)
# 겹치는 구간 믹싱
mixed = (
a[-fade:] * fade_out
+ b[:fade] * fade_in
)
# a의 앞부분 + 믹싱 구간 + b의 뒷부분 결합
return np.concatenate([
a[:-fade],
mixed,
b[fade:]
])
def generate_continuation(prompt_audio, seconds, description):
"""
[함수] 이전 오디오의 파형과 텍스트 프롬프트를 조건으로 후속 음악을 AI 생성
"""
# 오디오 및 텍스트 프롬프트 텐서화
inputs = processor(
audio=prompt_audio,
sampling_rate=SAMPLE_RATE,
text=[description],
padding=True,
return_tensors="pt"
)
inputs = {
k: v.to(device)
for k, v in inputs.items()
}
# 생성할 토큰 수 계산
max_new_tokens = int(seconds * TOKENS_PER_SECOND)
with torch.no_grad():
# MusicGen 모델 추론 실행 (샘플링 생성)
audio_values = model.generate(
**inputs,
do_sample=True,
guidance_scale=3.0,
temperature=1.0,
top_k=250,
max_new_tokens=max_new_tokens
)
# 생성된 오디오 텐서를 NumPy 1차원 배열로 변환
generated = (
audio_values[0, 0]
.detach()
.cpu()
.float()
.numpy()
)
# 생성 결과물에 입력 prompt가 포함되어 있는 경우 prompt 길이만큼 잘라내기
prompt_samples = len(prompt_audio)
if len(generated) > prompt_samples:
continuation = generated[prompt_samples:]
else:
continuation = generated
return continuation
# ==========================================
# 생성 시작: 난수 시드 고정 및 초기화
# ==========================================
torch.manual_seed(SEED)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(SEED)
# 원본 오디오 복사본으로 결과 버퍼 시작
result = original_audio.copy()
# 목표 샘플 수 계산
target_samples = int(TARGET_SECONDS * SAMPLE_RATE)
print(
"원본 길이:",
round(len(result) / SAMPLE_RATE, 3),
"초"
)
print(
"목표 길이:",
TARGET_SECONDS,
"초"
)
iteration = 1
# ==========================================
# 목표 길이에 도달할 때까지 반복 생성 및 접합
# ==========================================
while len(result) < target_samples:
current_seconds = len(result) / SAMPLE_RATE
remaining_seconds = TARGET_SECONDS - current_seconds
print()
print(
f"{iteration}번째 AI 생성 시작"
)
print(
"현재 길이:",
round(current_seconds, 3),
"초"
)
print(
"남은 길이:",
round(remaining_seconds, 3),
"초"
)
# 현재까지 만들어진 오디오의 마지막 구간을 새 prompt로 슬라이싱
prompt_samples = min(
int(PROMPT_SECONDS * SAMPLE_RATE),
len(result)
)
prompt = result[-prompt_samples:]
# 필요한 길이보다 약간 넉넉하게 생성 요청
seconds_to_generate = max(
GENERATE_SECONDS,
remaining_seconds + 1
)
# AI 음악 생성 실행
new_audio = generate_continuation(
prompt,
seconds_to_generate,
DESCRIPTION
)
print(
"AI가 실제 생성한 길이:",
round(len(new_audio) / SAMPLE_RATE, 3),
"초"
)
# 생성물이 비정상적으로 짧거나 비어 있으면 무한루프 방지용 탈출
if len(new_audio) < int(0.1 * SAMPLE_RATE):
print("생성된 오디오가 너무 짧습니다.")
break
# 기존 오디오 + 새로 생성된 오디오를 크로스페이드로 자연스럽게 접합
result = crossfade_join(
result,
new_audio,
SAMPLE_RATE,
crossfade_ms=CROSSFADE_MS
)
iteration += 1
# GPU 메모리 캐시 정리
if torch.cuda.is_available():
torch.cuda.empty_cache()
# ==========================================
# 정확한 목표 길이로 슬라이싱 및 패딩
# ==========================================
if len(result) >= target_samples:
# 목표 길이를 초과한 경우 정확히 타겟 샘플 수에서 컷
result = result[:target_samples]
else:
# 혹시 모자란 경우 무음(0)으로 패딩
missing = target_samples - len(result)
result = np.pad(
result,
(0, missing),
mode="constant"
)
# ==========================================
# 엔딩 2초 Fade Out 적용 (부드러운 마감)
# ==========================================
fade_samples = int(
FADE_OUT_SECONDS * SAMPLE_RATE
)
fade_samples = min(
fade_samples,
len(result)
)
# 처음에는 천천히, 끝으로 갈수록 부드럽게 감소하는 2차 제곱 감쇄 곡선
fade_curve = np.linspace(
1.0,
0.0,
fade_samples
) ** 2
# 오디오 뒷부분에 페이드아웃 게인 곱하기
result[-fade_samples:] *= fade_curve
# ==========================================
# 최종 결과 요약 및 오디오 플레이어 출력
# ==========================================
print()
print("==========================")
print(
"최종 샘플 수:",
len(result)
)
print(
"최종 실제 길이:",
len(result) / SAMPLE_RATE,
"초"
)
print(
"Fade Out:",
FADE_OUT_SECONDS,
"초"
)
print("==========================")
# 완성된 연장 음원 재생
display(
Audio(
result,
rate=SAMPLE_RATE
)
)