Download plus/plus_render.py from Nokia123543/muscriptor-large-fork: direct link, hf CLI and curl.
- Browser
- Download file 3.76 kB
-
https://huggingface.co/Nokia123543/muscriptor-large-fork/resolve/main/plus/plus_render.py
- Command line
-
hf download hf://Nokia123543/muscriptor-large-fork/plus/plus_render.py
-
curl -L -o plus_render.py https://huggingface.co/Nokia123543/muscriptor-large-fork/resolve/main/plus/plus_render.py
3.76 kB
| """PLUS render: MIDI в WAV без внешних синтезаторов, чтобы слушать до DAW. | |
| Синтез аддитивный: три партиала с экспоненциальным спадом, стерео-панорама по | |
| высоте, мягкий лимитер. Никаких сэмплов и GPU, только numpy и soundfile. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import os | |
| import sys | |
| import numpy as np | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from plus_core import load_midi, step # noqa: E402 | |
| __version__ = "1.0" | |
| PARTIALS = ((1.0, 1.00), (2.0, 0.42), (3.0, 0.18), (4.01, 0.09)) | |
| def note_wave(freq: float, seconds: float, rate: int, velocity: float) -> np.ndarray: | |
| """Одна нота: сумма партиалов с атакой 4 мс и экспоненциальным спадом.""" | |
| count = max(1, int(seconds * rate)) | |
| time_axis = np.arange(count) / rate | |
| wave = np.zeros(count, dtype=np.float32) | |
| for multiple, amplitude in PARTIALS: | |
| if freq * multiple > rate / 2 - 100: | |
| continue | |
| wave += amplitude * np.sin(2 * np.pi * freq * multiple * time_axis) | |
| decay = np.exp(-3.0 * time_axis / max(0.12, seconds)) | |
| attack = np.clip(time_axis / 0.004, 0.0, 1.0) | |
| tail = np.clip((count - np.arange(count)) / max(1, int(0.05 * rate)), 0.0, 1.0) | |
| return (wave * decay * attack * tail * velocity).astype(np.float32) | |
| def render(piece, rate: int = 44100, gain: float = 0.28, stop: float | None = None) -> np.ndarray: | |
| total = float(stop if stop else piece.duration) + 0.6 | |
| frames = int(total * rate) | |
| left = np.zeros(frames, dtype=np.float32) | |
| right = np.zeros(frames, dtype=np.float32) | |
| for note in piece.notes: | |
| if stop and note.start > stop: | |
| continue | |
| freq = 440.0 * 2 ** ((note.pitch - 69) / 12.0) | |
| seconds = min(note.dur + 0.25, max(0.05, total - note.start)) | |
| wave = note_wave(freq, seconds, rate, note.velocity / 127.0) | |
| start = int(note.start * rate) | |
| end = min(frames, start + wave.size) | |
| if start >= frames or end <= start: | |
| continue | |
| chunk = wave[: end - start] | |
| pan = float(np.clip((note.pitch - 60) / 36.0, -1.0, 1.0)) | |
| left[start:end] += chunk * (0.5 * (1 - pan) + 0.25) | |
| right[start:end] += chunk * (0.5 * (1 + pan) + 0.25) | |
| mix = np.stack([left, right], axis=1) * gain | |
| peak = float(np.abs(mix).max()) | |
| if peak > 0.98: | |
| mix *= 0.98 / peak | |
| return mix | |
| def main(argv: list[str] | None = None) -> int: | |
| parser = argparse.ArgumentParser(description="PLUS render: MIDI в WAV") | |
| parser.add_argument("--midi", required=True) | |
| parser.add_argument("--out", help="выходной WAV") | |
| parser.add_argument("--rate", type=int, default=44100) | |
| parser.add_argument("--gain", type=float, default=0.28) | |
| parser.add_argument("--stop", type=float, help="рендерить только первые N секунд") | |
| args = parser.parse_args(argv) | |
| import soundfile as sf | |
| out_path = args.out or os.path.splitext(args.midi)[0] + "_preview.wav" | |
| piece = load_midi(args.midi) | |
| step(1, 3, "нот %d, %.1f с" % (len(piece.notes), piece.duration)) | |
| audio = render(piece, args.rate, args.gain, args.stop) | |
| step(2, 3, "свожу %d сэмплов" % audio.shape[0]) | |
| os.makedirs(os.path.dirname(os.path.abspath(out_path)) or ".", exist_ok=True) | |
| sf.write(out_path, audio, args.rate, subtype="PCM_16") | |
| peak = float(np.abs(audio).max()) | |
| rms = float(np.sqrt((audio ** 2).mean())) | |
| step(3, 3, "готово: %s | peak %.2f | rms %.3f" % (out_path, peak, rms)) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) | |