commit 9febdb6bdd7255cdce6af5c03f7acba6d8b8d78e
parent 35494ade96c6f9819bbf78064fb907df84895092
Author: Steven Atkinson <steven@atkinson.mn>
Date: Sat, 28 Sep 2024 16:04:04 -0700
[FEATURE] Add I/O calibration to metadata (#477)
* Add i/o calibration levels to metadata
* Tidy up formatting
* Black
* Change labels, add tooltips
* Update documentation for .nam files, bump file version
Diffstat:
6 files changed, 140 insertions(+), 54 deletions(-)
diff --git a/docs/source/model-file.rst b/docs/source/model-file.rst
@@ -48,6 +48,11 @@ There are also some optional keys that ``nam`` may use:
``"overdrive"``, ``"crunch"``, ``"hi_gain"``, and ``"fuzz"``.
* ``"training"``: A dictionary containing information about training (*Only
when the simplified trainers are used.*)
+ * ``"input_level_dbu"``: The level being input to the gear, in dBu, corresponding to a
+ 1kHz sine wave with 0dBFS peak.
+ * ``"output_level_dbu"``: The level, in dBu, of a 1kHz sine wave that achieves 0dBFS
+ peak when input to the interface that's recording the output of the gear being
+ modeled.
Change log
@@ -56,6 +61,13 @@ Change log
v0.5
^^^^
+v0.5.4
+""""""
+
+Introduced in ``neural-amp-modeler`` `version 0.10.0 <https://github.com/sdatkinson/neural-amp-modeler/releases/tag/v0.10.0>`_.
+
+* Add ``"input_level_dbu"`` and ``"output_level_dbu"`` fields under ``"metadata"``.
+
v0.5.3
""""""
diff --git a/nam/data.py b/nam/data.py
@@ -451,7 +451,6 @@ class Dataset(AbstractDataset, InitializableFromConfig):
channels_to_stereo_mono = {1: "mono", 2: "stereo"}
msg += f"\n * The input is {channels_to_stereo_mono[x_channels]}, but the output is {channels_to_stereo_mono[y_channels]}!"
if x_samples != y_samples:
-
msg += f"\n * The input is {_sample_to_time(x_samples, sample_rate)} long"
msg += f"\n * The output is {_sample_to_time(y_samples, sample_rate)} long"
msg += f"\n\nOriginal exception:\n{e}"
diff --git a/nam/models/exportable.py b/nam/models/exportable.py
@@ -18,7 +18,7 @@ logger = logging.getLogger(__name__)
# Model version is independent from package version as of package version 0.5.2 so that
# the API of the package can iterate at a different pace from that of the model files.
-_MODEL_VERSION = "0.5.3"
+_MODEL_VERSION = "0.5.4"
def _cast_enums(d: Dict[Any, Any]) -> Dict[Any, Any]:
diff --git a/nam/models/metadata.py b/nam/models/metadata.py
@@ -44,6 +44,17 @@ class Date(BaseModel):
class UserMetadata(BaseModel):
"""
Metadata that users provide for a NAM model
+
+ :param name: A "human-readable" name for the model.
+ :param modeled_by: Who made the model
+ :param gear_type: Type of gear.
+ :param gear_make: Make of the gear.
+ :param gear_model: Model of the gear.
+ :param tone_type: What sort of tone the gear has.
+ :input_level_dbu: What analog loudness, in dBu, corresponds to 0 dbFS input to the
+ model.
+ :output_level_dbu: What analog loudness, in dBu, corresponds to 0 dbFS outputted by
+ the model.
"""
name: Optional[str] = None
@@ -52,3 +63,5 @@ class UserMetadata(BaseModel):
gear_make: Optional[str] = None
gear_model: Optional[str] = None
tone_type: Optional[ToneType] = None
+ input_level_dbu: Optional[float] = None
+ output_level_dbu: Optional[float] = None
diff --git a/nam/train/gui/__init__.py b/nam/train/gui/__init__.py
@@ -19,6 +19,7 @@ import webbrowser
from dataclasses import dataclass
from enum import Enum
from functools import partial
+from idlelib.tooltip import Hovertip
from pathlib import Path
from tkinter import filedialog
from typing import Any, Callable, Dict, NamedTuple, Optional, Sequence
@@ -61,6 +62,7 @@ _DEFAULT_THRESHOLD_ESR = None
_ADVANCED_OPTIONS_LEFT_WIDTH = 12
_ADVANCED_OPTIONS_RIGHT_WIDTH = 12
+_METADATA_LEFT_WIDTH = 19
_METADATA_RIGHT_WIDTH = 60
@@ -234,28 +236,6 @@ class _InputPathButton(_PathButton):
return
-class _ClearablePathButton(_PathButton):
- """
- Can clear a path
- """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, color_when_not_set="black", **kwargs)
- # Download the training file!
- self._widgets["button_clear"] = tk.Button(
- self._frame,
- text="Clear",
- width=_BUTTON_WIDTH,
- height=_BUTTON_HEIGHT,
- command=self._clear_path,
- )
- self._widgets["button_clear"].pack(side=tk.RIGHT)
-
- def _clear_path(self):
- self._path = None
- self._set_text()
-
-
class _CheckboxKeys(Enum):
"""
Keys for checkboxes
@@ -588,7 +568,7 @@ class GUI(object):
Open window for metadata
"""
- self._wait_while_func(lambda resume: _UserMetadataGUI(resume, self))
+ self._wait_while_func(lambda resume: UserMetadataGUI(resume, self))
def _pack_update_button(self, version_from: Version, version_to: Version):
"""
@@ -967,6 +947,27 @@ class LabeledOptionMenu(object):
self._selected_value = self._choices(val)
+class _Hovertip(Hovertip):
+ """
+ Adjustments:
+
+ * Always black text (macOS)
+ """
+
+ def showcontents(self):
+ # Override
+ label = tk.Label(
+ self.tipwindow,
+ text=self.text,
+ justify=tk.LEFT,
+ background="#ffffe0",
+ relief=tk.SOLID,
+ borderwidth=1,
+ fg="black",
+ )
+ label.pack()
+
+
class LabeledText(object):
"""
Label (left) and text input (right)
@@ -985,6 +986,8 @@ class LabeledText(object):
:param command: Called to propagate option selection. Is provided with the
value corresponding to the radio button selected.
:param type: If provided, casts value to given type
+ :param left_width: How much space to use on the left side (text)
+ :param right_width: How much space for the Text field
"""
self._frame = frame
label_height = 2
@@ -994,7 +997,7 @@ class LabeledText(object):
width=left_width,
height=label_height,
bg=None,
- anchor="w",
+ anchor="e",
text=label,
)
self._label.pack(side=tk.LEFT)
@@ -1012,6 +1015,13 @@ class LabeledText(object):
if default is not None:
self._text.insert("1.0", str(default))
+ # You can assign a tooltip for the label if you'd like.
+ self.label_tooltip: Optional[_Hovertip] = None
+
+ @property
+ def label(self) -> tk.Label:
+ return self._label
+
def get(self):
try:
val = self._text.get("1.0", tk.END) # Line 1, character zero (wat)
@@ -1110,16 +1120,51 @@ class AdvancedOptionsGUI(object):
)
-class _UserMetadataGUI(object):
+class UserMetadataGUI(object):
# Things that are auto-filled:
# Model date
# gain
def __init__(self, resume_main, parent: GUI):
self._parent = parent
- self._root = _TopLevelWithOk(self._apply, resume_main)
+ self._root = _TopLevelWithOk(self.apply, resume_main)
self._root.title("Metadata")
- LabeledText_ = partial(LabeledText, right_width=_METADATA_RIGHT_WIDTH)
+ # Pack all the widgets
+ self.pack()
+
+ # "Ok": apply and destroy
+ self._frame_ok = tk.Frame(self._root)
+ self._frame_ok.pack()
+ self._button_ok = tk.Button(
+ self._frame_ok,
+ text="Ok",
+ width=_BUTTON_WIDTH,
+ height=_BUTTON_HEIGHT,
+ command=lambda: self._root.destroy(pressed_ok=True),
+ )
+ self._button_ok.pack()
+
+ def apply(self):
+ """
+ Set values to parent and destroy this object
+ """
+ self._parent.user_metadata.name = self._name.get()
+ self._parent.user_metadata.modeled_by = self._modeled_by.get()
+ self._parent.user_metadata.gear_make = self._gear_make.get()
+ self._parent.user_metadata.gear_model = self._gear_model.get()
+ self._parent.user_metadata.gear_type = self._gear_type.get()
+ self._parent.user_metadata.tone_type = self._tone_type.get()
+ self._parent.user_metadata.input_level_dbu = self._input_dbu.get()
+ self._parent.user_metadata.output_level_dbu = self._output_dbu.get()
+ self._parent.user_metadata_flag = True
+
+ def pack(self):
+ LabeledText_ = partial(
+ LabeledText,
+ left_width=_METADATA_LEFT_WIDTH,
+ right_width=_METADATA_RIGHT_WIDTH,
+ )
+ parent = self._parent
# Name
self._frame_name = tk.Frame(self._root)
@@ -1157,6 +1202,45 @@ class _UserMetadataGUI(object):
default=parent.user_metadata.gear_model,
type=_rstripped_str,
)
+ # Calibration: input & output dBu
+ self._frame_input_dbu = tk.Frame(self._root)
+ self._frame_input_dbu.pack()
+ self._input_dbu = LabeledText_(
+ self._frame_input_dbu,
+ "Reamp send level (dBu)",
+ default=parent.user_metadata.input_level_dbu,
+ type=float,
+ )
+ self._input_dbu.label_tooltip = _Hovertip(
+ anchor_widget=self._input_dbu.label,
+ text=(
+ "(Ok to leave blank)\n\n"
+ "Play a sine wave with frequency 1kHz and peak amplitude 0dBFS. Use\n"
+ "a multimeter to measure the RMS voltage of the signal at the jack\n"
+ "that connects to your gear, and convert to dBu.\n"
+ "Record the value here."
+ ),
+ )
+ self._frame_output_dbu = tk.Frame(self._root)
+ self._frame_output_dbu.pack()
+ self._output_dbu = LabeledText_(
+ self._frame_output_dbu,
+ "Reamp return level (dBu)",
+ default=parent.user_metadata.output_level_dbu,
+ type=float,
+ )
+ self._output_dbu.label_tooltip = _Hovertip(
+ anchor_widget=self._output_dbu.label,
+ text=(
+ "(Ok to leave blank)\n\n"
+ "Play a sine wave with frequency 1kHz into your interface where\n"
+ "you're recording your gear. Keeping the interface's input gain\n"
+ "trimmed as you will use it when recording, adjust the sine wave\n"
+ "until the input peaks at exactly 0dBFS in your DAW. Measure the RMS\n"
+ "voltage and convert to dBu.\n"
+ "Record the value here."
+ ),
+ )
# Gear type
self._frame_gear_type = tk.Frame(self._root)
self._frame_gear_type.pack()
@@ -1176,30 +1260,6 @@ class _UserMetadataGUI(object):
default=parent.user_metadata.tone_type,
)
- # "Ok": apply and destroy
- self._frame_ok = tk.Frame(self._root)
- self._frame_ok.pack()
- self._button_ok = tk.Button(
- self._frame_ok,
- text="Ok",
- width=_BUTTON_WIDTH,
- height=_BUTTON_HEIGHT,
- command=lambda: self._root.destroy(pressed_ok=True),
- )
- self._button_ok.pack()
-
- def _apply(self):
- """
- Set values to parent and destroy this object
- """
- self._parent.user_metadata.name = self._name.get()
- self._parent.user_metadata.modeled_by = self._modeled_by.get()
- self._parent.user_metadata.gear_make = self._gear_make.get()
- self._parent.user_metadata.gear_model = self._gear_model.get()
- self._parent.user_metadata.gear_type = self._gear_type.get()
- self._parent.user_metadata.tone_type = self._tone_type.get()
- self._parent.user_metadata_flag = True
-
def _install_error():
window = tk.Tk()
diff --git a/tests/test_nam/test_models/test_exportable.py b/tests/test_nam/test_models/test_exportable.py
@@ -10,7 +10,7 @@ import json
from enum import Enum
from pathlib import Path
from tempfile import TemporaryDirectory
-from typing import Optional, Tuple, Union
+from typing import Optional, Tuple
import numpy as np
import pytest
@@ -57,6 +57,8 @@ class TestExportable(object):
gear_make="SteveCo",
gear_model="SteveAmp",
tone_type=metadata.ToneType.HI_GAIN,
+ input_level_dbu=-6.5,
+ output_level_dbu=-12.5,
),
None,
),