batoon commited on
Commit
cce9569
·
verified ·
1 Parent(s): 7b3fa89

Fix bass sampler range and cover audible notes

Browse files
Files changed (4) hide show
  1. README.md +2 -1
  2. app.py +1 -1
  3. pipeline.py +5 -2
  4. 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 one octave, with note starts and lengths preserved. A continuous-F0 synth
 
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": -12,
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", -12, "bass"),
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 test_bass_octave_preserves_note_times(self):
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=-12)
75
  notes = pretty_midi.PrettyMIDI(str(path)).instruments[0].notes
76
- self.assertEqual([n.pitch for n in notes], [48, 52])
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)