Initial commit: AuroraMusic monorepo
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import hashlib
|
||||
import uuid
|
||||
import wave
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .base import GenerationRequest, GenerationResult, MusicGenerationProvider
|
||||
|
||||
SAMPLE_RATE = 32000
|
||||
|
||||
# A few simple chord progressions (root frequencies in Hz) so different
|
||||
# prompts audibly differ instead of producing the same tone every time.
|
||||
_PROGRESSIONS = [
|
||||
[261.63, 329.63, 392.00, 261.63], # C major-ish
|
||||
[220.00, 261.63, 329.63, 220.00], # A minor-ish
|
||||
[196.00, 246.94, 293.66, 196.00], # G-ish
|
||||
[174.61, 220.00, 261.63, 174.61], # F-ish
|
||||
]
|
||||
|
||||
|
||||
class MockMusicGenerationProvider(MusicGenerationProvider):
|
||||
"""Fast, dependency-light stand-in for a real model.
|
||||
|
||||
Synthesizes a short procedurally-generated audio clip (chord pad +
|
||||
simple melody, with a gentle "vocal-ish" hum layer when mode is
|
||||
"vocals") so the rest of the product — jobs, storage, playback,
|
||||
library, feed — can be built and verified before wiring a real,
|
||||
much slower/heavier local model.
|
||||
"""
|
||||
|
||||
def generate(self, request: GenerationRequest, output_dir: Path) -> GenerationResult:
|
||||
seed = int(hashlib.sha256(request.prompt.encode()).hexdigest(), 16) % (2**32)
|
||||
rng = np.random.default_rng(seed)
|
||||
progression = _PROGRESSIONS[seed % len(_PROGRESSIONS)]
|
||||
|
||||
duration = max(5, min(request.duration_seconds, 30))
|
||||
t = np.linspace(0, duration, int(SAMPLE_RATE * duration), endpoint=False)
|
||||
audio = np.zeros_like(t)
|
||||
|
||||
bar_len = duration / len(progression)
|
||||
for i, freq in enumerate(progression):
|
||||
start = int(i * bar_len * SAMPLE_RATE)
|
||||
end = int((i + 1) * bar_len * SAMPLE_RATE)
|
||||
seg_t = t[start:end] - t[start]
|
||||
pad = (
|
||||
0.18 * np.sin(2 * np.pi * freq * seg_t)
|
||||
+ 0.12 * np.sin(2 * np.pi * freq * 1.5 * seg_t)
|
||||
+ 0.08 * np.sin(2 * np.pi * freq * 2 * seg_t)
|
||||
)
|
||||
envelope = np.clip(np.minimum(seg_t, bar_len - seg_t) * 4, 0, 1)
|
||||
audio[start:end] += pad * envelope
|
||||
|
||||
# simple plucked melody riding on top, tempo/notes derived from the prompt
|
||||
note_len = 60 / (90 + seed % 60)
|
||||
n_notes = int(duration / note_len)
|
||||
for i in range(n_notes):
|
||||
start = int(i * note_len * SAMPLE_RATE)
|
||||
end = min(int((i + 1) * note_len * SAMPLE_RATE), len(t))
|
||||
if end <= start:
|
||||
continue
|
||||
freq = progression[i % len(progression)] * rng.choice([1, 1.25, 1.5, 2])
|
||||
seg_t = t[start:end] - t[start]
|
||||
note = 0.1 * np.sin(2 * np.pi * freq * seg_t) * np.exp(-3 * seg_t)
|
||||
audio[start:end] += note
|
||||
|
||||
if request.mode == "vocals" and request.lyrics:
|
||||
words = request.lyrics.split()
|
||||
if words:
|
||||
syl_len = duration / max(len(words), 1)
|
||||
for i, _word in enumerate(words):
|
||||
start = int(i * syl_len * SAMPLE_RATE)
|
||||
end = min(int((i + 1) * syl_len * SAMPLE_RATE), len(t))
|
||||
if end <= start:
|
||||
continue
|
||||
base = progression[i % len(progression)] * 2
|
||||
vib = 1 + 0.01 * np.sin(2 * np.pi * 5 * (t[start:end] - t[start]))
|
||||
seg_t = t[start:end] - t[start]
|
||||
voice = 0.14 * np.sin(2 * np.pi * base * vib * seg_t)
|
||||
env = np.clip(np.minimum(seg_t, syl_len - seg_t) * 10, 0, 1)
|
||||
audio[start:end] += voice * env
|
||||
|
||||
audio = audio / max(np.abs(audio).max(), 1e-6) * 0.9
|
||||
pcm = (audio * 32767).astype(np.int16)
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
filename = f"{uuid.uuid4().hex}.wav"
|
||||
path = output_dir / filename
|
||||
with wave.open(str(path), "wb") as f:
|
||||
f.setnchannels(1)
|
||||
f.setsampwidth(2)
|
||||
f.setframerate(SAMPLE_RATE)
|
||||
f.writeframes(pcm.tobytes())
|
||||
|
||||
return GenerationResult(audio_path=path, duration_seconds=duration, sample_rate=SAMPLE_RATE)
|
||||
Reference in New Issue
Block a user