Fix bass sampler range and cover audible notes
Browse files- README.md +2 -1
- app.py +1 -1
- pipeline.py +5 -2
- test_sampler.py +31 -4
README.md
CHANGED
|
@@ -21,7 +21,8 @@ source-timed comparisons. SheetSage2 extracts notes and beats; FluidSynth
|
|
| 21 |
uses the full [FluidR3 GM bank](https://www.fluidsynth.org/wiki/GettingStarted/)
|
| 22 |
for the selected instrument and drum kit, plus sampled electric
|
| 23 |
guitar and fingered bass in clean and processed versions. The bass is shifted
|
| 24 |
-
down
|
|
|
|
| 25 |
follows voice pitch and amplitude. The sampler uses full-size FreePats electric
|
| 26 |
guitar and fingered bass SoundFonts, with FFmpeg compression, filtering,
|
| 27 |
oversampled soft clipping, and a short guitar room echo in processed versions.
|
|
|
|
| 21 |
uses the full [FluidR3 GM bank](https://www.fluidsynth.org/wiki/GettingStarted/)
|
| 22 |
for the selected instrument and drum kit, plus sampled electric
|
| 23 |
guitar and fingered bass in clean and processed versions. The bass is shifted
|
| 24 |
+
down two octaves into the actual sample range, with note starts and lengths
|
| 25 |
+
preserved. A continuous-F0 synth
|
| 26 |
follows voice pitch and amplitude. The sampler uses full-size FreePats electric
|
| 27 |
guitar and fingered bass SoundFonts, with FFmpeg compression, filtering,
|
| 28 |
oversampled soft clipping, and a short guitar room echo in processed versions.
|
app.py
CHANGED
|
@@ -72,7 +72,7 @@ with gr.Blocks(title="Hum · напев в инструмент", css=CSS, theme
|
|
| 72 |
go = gr.Button("Создать все варианты", variant="primary", size="lg")
|
| 73 |
gr.HTML("""<div class='note'><b>Текущая цель:</b> один инструмент и
|
| 74 |
приблизительно исходное время. Электрогитара и бас используют
|
| 75 |
-
записанные сэмплы; бас звучит октав
|
| 76 |
добавляют усиление и лёгкий эффект, но не новые ноты. Барабаны
|
| 77 |
привязаны к долям SheetSage2. YuE2 может изменить
|
| 78 |
длительность, добавить человеческие звуки или другие инструменты;
|
|
|
|
| 72 |
go = gr.Button("Создать все варианты", variant="primary", size="lg")
|
| 73 |
gr.HTML("""<div class='note'><b>Текущая цель:</b> один инструмент и
|
| 74 |
приблизительно исходное время. Электрогитара и бас используют
|
| 75 |
+
записанные сэмплы; бас звучит на две октавы ниже. Обработанные версии
|
| 76 |
добавляют усиление и лёгкий эффект, но не новые ноты. Барабаны
|
| 77 |
привязаны к долям SheetSage2. YuE2 может изменить
|
| 78 |
длительность, добавить человеческие звуки или другие инструменты;
|
pipeline.py
CHANGED
|
@@ -20,6 +20,9 @@ YUE_PYTHON = Path(os.environ.get("HUM_YUE_PYTHON", "/opt/yue/bin/python"))
|
|
| 20 |
SOUNDFONT_DIR = Path(os.environ.get("HUM_SOUNDFONTS", "/opt/soundfonts"))
|
| 21 |
GM_SOUNDFONT = Path(os.environ.get("HUM_GM_SOUNDFONT",
|
| 22 |
"/usr/share/sounds/sf2/FluidR3_GM.sf2"))
|
|
|
|
|
|
|
|
|
|
| 23 |
SAMPLER_VARIANTS = (
|
| 24 |
"instrument", "instrument_drums", "contour",
|
| 25 |
"guitar_clean", "guitar_amp", "bass_clean", "bass_amp",
|
|
@@ -348,7 +351,7 @@ def generate_all(source: str, instrument: str, styles: dict[str, str], seed: int
|
|
| 348 |
"yue_requested": include_yue, "yue_timing": dict(yue_timing),
|
| 349 |
"errors": dict(errors), "complete": complete,
|
| 350 |
"source_timing_preserved": list(SAMPLER_VARIANTS),
|
| 351 |
-
"bass_transpose_semitones":
|
| 352 |
"yue_vocal_absence_guaranteed": False,
|
| 353 |
"elapsed_seconds": round(time.monotonic() - began, 2)}
|
| 354 |
return (*(outputs[name] for name in VARIANTS), str(melody_midi),
|
|
@@ -387,7 +390,7 @@ def generate_all(source: str, instrument: str, styles: dict[str, str], seed: int
|
|
| 387 |
progress(.48, desc="Сэмплирую электрогитару и бас")
|
| 388 |
for name, font_name, transpose, effect in (
|
| 389 |
("guitar_clean", "electric-clean.sf2", 0, "guitar"),
|
| 390 |
-
("bass_clean", "finger-bass.sf2",
|
| 391 |
):
|
| 392 |
try:
|
| 393 |
raw, _ = render_preset(midi, folder, name, 0, duration, log,
|
|
|
|
| 20 |
SOUNDFONT_DIR = Path(os.environ.get("HUM_SOUNDFONTS", "/opt/soundfonts"))
|
| 21 |
GM_SOUNDFONT = Path(os.environ.get("HUM_GM_SOUNDFONT",
|
| 22 |
"/usr/share/sounds/sf2/FluidR3_GM.sf2"))
|
| 23 |
+
# FreePats Finger Bass is silent over much of the voice-minus-one-octave
|
| 24 |
+
# range; two octaves put the seen 54..68 melody into its audible 30..44 range.
|
| 25 |
+
BASS_TRANSPOSE = -24
|
| 26 |
SAMPLER_VARIANTS = (
|
| 27 |
"instrument", "instrument_drums", "contour",
|
| 28 |
"guitar_clean", "guitar_amp", "bass_clean", "bass_amp",
|
|
|
|
| 351 |
"yue_requested": include_yue, "yue_timing": dict(yue_timing),
|
| 352 |
"errors": dict(errors), "complete": complete,
|
| 353 |
"source_timing_preserved": list(SAMPLER_VARIANTS),
|
| 354 |
+
"bass_transpose_semitones": BASS_TRANSPOSE,
|
| 355 |
"yue_vocal_absence_guaranteed": False,
|
| 356 |
"elapsed_seconds": round(time.monotonic() - began, 2)}
|
| 357 |
return (*(outputs[name] for name in VARIANTS), str(melody_midi),
|
|
|
|
| 390 |
progress(.48, desc="Сэмплирую электрогитару и бас")
|
| 391 |
for name, font_name, transpose, effect in (
|
| 392 |
("guitar_clean", "electric-clean.sf2", 0, "guitar"),
|
| 393 |
+
("bass_clean", "finger-bass.sf2", BASS_TRANSPOSE, "bass"),
|
| 394 |
):
|
| 395 |
try:
|
| 396 |
raw, _ = render_preset(midi, folder, name, 0, duration, log,
|
test_sampler.py
CHANGED
|
@@ -10,6 +10,33 @@ from unittest.mock import patch
|
|
| 10 |
|
| 11 |
|
| 12 |
class SamplerTest(unittest.TestCase):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
def test_optional_yue_does_not_reclassify_sampler_outputs_as_missing(self):
|
| 14 |
import numpy as np
|
| 15 |
import soundfile as sf
|
|
@@ -54,9 +81,9 @@ class SamplerTest(unittest.TestCase):
|
|
| 54 |
final = list(generate_all(str(wave), "Флейта", {}, 4096, True))[-1]
|
| 55 |
self.assertEqual(final[-1]["errors"], {})
|
| 56 |
|
| 57 |
-
def
|
| 58 |
import pretty_midi
|
| 59 |
-
from pipeline import render_preset
|
| 60 |
|
| 61 |
with tempfile.TemporaryDirectory() as directory:
|
| 62 |
folder = Path(directory)
|
|
@@ -71,9 +98,9 @@ class SamplerTest(unittest.TestCase):
|
|
| 71 |
original.write(str(source))
|
| 72 |
with patch("pipeline.render_midi", return_value=folder / "bass.wav"):
|
| 73 |
_, path = render_preset(source, folder, "bass", 0, 1.,
|
| 74 |
-
folder / "log", transpose=
|
| 75 |
notes = pretty_midi.PrettyMIDI(str(path)).instruments[0].notes
|
| 76 |
-
self.assertEqual([n.pitch for n in notes], [
|
| 77 |
for before, after in zip(voice.notes, notes):
|
| 78 |
self.assertAlmostEqual(before.start, after.start, places=4)
|
| 79 |
self.assertAlmostEqual(before.end, after.end, places=4)
|
|
|
|
| 10 |
|
| 11 |
|
| 12 |
class SamplerTest(unittest.TestCase):
|
| 13 |
+
def test_bass_font_sounds_at_both_ends_of_development_note_range(self):
|
| 14 |
+
import numpy as np
|
| 15 |
+
import pretty_midi
|
| 16 |
+
import soundfile as sf
|
| 17 |
+
from pipeline import BASS_TRANSPOSE, SOUNDFONT_DIR, render_preset
|
| 18 |
+
|
| 19 |
+
font = SOUNDFONT_DIR / "finger-bass.sf2"
|
| 20 |
+
if not font.is_file():
|
| 21 |
+
self.skipTest("Pinned FreePats bass SoundFont not installed")
|
| 22 |
+
# The seen development recording spans MIDI 54..68. Keep this tiny
|
| 23 |
+
# synthetic fixture separate from the source audio and its title.
|
| 24 |
+
with tempfile.TemporaryDirectory() as directory:
|
| 25 |
+
folder = Path(directory)
|
| 26 |
+
score = pretty_midi.PrettyMIDI()
|
| 27 |
+
voice = pretty_midi.Instrument(program=0)
|
| 28 |
+
voice.notes = [pretty_midi.Note(90, 54, .1, .35),
|
| 29 |
+
pretty_midi.Note(90, 68, .6, .85)]
|
| 30 |
+
score.instruments.append(voice)
|
| 31 |
+
midi = folder / "notes.mid"
|
| 32 |
+
score.write(str(midi))
|
| 33 |
+
wav, _ = render_preset(midi, folder, "bass", 0, 1., folder / "log",
|
| 34 |
+
soundfont=font, transpose=BASS_TRANSPOSE)
|
| 35 |
+
audio, rate = sf.read(wav, dtype="float32", always_2d=True)
|
| 36 |
+
for start in (.12, .62):
|
| 37 |
+
span = audio[round(start * rate):round((start + .1) * rate)]
|
| 38 |
+
self.assertGreater(float(np.sqrt(np.mean(span**2))), .001)
|
| 39 |
+
|
| 40 |
def test_optional_yue_does_not_reclassify_sampler_outputs_as_missing(self):
|
| 41 |
import numpy as np
|
| 42 |
import soundfile as sf
|
|
|
|
| 81 |
final = list(generate_all(str(wave), "Флейта", {}, 4096, True))[-1]
|
| 82 |
self.assertEqual(final[-1]["errors"], {})
|
| 83 |
|
| 84 |
+
def test_bass_register_preserves_note_times(self):
|
| 85 |
import pretty_midi
|
| 86 |
+
from pipeline import BASS_TRANSPOSE, render_preset
|
| 87 |
|
| 88 |
with tempfile.TemporaryDirectory() as directory:
|
| 89 |
folder = Path(directory)
|
|
|
|
| 98 |
original.write(str(source))
|
| 99 |
with patch("pipeline.render_midi", return_value=folder / "bass.wav"):
|
| 100 |
_, path = render_preset(source, folder, "bass", 0, 1.,
|
| 101 |
+
folder / "log", transpose=BASS_TRANSPOSE)
|
| 102 |
notes = pretty_midi.PrettyMIDI(str(path)).instruments[0].notes
|
| 103 |
+
self.assertEqual([n.pitch for n in notes], [36, 40])
|
| 104 |
for before, after in zip(voice.notes, notes):
|
| 105 |
self.assertAlmostEqual(before.start, after.start, places=4)
|
| 106 |
self.assertAlmostEqual(before.end, after.end, places=4)
|