neural-amp-modeler

Neural network emulator for guitar amplifiers
Log | Files | Refs | README | LICENSE

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:
Mdocs/source/model-file.rst | 12++++++++++++
Mnam/data.py | 1-
Mnam/models/exportable.py | 2+-
Mnam/models/metadata.py | 13+++++++++++++
Mnam/train/gui/__init__.py | 162++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Mtests/test_nam/test_models/test_exportable.py | 4+++-
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, ),