batoon commited on
Commit
e43fe76
·
verified ·
1 Parent(s): 085827b

Use full GM SoundFont and verify sampler timing

Browse files
Files changed (4) hide show
  1. Dockerfile +2 -1
  2. README.md +2 -1
  3. pipeline.py +4 -3
  4. test_sampler.py +60 -0
Dockerfile CHANGED
@@ -6,7 +6,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
6
  GRADIO_ANALYTICS_ENABLED=False
7
 
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
9
- ffmpeg fluidsynth libsndfile1 libgomp1 curl ca-certificates p7zip-full && \
 
10
  rm -rf /var/lib/apt/lists/*
11
 
12
  # Full multi-sampled CC0 electric guitar and bass. Pin release archives so a
 
6
  GRADIO_ANALYTICS_ENABLED=False
7
 
8
  RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ ffmpeg fluidsynth fluid-soundfont-gm libsndfile1 libgomp1 \
10
+ curl ca-certificates p7zip-full && \
11
  rm -rf /var/lib/apt/lists/*
12
 
13
  # Full multi-sampled CC0 electric guitar and bass. Pin release archives so a
README.md CHANGED
@@ -18,7 +18,8 @@ models:
18
 
19
  Upload a short hum or record one in the browser. One request generates seven
20
  source-timed comparisons. SheetSage2 extracts notes and beats; FluidSynth
21
- renders an instrument alone and with a GM drum kit, plus sampled electric
 
22
  guitar and fingered bass in clean and processed versions. The bass is shifted
23
  down one octave, with note starts and lengths preserved. A continuous-F0 synth
24
  follows voice pitch and amplitude. The sampler uses full-size FreePats electric
 
18
 
19
  Upload a short hum or record one in the browser. One request generates seven
20
  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
pipeline.py CHANGED
@@ -18,6 +18,8 @@ ROOT = Path(__file__).resolve().parent
18
  SHEET_PYTHON = Path(os.environ.get("HUM_SHEET_PYTHON", "/opt/sheet/bin/python"))
19
  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
  VARIANTS = (
22
  "instrument", "instrument_drums", "contour",
23
  "guitar_clean", "guitar_amp", "bass_clean", "bass_amp",
@@ -77,12 +79,11 @@ def transcribe(pcm24: Path, folder: Path, log: Path) -> tuple[Path, Path, Path,
77
  def render_midi(midi_path: Path, output: Path, duration: float, log: Path,
78
  soundfont: Path | None = None) -> Path:
79
  import numpy as np
80
- import pretty_midi
81
  import soundfile as sf
82
 
83
- soundfont = soundfont or Path(pretty_midi.__file__).parent / "TimGM6mb.sf2"
84
  if not soundfont.is_file():
85
- raise RuntimeError("SoundFont missing from pretty_midi installation")
86
  raw = output.with_name(output.stem + "-tail.wav")
87
  command(["fluidsynth", "-ni", "-C", "0", "-R", "0", "-r", "48000",
88
  "-z", "64", "-g", ".2", "-o", "player.timing-source=sample",
 
18
  SHEET_PYTHON = Path(os.environ.get("HUM_SHEET_PYTHON", "/opt/sheet/bin/python"))
19
  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
  VARIANTS = (
24
  "instrument", "instrument_drums", "contour",
25
  "guitar_clean", "guitar_amp", "bass_clean", "bass_amp",
 
79
  def render_midi(midi_path: Path, output: Path, duration: float, log: Path,
80
  soundfont: Path | None = None) -> Path:
81
  import numpy as np
 
82
  import soundfile as sf
83
 
84
+ soundfont = soundfont or GM_SOUNDFONT
85
  if not soundfont.is_file():
86
+ raise RuntimeError(f"SoundFont не найден: {soundfont}")
87
  raw = output.with_name(output.stem + "-tail.wav")
88
  command(["fluidsynth", "-ni", "-C", "0", "-R", "0", "-r", "48000",
89
  "-z", "64", "-g", ".2", "-o", "player.timing-source=sample",
test_sampler.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sampler note timing and audio effect contracts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import tempfile
7
+ import unittest
8
+ from unittest.mock import patch
9
+
10
+
11
+ class SamplerTest(unittest.TestCase):
12
+ def test_bass_octave_preserves_note_times(self):
13
+ import pretty_midi
14
+ from pipeline import render_preset
15
+
16
+ with tempfile.TemporaryDirectory() as directory:
17
+ folder = Path(directory)
18
+ original = pretty_midi.PrettyMIDI()
19
+ voice = pretty_midi.Instrument(program=0)
20
+ voice.notes = [
21
+ pretty_midi.Note(90, 60, .125, .375),
22
+ pretty_midi.Note(90, 64, .5, .875),
23
+ ]
24
+ original.instruments.append(voice)
25
+ source = folder / "source.mid"
26
+ original.write(str(source))
27
+ with patch("pipeline.render_midi", return_value=folder / "bass.wav"):
28
+ _, path = render_preset(source, folder, "bass", 0, 1.,
29
+ folder / "log", transpose=-12)
30
+ notes = pretty_midi.PrettyMIDI(str(path)).instruments[0].notes
31
+ self.assertEqual([n.pitch for n in notes], [48, 52])
32
+ for before, after in zip(voice.notes, notes):
33
+ self.assertAlmostEqual(before.start, after.start, places=4)
34
+ self.assertAlmostEqual(before.end, after.end, places=4)
35
+
36
+ def test_effects_preserve_duration_and_change_waveform(self):
37
+ import numpy as np
38
+ import soundfile as sf
39
+ from pipeline import apply_effect
40
+
41
+ rate = 48000
42
+ t = np.arange(rate, dtype=np.float32) / rate
43
+ source_wave = (.12 * np.sin(2 * np.pi * 220 * t)).astype("float32")
44
+ with tempfile.TemporaryDirectory() as directory:
45
+ folder = Path(directory)
46
+ source = folder / "source.wav"
47
+ sf.write(source, source_wave, rate, subtype="FLOAT")
48
+ for kind in ("guitar", "bass"):
49
+ result = apply_effect(source, folder / f"{kind}.wav", kind,
50
+ 1., folder / "log")
51
+ output, output_rate = sf.read(result, dtype="float32",
52
+ always_2d=True)
53
+ self.assertEqual(output_rate, rate)
54
+ self.assertEqual(len(output), rate)
55
+ self.assertTrue(np.isfinite(output).all())
56
+ self.assertGreater(np.mean(np.abs(output[:, 0] - source_wave)), .001)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ unittest.main()