Repository: claritychallenge/clarity (language: python)

Issue:
introducing dataclass to represent listener
# Introducing a Listener dataclass

A listener data class which stores an id and an audiogram for the left and right ear will allow us to simplify recipe code and reduce duplication.

- The class can validate that the left and right ear have audiogram measured at the same frequencies
- It can handle the reading of listener data from the standard clarity metadata files
- It will serve as a useful way to pass parameters to functions that expect left and right ear measurements.




Discussion / hints:

Fix patch:
diff --git a/clarity/enhancer/gha/gha_interface.py b/clarity/enhancer/gha/gha_interface.py
index 0483f7b77..196f92782 100644
--- a/clarity/enhancer/gha/gha_interface.py
+++ b/clarity/enhancer/gha/gha_interface.py
@@ -11,6 +11,7 @@
 from jinja2 import Environment, FileSystemLoader
 
 from clarity.enhancer.gha.gha_utils import format_gaintable, get_gaintable
+from clarity.utils.audiogram import Listener
 from clarity.utils.file_io import read_signal, write_signal
 
 
@@ -96,7 +97,7 @@ def create_configured_cfgfile(
         return output
 
     def process_files(
-        self, infile_names, outfile_name, audiogram_left, audiogram_right, listener=None
+        self, infile_names: list[str], outfile_name: str, listener: Listener
     ):
         """Process a set of input signals and generate an output.
 
@@ -106,15 +107,15 @@ def process_files(
             outfile_name (str): File in which to store output wav files
             dry_run (bool): perform dry run only
         """
-        logging.info("Processing %s with listener %s", outfile_name, listener)
+        logging.info("Processing %s with listener %s", outfile_name, listener.id)
 
         logging.info(
             "Audiogram severity is %s (left) and %s (right)",
-            audiogram_left.severity,
-            audiogram_right.severity,
+            listener.audiogram_left.severity,
+            listener.audiogram_right.severity,
         )
-        audiogram_left = audiogram_left.resample(self.audf)
-        audiogram_right = audiogram_right.resample(self.audf)
+        audiogram_left = listener.audiogram_left.resample(self.audf)
+        audiogram_right = listener.audiogram_right.resample(self.audf)
 
         # Get gain table with noisegate correction
         gaintable = get_gaintable(
diff --git a/clarity/enhancer/nalr.py b/clarity/enhancer/nalr.py
index 6eb84287a..a6c1a0ec2 100644
--- a/clarity/enhancer/nalr.py
+++ b/clarity/enhancer/nalr.py
@@ -7,11 +7,15 @@
 import scipy.signal
 
 from clarity.evaluator.msbg.msbg_utils import firwin2
+from clarity.utils.audiogram import Audiogram
 
 if TYPE_CHECKING:
     from numpy import ndarray
 
 
+NALR_FREQS = np.array([250, 500, 1000, 2000, 4000, 6000])
+
+
 class NALR:
     def __init__(self, nfir: int, sample_rate: float) -> None:
         """
@@ -23,28 +27,11 @@ def __init__(self, nfir: int, sample_rate: float) -> None:
         # Processing parameters
         self.fmax = 0.5 * sample_rate
 
-        # Audiometric frequencies
-        self.aud = np.array([250, 500, 1000, 2000, 4000, 6000])
-
         # Design a flat filter having the same delay as the NAL-R filter
         self.delay = np.zeros(nfir + 1)
         self.delay[nfir // 2] = 1.0
 
-    def hl_interp(self, hl: np.ndarray, cfs: np.ndarray) -> ndarray:
-        """Interpolate the hearing loss at the audiometric frequencies."""
-        try:
-            hl_interp_fn = scipy.interpolate.interp1d(cfs, hl)
-        except ValueError as exception:
-            raise ValueError(
-                "Hearing losses (hl) and center frequencies (cfs) don't match!"
-            ) from exception
-        return hl_interp_fn(self.aud)
-
-    def build(
-        self,
-        hl: ndarray,
-        cfs: ndarray | None = None,
-    ) -> tuple[ndarray, ndarray]:
+    def build(self, audiogram: Audiogram) -> tuple[ndarray, ndarray]:
         """
         Args:
             hl: hearing thresholds at [250, 500, 1000, 2000, 4000, 6000] Hz
@@ -55,29 +42,29 @@ def build(
             delay
         """
 
-        # Apply interpolation only if cfs is not None
-        if cfs is not None:
-            hl = self.hl_interp(hl, cfs)
+        audiogram = audiogram.resample(NALR_FREQS)
 
-        max_loss = np.max(hl)
+        max_loss = np.max(audiogram.levels)
 
         if max_loss > 0:
             # Compute the NAL-R frequency response at the audiometric frequencies
             bias = np.array([-17, -8, 1, -1, -2, -2])
 
-            critical_loss = hl[1] + hl[2] + hl[3]  # <-- loss at 500 Hz to 2 kHz
+            critical_loss = (
+                audiogram.levels[1] + audiogram.levels[2] + audiogram.levels[3]
+            )  # <-- loss at 500 Hz to 2 kHz
             if critical_loss <= 180:
                 x_ave = 0.05 * critical_loss
             else:
                 x_ave = 9.0 + 0.116 * (critical_loss - 180)
-            gain_db = x_ave + 0.31 * hl + bias
+            gain_db = x_ave + 0.31 * audiogram.levels + bias
             gain_db = np.clip(gain_db, a_min=0, a_max=None)
 
             # Design the linear-phase FIR filter
 
             # Build the gain interpolation function
             freq_ext: ndarray = np.concatenate(
-                (np.array([0.0]), self.aud, np.array([self.fmax]))
+                (np.array([0.0]), NALR_FREQS, np.array([self.fmax]))
             )
             gain_db_ext = np.concatenate(([gain_db[0]], gain_db, [gain_db[-1]]))
             interp_fn = scipy.interpolate.interp1d(freq_ext, gain_db_ext)
diff --git a/clarity/evaluator/haspi/eb.py b/clarity/evaluator/haspi/eb.py
index f3cb7797f..32692db92 100644
--- a/clarity/evaluator/haspi/eb.py
+++ b/clarity/evaluator/haspi/eb.py
@@ -19,6 +19,7 @@
 )
 
 from clarity.enhancer.nalr import NALR
+from clarity.utils.audiogram import Audiogram
 
 if TYPE_CHECKING:
     from numpy import ndarray
@@ -148,8 +149,11 @@ def ear_model(
     if itype == 1:
         nfir = 140  # Length in samples of the FIR NAL-R EQ filter (24-kHz rate)
         enhancer = NALR(nfir, freq_sample)
-        aud = np.array([250, 500, 1000, 2000, 4000, 6000])
-        nalr_fir, _ = enhancer.build(hearing_loss, aud)
+        audiogram = Audiogram(
+            levels=hearing_loss,
+            frequencies=np.array([250, 500, 1000, 2000, 4000, 6000]),
+        )
+        nalr_fir, _ = enhancer.build(audiogram)
         reference_24hz = enhancer.apply(nalr_fir, reference_24hz)
         reference_24hz = reference_24hz[nfir : nfir + nsamp]
 
diff --git a/clarity/evaluator/haspi/haspi.py b/clarity/evaluator/haspi/haspi.py
index 8803958dd..7dae0899d 100644
--- a/clarity/evaluator/haspi/haspi.py
+++ b/clarity/evaluator/haspi/haspi.py
@@ -14,7 +14,7 @@
     modulation_cross_correlation,
 )
 from clarity.evaluator.haspi.ip import get_neural_net, nn_feed_forward_ensemble
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 
 if TYPE_CHECKING:
     from numpy import ndarray
@@ -155,8 +155,7 @@ def haspi_v2_be(  # pylint: disable=too-many-arguments
     processed_left: ndarray,
     processed_right: ndarray,
     sample_rate: float,
-    audiogram_left: Audiogram,
-    audiogram_right: Audiogram,
+    listener: Listener,
     level: float = 100.0,
 ) -> float:
     """Better ear HASPI.
@@ -185,7 +184,7 @@ def haspi_v2_be(  # pylint: disable=too-many-arguments
         sample_rate,
         processed_left,
         sample_rate,
-        audiogram_left,
+        listener.audiogram_left,
         level,
     )
     score_right, _ = haspi_v2(
@@ -193,7 +192,7 @@ def haspi_v2_be(  # pylint: disable=too-many-arguments
         sample_rate,
         processed_right,
         sample_rate,
-        audiogram_right,
+        listener.audiogram_right,
         level,
     )
 
diff --git a/clarity/evaluator/hasqi/hasqi.py b/clarity/evaluator/hasqi/hasqi.py
index d3981dd40..b11ee47ef 100644
--- a/clarity/evaluator/hasqi/hasqi.py
+++ b/clarity/evaluator/hasqi/hasqi.py
@@ -6,7 +6,7 @@
 import numpy as np
 
 from clarity.evaluator.haspi import eb
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 
 if TYPE_CHECKING:
     from numpy import ndarray
@@ -174,8 +174,7 @@ def hasqi_v2_better_ear(
     processed_left: ndarray,
     processed_right: ndarray,
     sample_rate: float,
-    audiogram_left: Audiogram,
-    audiogram_right: Audiogram,
+    listener: Listener,
     level: float = 100.0,
 ) -> float:
     """Better ear HASQI.
@@ -204,7 +203,7 @@ def hasqi_v2_better_ear(
         sample_rate,
         processed_left,
         sample_rate,
-        audiogram_left,
+        listener.audiogram_left,
         equalisation=1,
         level1=level,
     )
@@ -213,7 +212,7 @@ def hasqi_v2_better_ear(
         sample_rate,
         processed_right,
         sample_rate,
-        audiogram_right,
+        listener.audiogram_right,
         equalisation=1,
         level1=level,
     )
diff --git a/clarity/utils/audiogram.py b/clarity/utils/audiogram.py
index 77ec57188..712f40ca2 100644
--- a/clarity/utils/audiogram.py
+++ b/clarity/utils/audiogram.py
@@ -1,8 +1,10 @@
 """Dataclass to represent a monaural audiogram"""
 from __future__ import annotations
 
+import json
 from dataclasses import dataclass, field
-from typing import TYPE_CHECKING, Final
+from pathlib import Path
+from typing import TYPE_CHECKING, Callable, Final
 
 import numpy as np
 
@@ -60,6 +62,11 @@ class Audiogram:
 
     def __post_init__(self) -> None:
         """Check that dimensions of levels and frequencies match."""
+
+        # Ensure that levels and frequencies are numpy arrays
+        self.levels = np.array(self.levels)
+        self.frequencies = np.array(self.frequencies)
+
         if len(self.levels) != len(self.frequencies):
             raise ValueError(
                 f"Levels ({len(self.levels)}) and frequencies ({len(self.frequencies)})"
@@ -119,10 +126,14 @@ def has_frequencies(self, frequencies: ndarray) -> bool:
         """
         return np.all(np.isin(frequencies, self.frequencies, assume_unique=True))
 
-    def resample(self, new_frequencies: ndarray) -> Audiogram:
+    def resample(
+        self, new_frequencies: ndarray, linear_frequency: bool = False
+    ) -> Audiogram:
         """Resample the audiogram to a new set of frequencies.
 
-        Interpolates linearly on a log frequency axis.
+        Interpolates linearly on a (by default) log frequency axis. If
+        linear_frequencies is set True then interpolation is done on a linear
+        frequency axis.
 
         Args:
             new_frequencies (ndarray): The new frequencies to resample to
@@ -132,10 +143,13 @@ def resample(self, new_frequencies: ndarray) -> Audiogram:
 
         """
 
+        # Either log frequency scaling or linear frequency scaling
+        axis_fn: Callable = (lambda x: x) if linear_frequency else np.log
+
         return Audiogram(
             levels=np.interp(
-                np.log(new_frequencies),
-                np.log(self.frequencies),
+                axis_fn(new_frequencies),
+                axis_fn(self.frequencies),
                 self.levels,
                 left=self.levels[0],
                 right=self.levels[-1],
@@ -153,6 +167,12 @@ def resample(self, new_frequencies: ndarray) -> Audiogram:
     levels=np.array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]),
 )
 
+# No loss
+AUDIOGRAM_REF_CLARITY: Final = Audiogram(
+    frequencies=DEFAULT_CLARITY_AUDIOGRAM_FREQUENCIES,
+    levels=np.array([0, 0, 0, 0, 0, 0, 0, 0]),
+)
+
 # Mild age-related hearing loss, slightly reduced from CF, first-time aid
 # wearers group (used in Stafa talk by MAS).
 AUDIOGRAM_MILD: Final = Audiogram(
@@ -173,3 +193,73 @@ def resample(self, new_frequencies: ndarray) -> Audiogram:
     frequencies=FULL_STANDARD_AUDIOGRAM_FREQUENCIES,
     levels=np.array([19, 19, 28, 35, 40, 47, 52, 56, 58, 58, 63, 70, 75, 80, 80]),
 )
+
+
+@dataclass
+class Listener:
+    """Dataclass to represent a Listener.
+
+    The listener is currently defined by their left and right ear
+    audiogram. In later versions, this may be extended to include
+    further audiometric data.
+
+    The class provides methods for reading metadata files which
+    will also include some basic validation.
+
+    Attributes:
+        id (str): The ID of the listener
+        audiogram_left (Audiogram): The audiogram for the left ear
+        audiogram_right (Audiogram): The audiogram for the right ear
+    """
+
+    audiogram_left: Audiogram
+    audiogram_right: Audiogram
+    id: str = ""
+
+    @staticmethod
+    def from_dict(listener_dict: dict) -> Listener:
+        """Create a Listener from a dict.
+
+        The dict structure and fields are based on those used
+        in the Clarity metadata files.
+
+        Args:
+            listener_dict (dict): The listener dict
+
+        Returns:
+            Listener: The listener
+
+        """
+        return Listener(
+            id=listener_dict["name"],
+            audiogram_left=Audiogram(
+                levels=listener_dict["audiogram_levels_l"],
+                frequencies=listener_dict["audiogram_cfs"],
+            ),
+            audiogram_right=Audiogram(
+                levels=listener_dict["audiogram_levels_r"],
+                frequencies=listener_dict["audiogram_cfs"],
+            ),
+        )
+
+    @staticmethod
+    def load_listener_dict(filename: Path) -> dict[str, Listener]:
+        """Read a Clarity Listener dict file.
+
+        The standard Clarity metadata files presents listeners as a
+        dictionary of listeners, keyed by listener ID.
+
+        Args:
+            filename (Path): The path to the listener dict file
+
+        Returns:
+            dict[str, Listener]: A dict of listeners keyed by id
+
+        """
+        with open(filename, encoding="utf-8") as fp:
+            listeners_raw = json.load(fp)
+
+        listeners = {}
+        for listener_id, listener_dict in listeners_raw.items():
+            listeners[listener_id] = Listener.from_dict(listener_dict)
+        return listeners
diff --git a/recipes/cad1/task1/baseline/enhance.py b/recipes/cad1/task1/baseline/enhance.py
index e970e0d98..1daf24a92 100644
--- a/recipes/cad1/task1/baseline/enhance.py
+++ b/recipes/cad1/task1/baseline/enhance.py
@@ -19,7 +19,7 @@
 
 from clarity.enhancer.compressor import Compressor
 from clarity.enhancer.nalr import NALR
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 from clarity.utils.signal_processing import denormalize_signals, normalize_signal
 from recipes.cad1.task1.baseline.evaluate import make_song_listener_list
 
@@ -147,8 +147,7 @@ def decompose_signal(
     signal: np.ndarray,
     sample_rate: int,
     device: torch.device,
-    left_audiogram: Audiogram,
-    right_audiogram: Audiogram,
+    listener: Listener,
 ) -> dict[str, np.ndarray]:
     """
     Decompose signal into 8 stems.
@@ -164,8 +163,7 @@ def decompose_signal(
         signal (np.ndarray): Signal to be decomposed.
         sample_rate (int): Sample frequency.
         device (torch.device): Torch device to use for processing.
-        left_audiogram (Audiogram): Left ear audiogram.
-        right_audiogram (Audiogram): Right ear audiogram.
+        listener (Listener).
 
      Returns:
          Dictionary: Indexed by sources with the associated model as values.
@@ -215,7 +213,8 @@ def apply_baseline_ha(
     Returns:
         An ndarray representing the processed signal.
     """
-    nalr_fir, _ = enhancer.build(audiogram.levels, audiogram.frequencies)
+    print("XXX", audiogram)
+    nalr_fir, _ = enhancer.build(audiogram)
     proc_signal = enhancer.apply(nalr_fir, signal)
     if apply_compressor:
         proc_signal, _, _ = compressor.process(proc_signal)
@@ -226,8 +225,7 @@ def process_stems_for_listener(
     stems: dict,
     enhancer: NALR,
     compressor: Compressor,
-    audiogram_left: Audiogram,
-    audiogram_right: Audiogram,
+    listener: Listener,
     apply_compressor: bool = False,
 ) -> dict:
     """Process the stems from sources.
@@ -236,8 +234,7 @@ def process_stems_for_listener(
         stems (dict) : Dictionary of stems
         enhancer (NALR) : NAL-R prescription hearing aid
         compressor (Compressor) : Compressor
-        audiogram_left (np.ndarray) : Left channel audiogram
-        audiogram_right (np.ndarray) : Right channel audiogram
+        listener: Listener object
         cfs (np.ndarray) : Center frequencies
         apply_compressor (bool) : Whether to apply the compressor
     Returns:
@@ -250,7 +247,11 @@ def process_stems_for_listener(
         stem_signal = stems[stem_str]
 
         # Determine the audiogram to use
-        audiogram = audiogram_left if stem_str.startswith("l") else audiogram_right
+        audiogram = (
+            listener.audiogram_left
+            if stem_str.startswith("l")
+            else listener.audiogram_right
+        )
 
         # Apply NALR prescription to stem_signal
         proc_signal = apply_baseline_ha(
@@ -327,15 +328,14 @@ def enhance(config: DictConfig) -> None:
 
     # Processing Validation Set
     # Load listener audiograms and songs
-    with open(config.path.listeners_valid_file, encoding="utf-8") as file:
-        listener_valid_audiograms = json.load(file)
+    listener_dict = Listener.load_listener_dict(config.path.listeners_valid_file)
 
     with open(config.path.music_valid_file, encoding="utf-8") as file:
         song_data = json.load(file)
     songs_valid = pd.DataFrame.from_dict(song_data)
 
     valid_song_listener_pairs = make_song_listener_list(
-        songs_valid["Track Name"], listener_valid_audiograms
+        songs_valid["Track Name"], listener_dict
     )
     # Select a batch to process
     valid_song_listener_pairs = valid_song_listener_pairs[
@@ -356,7 +356,7 @@ def enhance(config: DictConfig) -> None:
             f"Processing {song_name} for {listener_name}..."
         )
         # Get the listener's audiogram
-        listener_info = listener_valid_audiograms[listener_name]
+        listener = listener_dict[listener_name]
 
         # Find the music split directory
         split_directory = (
@@ -366,16 +366,6 @@ def enhance(config: DictConfig) -> None:
             else "train"
         )
 
-        critical_frequencies = np.array(listener_info["audiogram_cfs"])
-        audiogram_left = Audiogram(
-            levels=np.array(listener_info["audiogram_levels_l"]),
-            frequencies=critical_frequencies,
-        )
-        audiogram_right = Audiogram(
-            levels=np.array(listener_info["audiogram_levels_r"]),
-            frequencies=critical_frequencies,
-        )
-
         # Read the mixture signal
         # Convert to 32-bit floating point and transpose
         # from [samples, channels] to [channels, samples]
@@ -398,8 +388,7 @@ def enhance(config: DictConfig) -> None:
                 mixture_signal,
                 sample_rate,
                 device,
-                audiogram_left,
-                audiogram_right,
+                listener,
             )
 
         # Baseline applies NALR prescription to each stem instead of using the
@@ -409,8 +398,7 @@ def enhance(config: DictConfig) -> None:
             stems,
             enhancer,
             compressor,
-            audiogram_left,
-            audiogram_right,
+            listener,
             config.apply_compressor,
         )
 
@@ -425,9 +413,9 @@ def enhance(config: DictConfig) -> None:
 
             filename = (
                 enhanced_folder
-                / f"{listener_info['name']}"
+                / f"{listener.id}"
                 / f"{song_name}"
-                / f"{listener_info['name']}_{song_name}_{stem_str}.wav"
+                / f"{listener.id}_{song_name}_{stem_str}.wav"
             )
             filename.parent.mkdir(parents=True, exist_ok=True)
 
@@ -440,9 +428,9 @@ def enhance(config: DictConfig) -> None:
         enhanced = np.stack([output_left, output_right], axis=1)
         filename = (
             enhanced_folder
-            / f"{listener_info['name']}"
+            / f"{listener.id}"
             / f"{song_name}"
-            / f"{listener_info['name']}_{song_name}_remix.wav"
+            / f"{listener.id}_{song_name}_remix.wav"
         )
 
         # clip and save enhanced signal
diff --git a/recipes/cad1/task1/baseline/evaluate.py b/recipes/cad1/task1/baseline/evaluate.py
index a89f6982e..469513bc6 100644
--- a/recipes/cad1/task1/baseline/evaluate.py
+++ b/recipes/cad1/task1/baseline/evaluate.py
@@ -9,7 +9,6 @@
 import json
 import logging
 from pathlib import Path
-from typing import Any
 
 import hydra
 import numpy as np
@@ -18,7 +17,7 @@
 from scipy.io import wavfile
 
 from clarity.evaluator.haaqi import compute_haaqi
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Listener
 from clarity.utils.signal_processing import compute_rms
 
 logger = logging.getLogger(__name__)
@@ -63,7 +62,7 @@ def write_header(self):
 
     def add_result(
         self,
-        listener: str,
+        listener_id: str,
         song: str,
         score: float,
         instruments_scores: dict[str, float],
@@ -71,7 +70,7 @@ def add_result(
         """Add a result to the CSV file.
 
         Args:
-            listener (str): The name of the listener who submitted the result.
+            listener_id (str): The name of the listener who submitted the result.
             song (str): The name of the song that the result is for.
             score (float): The combined score for the result.
             instruments_scores (dict): A dictionary of scores for each instrument
@@ -86,7 +85,7 @@ def add_result(
             csv_writer.writerow(
                 [
                     song,
-                    listener,
+                    listener_id,
                     str(score),
                     str(instruments_scores["left_bass"]),
                     str(instruments_scores["right_bass"]),
@@ -108,7 +107,7 @@ def set_song_seed(song: str) -> None:
 
 
 def make_song_listener_list(
-    songs: list[str], listeners: dict[str, Any], small_test: bool = False
+    songs: list[str], listeners: dict[str, Listener], small_test: bool = False
 ) -> list[tuple[str, str]]:
     """Make the list of scene-listener pairing to process"""
     song_listener_pairs = list(itertools.product(songs, listeners.keys()))
@@ -121,10 +120,9 @@ def make_song_listener_list(
 
 def _evaluate_song_listener(
     song: str,
-    listener: str,
+    listener: Listener,
     config: DictConfig,
     split_dir: str,
-    listener_audiograms: dict,
     enhanced_folder: Path,
 ) -> tuple[float, dict]:
     """Evaluate a single song-listener pair
@@ -134,7 +132,6 @@ def _evaluate_song_listener(
         listener (str): The name of the listener to evaluate.
         config (DictConfig): The configuration object.
         split_dir (str): The name of the split directory.
-        listener_audiograms (dict): A dictionary of audiograms for each listener.
         enhanced_folder (Path): The path to the folder containing the enhanced signals.
 
     Returns:
@@ -144,7 +141,7 @@ def _evaluate_song_listener(
 
     """
 
-    logger.info(f"Evaluating {song} for {listener}")
+    logger.info(f"Evaluating {song} for {listener.id}")
 
     if config.evaluate.set_random_seed:
         set_song_seed(song)
@@ -168,18 +165,18 @@ def _evaluate_song_listener(
         # Load left channel
         sample_rate_left_enhanced_signal, left_enhanced_signal = wavfile.read(
             enhanced_folder
-            / f"{listener}"
+            / f"{listener.id}"
             / f"{song}"
-            / f"{listener}_{song}_left_{instrument}.wav"
+            / f"{listener.id}_{song}_left_{instrument}.wav"
         )
         left_enhanced_signal = (left_enhanced_signal / 32768.0).astype(np.float32)
 
         # Load right channel
         sample_rate_right_enhanced_signal, right_enhanced_signal = wavfile.read(
             enhanced_folder
-            / f"{listener}"
+            / f"{listener.id}"
             / f"{song}"
-            / f"{listener}_{song}_right_{instrument}.wav"
+            / f"{listener.id}_{song}_right_{instrument}.wav"
         )
         right_enhanced_signal = (right_enhanced_signal / 32768.0).astype(np.float32)
 
@@ -190,27 +187,17 @@ def _evaluate_song_listener(
             == config.sample_rate
         )
 
-        frequencies = np.array(listener_audiograms["audiogram_cfs"])
-        audiogram_left = Audiogram(
-            levels=np.array(listener_audiograms["audiogram_levels_l"]),
-            frequencies=frequencies,
-        )
-        audiogram_right = Audiogram(
-            levels=np.array(listener_audiograms["audiogram_levels_l"]),
-            frequencies=frequencies,
-        )
-
         per_instrument_score[f"left_{instrument}"] = compute_haaqi(
             left_enhanced_signal,
             reference_signal[:, 0],
-            audiogram_left,
+            listener.audiogram_left,
             config.sample_rate,
             65 - 20 * np.log10(compute_rms(reference_signal[:, 0])),
         )
         per_instrument_score[f"right_{instrument}"] = compute_haaqi(
             right_enhanced_signal,
             reference_signal[:, 1],
-            audiogram_right,
+            listener.audiogram_right,
             config.sample_rate,
             65 - 20 * np.log10(compute_rms(reference_signal[:, 1])),
         )
@@ -230,8 +217,7 @@ def run_calculate_aq(config: DictConfig) -> None:
     songs = pd.DataFrame.from_dict(songs)
 
     # Load listener data
-    with open(config.path.listeners_valid_file, encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
+    listener_dict = Listener.load_listener_dict(config.path.listeners_valid_file)
 
     enhanced_folder = Path("enhanced_signals")
     logger.info(f"Evaluating from {enhanced_folder} directory")
@@ -242,28 +228,27 @@ def run_calculate_aq(config: DictConfig) -> None:
     results_file.write_header()
 
     song_listener_pair = make_song_listener_list(
-        songs["Track Name"].tolist(), listener_audiograms, config.evaluate.small_test
+        songs["Track Name"].tolist(), listener_dict, config.evaluate.small_test
     )
 
     song_listener_pair = song_listener_pair[
         config.evaluate.batch :: config.evaluate.batch_size
     ]
 
-    for song, listener in song_listener_pair:
+    for song, listener_id in song_listener_pair:
         split_dir = "train"
         if songs[songs["Track Name"] == song]["Split"].tolist()[0] == "test":
             split_dir = "test"
-
+        listener = listener_dict[listener_id]
         combined_score, per_instrument_score = _evaluate_song_listener(
             song,
             listener,
             config,
             split_dir,
-            listener_audiograms[listener],
             enhanced_folder,
         )
         results_file.add_result(
-            listener,
+            listener.id,
             song,
             score=combined_score,
             instruments_scores=per_instrument_score,
diff --git a/recipes/cad1/task2/baseline/baseline_utils.py b/recipes/cad1/task2/baseline/baseline_utils.py
index 58688ba2c..fe67c02f3 100644
--- a/recipes/cad1/task2/baseline/baseline_utils.py
+++ b/recipes/cad1/task2/baseline/baseline_utils.py
@@ -11,6 +11,8 @@
 import pandas as pd
 from omegaconf import DictConfig
 
+from clarity.utils.audiogram import Listener
+
 # pylint: disable=import-error
 
 
@@ -72,7 +74,9 @@ def load_hrtf(config: DictConfig) -> dict:
     return hrtf_data[config.evaluate.split]
 
 
-def load_listeners_and_scenes(config: DictConfig) -> tuple[dict, dict, dict]:
+def load_listeners_and_scenes(
+    config: DictConfig,
+) -> tuple[dict, dict[str, Listener], dict]:
     """Load listener and scene data
 
     Args:
@@ -90,16 +94,15 @@ def load_listeners_and_scenes(config: DictConfig) -> tuple[dict, dict, dict]:
     with open(config.path.scenes_file, encoding="utf-8") as fp:
         df_scenes = pd.read_json(fp, orient="index")
 
-    # Load audiograms and scence data for the corresponding split
+    # Load audiograms and scene data for the corresponding split
     if config.evaluate.split == "train":
-        with open(config.path.listeners_train_file, encoding="utf-8") as fp:
-            listener_audiograms = json.load(fp)
+        listeners = Listener.load_listener_dict(config.path.listeners_train_file)
         scenes = df_scenes[df_scenes["split"] == "train"].to_dict("index")
-
     elif config.evaluate.split == "valid":
-        with open(config.path.listeners_valid_file, encoding="utf-8") as fp:
-            listener_audiograms = json.load(fp)
+        listeners = Listener.load_listener_dict(config.path.listeners_valid_file)
         scenes = df_scenes[df_scenes["split"] == "valid"].to_dict("index")
+    else:
+        raise ValueError(f"Unknown split {config.evaluate.split}")
 
     with open(config.path.scenes_listeners_file, encoding="utf-8") as fp:
         scenes_listeners = json.load(fp)
@@ -107,7 +110,7 @@ def load_listeners_and_scenes(config: DictConfig) -> tuple[dict, dict, dict]:
             k: v for k, v in scenes_listeners.items() if k in scenes.keys()
         }
 
-    return scenes, listener_audiograms, scenes_listeners
+    return scenes, listeners, scenes_listeners
 
 
 def make_scene_listener_list(scenes_listeners, small_test=False):
diff --git a/recipes/cad1/task2/baseline/car_scene_acoustics.py b/recipes/cad1/task2/baseline/car_scene_acoustics.py
index c608dfe6c..9fe8cc3e1 100644
--- a/recipes/cad1/task2/baseline/car_scene_acoustics.py
+++ b/recipes/cad1/task2/baseline/car_scene_acoustics.py
@@ -15,6 +15,7 @@
 
 from clarity.enhancer.compressor import Compressor
 from clarity.enhancer.nalr import NALR
+from clarity.utils.audiogram import Audiogram, Listener
 from clarity.utils.car_noise_simulator.carnoise_signal_generator import (
     CarNoiseSignalGenerator,
 )
@@ -104,9 +105,7 @@ def preload_anechoic_hrtf(self, hrtf_dir: str) -> None:
         for key, item in self.ANECHOIC_HRTF_FOR_NOISE.items():
             self.hrir_for_noise[key] = wavfile.read(anechoic_hrtf_dir / item)[1]
 
-    def apply_hearing_aid(
-        self, signal: np.ndarray, audiogram: np.ndarray, center_frequencies: np.ndarray
-    ) -> np.ndarray:
+    def apply_hearing_aid(self, signal: np.ndarray, audiogram: Audiogram) -> np.ndarray:
         """
         Applies the hearing aid:
         It consists in NALR prescription and Compressor
@@ -120,7 +119,7 @@ def apply_hearing_aid(
         Returns:
             np.ndarray: The enhanced audio signal.
         """
-        nalr_fir, _ = self.enhancer.build(audiogram, center_frequencies)
+        nalr_fir, _ = self.enhancer.build(audiogram)
         signal = self.enhancer.apply(nalr_fir, signal)
         signal, _, _ = self.compressor.process(signal)
         return signal
@@ -303,7 +302,7 @@ def apply_car_acoustics_to_signal(
         self,
         enh_signal: np.ndarray,
         scene: dict,
-        listener: dict,
+        listener: Listener,
         hrtf: dict,
         audio_manager: AudioManager,
         config: DictConfig,
@@ -314,7 +313,7 @@ def apply_car_acoustics_to_signal(
         Args:
             enh_signal (np.ndarray): The enhanced signal to apply the car acoustics to.
             scene (dict): The scene dictionary with the acoustics parameters.
-            listener (dict): The listener dictionary with the audiograms.
+            listener (Listener): The listener characteristics.
             hrtf (dict): A dictionary containing the head-related transfer functions
                 (HRTFs) for the listener being evaluated. This includes the left and
                 right HRTFs for the car and the anechoic room.
@@ -371,15 +370,11 @@ def apply_car_acoustics_to_signal(
 
         # 5. Apply Hearing Aid to Left and Right channels and join them
         processed_signal_left = self.apply_hearing_aid(
-            processed_signal[0, :],
-            np.array(listener["audiogram_levels_l"]),
-            np.array(listener["audiogram_cfs"]),
+            processed_signal[0, :], listener.audiogram_left
         )
 
         processed_signal_right = self.apply_hearing_aid(
-            processed_signal[1, :],
-            np.array(listener["audiogram_levels_r"]),
-            np.array(listener["audiogram_cfs"]),
+            processed_signal[1, :], listener.audiogram_right
         )
 
         processed_signal = np.stack(
diff --git a/recipes/cad1/task2/baseline/enhance.py b/recipes/cad1/task2/baseline/enhance.py
index 717e30de5..eda3ac47e 100644
--- a/recipes/cad1/task2/baseline/enhance.py
+++ b/recipes/cad1/task2/baseline/enhance.py
@@ -15,6 +15,7 @@
 from scipy.io import wavfile
 from tqdm import tqdm
 
+from clarity.utils.audiogram import Listener
 from recipes.cad1.task2.baseline.baseline_utils import (
     make_scene_listener_list,
     read_mp3,
@@ -24,7 +25,7 @@
 logger = logging.getLogger(__name__)
 
 
-def compute_average_hearing_loss(listener: dict) -> float:
+def compute_average_hearing_loss(listener: Listener) -> float:
     """
     Compute the average hearing loss of a listener.
 
@@ -35,23 +36,15 @@ def compute_average_hearing_loss(listener: dict) -> float:
         average_hearing_loss (float): The average hearing loss of the listener.
 
     """
-    CFS: Final = (500, 1000, 2000, 4000)
-    left_loss = [
-        listener["audiogram_levels_l"][i]
-        for i in range(len(listener["audiogram_cfs"]))
-        if listener["audiogram_cfs"][i] in CFS
-    ]
-    right_loss = [
-        listener["audiogram_levels_r"][i]
-        for i in range(len(listener["audiogram_cfs"]))
-        if listener["audiogram_cfs"][i] in CFS
-    ]
-    return max(float(np.mean(left_loss)), float(np.mean(right_loss)))
+    CFS: Final = np.array([500, 1000, 2000, 4000])
+    left_levels = listener.audiogram_left.resample(CFS).levels
+    right_levels = listener.audiogram_right.resample(CFS).levels
+    return max(float(np.mean(left_levels)), float(np.mean(right_levels)))
 
 
 def enhance_song(
     waveform: np.ndarray,
-    listener_audiograms: dict,
+    listener: Listener,
     config: DictConfig,
 ) -> tuple[np.ndarray, np.ndarray]:
     """
@@ -63,7 +56,7 @@ def enhance_song(
 
     Args:
         waveform (np.ndarray): The waveform of the song.
-        listener_audiograms (dict): The audiograms of the listener.
+        listener_dict (dict): The audiograms of the listener.
         config (dict): Dictionary of configuration options for enhancing music.
 
     Returns:
@@ -74,7 +67,7 @@ def enhance_song(
     meter = pyln.Meter(config.sample_rate)
     original_loudness = meter.integrated_loudness(waveform.T)
 
-    average_hearing_loss = compute_average_hearing_loss(listener_audiograms)
+    average_hearing_loss = compute_average_hearing_loss(listener)
 
     extra_reduction = (
         (average_hearing_loss - 50) / 5 if (average_hearing_loss - 50) / 5 > 0 else 0
@@ -109,7 +102,7 @@ def enhance(config: DictConfig) -> None:
     enhanced_folder = Path("enhanced_signals") / config.evaluate.split
 
     # Load scenes and listeners depending on config.evaluate.split
-    scenes, listener_audiograms, scenes_listeners = load_listeners_and_scenes(config)
+    scenes, listener_dict, scenes_listeners = load_listeners_and_scenes(config)
     scene_listener_pairs = make_scene_listener_list(
         scenes_listeners, config.evaluate.small_test
     )
@@ -119,20 +112,20 @@ def enhance(config: DictConfig) -> None:
 
     for scene_id, listener_id in tqdm(scene_listener_pairs):
         current_scene = scenes[scene_id]
-        listener = listener_audiograms[listener_id]
+        listener = listener_dict[listener_id]
 
         song_path = Path(config.path.music_dir) / f"{current_scene['song_path']}"
 
         # Read song
         song_waveform, _ = read_mp3(song_path, config.sample_rate)
         out_l, out_r = enhance_song(
-            waveform=song_waveform, listener_audiograms=listener, config=config
+            waveform=song_waveform, listener=listener, config=config
         )
 
         enhanced = np.stack([out_l, out_r], axis=1)
-        filename = f"{scene_id}_{listener['name']}_{current_scene['song']}.wav"
+        filename = f"{scene_id}_{listener.id}_{current_scene['song']}.wav"
 
-        enhanced_folder_listener = enhanced_folder / f"{listener['name']}"
+        enhanced_folder_listener = enhanced_folder / f"{listener.id}"
         enhanced_folder_listener.mkdir(parents=True, exist_ok=True)
 
         # Clip and save
diff --git a/recipes/cad1/task2/baseline/evaluate.py b/recipes/cad1/task2/baseline/evaluate.py
index 8f5ffbf32..b4413ef9f 100644
--- a/recipes/cad1/task2/baseline/evaluate.py
+++ b/recipes/cad1/task2/baseline/evaluate.py
@@ -15,7 +15,7 @@
 from tqdm import tqdm
 
 from clarity.evaluator.haaqi import compute_haaqi
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Listener
 from recipes.cad1.task2.baseline.audio_manager import AudioManager
 from recipes.cad1.task2.baseline.baseline_utils import (
     load_hrtf,
@@ -120,7 +120,7 @@ def evaluate_scene(
     sample_rate: int,
     scene_id: str,
     current_scene: dict,
-    listener_audiogram: dict,
+    listener: Listener,
     car_scene_acoustic: CarSceneAcoustics,
     hrtf: dict,
     config: DictConfig,
@@ -137,9 +137,7 @@ def evaluate_scene(
         current_scene (dict): A dictionary containing information about the scene being
             evaluated, including the song ID, the listener ID, the car noise type, and
             the split.
-        listener_audiogram (dict): A dictionary containing the listener's audiogram
-            data, including the center frequencies (cfs) and the hearing levels for both
-            ears (audiogram_levels_l and audiogram_levels_r).
+        listener (Listener): the listener to use
         car_scene_acoustic (CarSceneAcoustics): An instance of the CarSceneAcoustics
             class, which is used to generate car noise and add binaural room impulse
             responses (BRIRs) to the enhanced signal.
@@ -157,7 +155,7 @@ def evaluate_scene(
     """
     audio_manager = AudioManager(
         output_audio_path=Path("evaluation_signals")
-        / f"{listener_audiogram['name']}"
+        / f"{listener.id}"
         / f"{current_scene['song']}",
         sample_rate=sample_rate,
         soft_clip=config.soft_clip,
@@ -171,7 +169,7 @@ def evaluate_scene(
     processed_signal = car_scene_acoustic.apply_car_acoustics_to_signal(
         enh_signal,
         current_scene,
-        listener_audiogram,
+        listener,
         hrtf,
         audio_manager,
         config,
@@ -194,18 +192,17 @@ def evaluate_scene(
 
     audio_manager.save_audios()
 
-    freqs = np.array(listener_audiogram["audiogram_cfs"])
     # Compute HAAQI scores
     aq_score_l = compute_haaqi(
         processed_signal[0, :],
         ref_signal[0, :],
-        Audiogram(levels=listener_audiogram["audiogram_levels_l"], frequencies=freqs),
+        listener.audiogram_left,
         sample_rate,
     )
     aq_score_r = compute_haaqi(
         processed_signal[1, :],
         ref_signal[1, :],
-        Audiogram(levels=listener_audiogram["audiogram_levels_r"], frequencies=freqs),
+        listener.audiogram_right,
         sample_rate,
     )
     return aq_score_l, aq_score_r
@@ -216,7 +213,7 @@ def run_calculate_audio_quality(config: DictConfig) -> None:
     """Evaluate the enhanced signals using the HAAQI metric."""
 
     # Load scenes and listeners depending on config.evaluate.split
-    scenes, listener_audiograms, scenes_listeners = load_listeners_and_scenes(config)
+    scenes, listener_dict, scenes_listeners = load_listeners_and_scenes(config)
     scene_listener_pairs = make_scene_listener_list(
         scenes_listeners, config.evaluate.small_test
     )
@@ -249,7 +246,7 @@ def run_calculate_audio_quality(config: DictConfig) -> None:
         current_scene = scenes[scene_id]
 
         # Retrieve audiograms
-        listener = listener_audiograms[listener_id]
+        listener = listener_dict[listener_id]
 
         # Retrieve HRTF according to the listener's head orientation
         hrtf_scene = hrtfs[str(current_scene["hr"])]
@@ -265,9 +262,9 @@ def run_calculate_audio_quality(config: DictConfig) -> None:
 
         # Load enhanced signal
         enhanced_folder = Path("enhanced_signals") / config.evaluate.split
-        enhanced_song_id = f"{scene_id}_{listener['name']}_{current_scene['song']}"
+        enhanced_song_id = f"{scene_id}_{listener.id}_{current_scene['song']}"
         enhanced_song_path = (
-            enhanced_folder / f"{listener['name']}" / f"{enhanced_song_id}.wav"
+            enhanced_folder / f"{listener.id}" / f"{enhanced_song_id}.wav"
         )
 
         # Read WAV enhanced signal using scipy.io.wavfile
@@ -294,7 +291,7 @@ def run_calculate_audio_quality(config: DictConfig) -> None:
             scene_id,
             current_scene["song"],
             current_scene["song_path"].split("/")[-2],
-            listener_id,
+            listener.id,
             score=float(score),
             haaqi_left=aq_score_l,
             haaqi_right=aq_score_r,
diff --git a/recipes/cec1/baseline/enhance.py b/recipes/cec1/baseline/enhance.py
index b96d651d3..595c4833d 100644
--- a/recipes/cec1/baseline/enhance.py
+++ b/recipes/cec1/baseline/enhance.py
@@ -2,12 +2,11 @@
 from pathlib import Path
 
 import hydra
-import numpy as np
 from omegaconf import DictConfig
 from tqdm import tqdm
 
 from clarity.enhancer.gha.gha_interface import GHAHearingAid
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Listener
 
 
 @hydra.main(config_path=".", config_name="config")
@@ -16,29 +15,12 @@ def enhance(cfg: DictConfig) -> None:
     enhanced_folder.mkdir(parents=True, exist_ok=True)
     with open(cfg.path.scenes_listeners_file, encoding="utf-8") as fp:
         scenes_listeners = json.load(fp)
-    with open(cfg.path.listeners_file, encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
-
+    listener_dict = Listener.load_listener_dict(cfg.path.listeners_file)
     enhancer = GHAHearingAid(**cfg["GHAHearingAid"])
 
     for scene in tqdm(scenes_listeners):
-        for listener in scenes_listeners[scene]:
-            # retrieve audiograms
-            frequencies = np.array(listener_audiograms[listener]["audiogram_cfs"])
-            audiogram_left_levels = np.array(
-                listener_audiograms[listener]["audiogram_levels_l"]
-            )
-            audiogram_right_levels = np.array(
-                listener_audiograms[listener]["audiogram_levels_r"]
-            )
-            audiogram_left = Audiogram(
-                frequencies=frequencies, levels=audiogram_left_levels
-            )
-
-            audiogram_right = Audiogram(
-                frequencies=frequencies,
-                levels=audiogram_right_levels,
-            )
+        for listener_id in scenes_listeners[scene]:
+            listener = listener_dict[listener_id]
 
             infile_names = [
                 f"{cfg.path.scenes_folder}/{scene}_mixed_CH{ch}.wav"
@@ -47,9 +29,7 @@ def enhance(cfg: DictConfig) -> None:
 
             enhancer.process_files(
                 infile_names=infile_names,
-                outfile_name=f"{enhanced_folder}/{scene}_{listener}_HA-output.wav",
-                audiogram_left=audiogram_left,
-                audiogram_right=audiogram_right,
+                outfile_name=f"{enhanced_folder}/{scene}_{listener_id}_HA-output.wav",
                 listener=listener,
             )
 
diff --git a/recipes/cec1/baseline/evaluate.py b/recipes/cec1/baseline/evaluate.py
index 5406e9b01..5e6b66e1c 100644
--- a/recipes/cec1/baseline/evaluate.py
+++ b/recipes/cec1/baseline/evaluate.py
@@ -5,6 +5,7 @@
 
 import hydra
 import numpy as np
+from numpy import ndarray
 from omegaconf import DictConfig
 from scipy.signal import unit_impulse
 from tqdm import tqdm
@@ -13,22 +14,23 @@
 from clarity.evaluator.mbstoi.mbstoi_utils import find_delay_impulse
 from clarity.evaluator.msbg.msbg import Ear
 from clarity.evaluator.msbg.msbg_utils import MSBG_FS, pad
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import AUDIOGRAM_REF_CLARITY, Listener
 from clarity.utils.file_io import read_signal, write_signal
 
 
-def listen(ear, signal, audiogram_l, audiogram_r):
+def listen(ear, signal: ndarray, listener: Listener):
     """
     Generate MSBG processed signal
     :param ear: MSBG ear
     :param wav: binaural signal
     :return: binaural signal
     """
-    ear.set_audiogram(audiogram_l)
+    ear.set_audiogram(listener.audiogram_left)
     out_l = ear.process(signal[:, 0])
 
-    ear.set_audiogram(audiogram_r)
+    ear.set_audiogram(listener.audiogram_right)
     out_r = ear.process(signal[:, 1])
+
     if len(out_l[0]) != len(out_r[0]):
         diff = len(out_l[0]) - len(out_r[0])
         if diff > 0:
@@ -44,23 +46,26 @@ def run_HL_processing(cfg: DictConfig) -> None:
     output_path.mkdir(parents=True, exist_ok=True)
     with open(cfg.path.scenes_listeners_file, encoding="utf-8") as fp:
         scenes_listeners = json.load(fp)
-    with open(cfg.path.listeners_file, encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
+    listener_dict = Listener.load_listener_dict(cfg.path.listeners_file)
     enhanced_folder = Path(cfg.path.enhanced_signals)
 
     # initialize ear
     ear = Ear(**cfg.MSBGEar)
 
+    # Make reference listener with flat audiogram
+    listener_ref = Listener(AUDIOGRAM_REF_CLARITY, AUDIOGRAM_REF_CLARITY)
+
     for scene in tqdm(scenes_listeners):
-        for listener in scenes_listeners[scene]:
+        for listener_id in scenes_listeners[scene]:
             if enhanced_folder.exists():
-                signal_file = enhanced_folder / f"{scene}_{listener}_HA-output.wav"
+                signal_file = enhanced_folder / f"{scene}_{listener_id}_HA-output.wav"
             # if no enhanced signals, use the unprocessed signal for si calculation
             else:
                 signal_file = Path(cfg.path.scenes_folder) / f"{scene}_mixed_CH0.wav"
+            listener = listener_dict[listener_id]
 
             # signals to write
-            outfile_stem = f"{output_path}/{scene}_{listener}"
+            outfile_stem = f"{output_path}/{scene}_{listener_id}"
             signal_files_to_write = [
                 f"{output_path}/{scene}_flat0dB_HL-output.wav",
                 f"{outfile_stem}_HL-output.wav",
@@ -76,32 +81,16 @@ def run_HL_processing(cfg: DictConfig) -> None:
                 Path(cfg.path.scenes_folder) / f"{scene}_mixed_CH0.wav"
             )
 
-            # retrieve audiograms
-            cfs = np.array(listener_audiograms[listener]["audiogram_cfs"])
-            audiogram_left = np.array(
-                listener_audiograms[listener]["audiogram_levels_l"]
-            )
-            left_audiogram = Audiogram(frequencies=cfs, levels=audiogram_left)
-            audiogram_right = np.array(
-                listener_audiograms[listener]["audiogram_levels_r"]
-            )
-            right_audiogram = Audiogram(frequencies=cfs, levels=audiogram_right)
-
             # Create discrete delta function (DDF) signal for time alignment
             ddf_signal = np.zeros(np.shape(signal))
             ddf_signal[:, 0] = unit_impulse(len(signal), int(MSBG_FS / 2))
             ddf_signal[:, 1] = unit_impulse(len(signal), int(MSBG_FS / 2))
 
-            # Get flat-0dB ear audiograms
-            flat0dB_audiogram = Audiogram(
-                frequencies=cfs, levels=np.zeros(np.shape(cfs))
-            )
-
             signals_to_write = [
-                listen(ear, ddf_signal, flat0dB_audiogram, flat0dB_audiogram),
-                listen(ear, signal, left_audiogram, right_audiogram),
-                listen(ear, ddf_signal, left_audiogram, right_audiogram),
-                listen(ear, mixture_signal, left_audiogram, right_audiogram),
+                listen(ear, ddf_signal, listener_ref),
+                listen(ear, signal, listener),
+                listen(ear, ddf_signal, listener),
+                listen(ear, mixture_signal, listener),
             ]
 
             for signal, out_signal_file in zip(signals_to_write, signal_files_to_write):
diff --git a/recipes/cec2/baseline/enhance.py b/recipes/cec2/baseline/enhance.py
index 252d7790a..99267916c 100644
--- a/recipes/cec2/baseline/enhance.py
+++ b/recipes/cec2/baseline/enhance.py
@@ -10,6 +10,7 @@
 
 from clarity.enhancer.compressor import Compressor
 from clarity.enhancer.nalr import NALR
+from clarity.utils.audiogram import Listener
 
 logger = logging.getLogger(__name__)
 
@@ -20,38 +21,30 @@ def enhance(cfg: DictConfig) -> None:
     enhanced_folder.mkdir(parents=True, exist_ok=True)
     with Path(cfg.path.scenes_listeners_file).open("r", encoding="utf-8") as fp:
         scenes_listeners = json.load(fp)
-    with Path(cfg.path.listeners_file).open("r", encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
+    listener_dict = Listener.load_listener_dict(cfg.path.listeners_file)
 
     enhancer = NALR(**cfg.nalr)
     compressor = Compressor(**cfg.compressor)
 
     for scene in tqdm(scenes_listeners):
-        for listener in scenes_listeners[scene]:
-            # retrieve audiograms
-            cfs = np.array(listener_audiograms[listener]["audiogram_cfs"])
-            audiogram_left = np.array(
-                listener_audiograms[listener]["audiogram_levels_l"]
-            )
-            audiogram_right = np.array(
-                listener_audiograms[listener]["audiogram_levels_r"]
-            )
+        for listener_id in scenes_listeners[scene]:
+            listener = listener_dict[listener_id]
 
             sample_rate, signal = wavfile.read(
                 Path(cfg.path.scenes_folder) / f"{scene}_mix_CH1.wav"
             )
             signal = signal / 32768.0
             assert sample_rate == cfg.nalr.sample_rate
-            nalr_fir, _ = enhancer.build(audiogram_left, cfs)
+            nalr_fir, _ = enhancer.build(listener.audiogram_left)
             out_l = enhancer.apply(nalr_fir, signal[:, 0])
 
-            nalr_fir, _ = enhancer.build(audiogram_right, cfs)
+            nalr_fir, _ = enhancer.build(listener.audiogram_right)
             out_r = enhancer.apply(nalr_fir, signal[:, 1])
 
             out_l, _, _ = compressor.process(out_l)
             out_r, _, _ = compressor.process(out_r)
             enhanced = np.stack([out_l, out_r], axis=1)
-            filename = f"{scene}_{listener}_HA-output.wav"
+            filename = f"{scene}_{listener_id}_HA-output.wav"
 
             if cfg.soft_clip:
                 enhanced = np.tanh(enhanced)
diff --git a/recipes/cec2/baseline/evaluate.py b/recipes/cec2/baseline/evaluate.py
index b3f21e2c6..db56c2239 100644
--- a/recipes/cec2/baseline/evaluate.py
+++ b/recipes/cec2/baseline/evaluate.py
@@ -3,6 +3,7 @@
 import json
 import logging
 from pathlib import Path
+from typing import Dict
 
 import hydra
 import numpy as np
@@ -11,12 +12,12 @@
 from tqdm import tqdm
 
 from clarity.evaluator.haspi import haspi_v2_be
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Listener
 
 logger = logging.getLogger(__name__)
 
 
-def read_csv_scores(file: Path):
+def read_csv_scores(file: Path) -> Dict[str, float]:
     score_dict = {}
     with file.open("r", encoding="utf-8") as fp:
         reader = csv.reader(fp)
@@ -30,8 +31,8 @@ def read_csv_scores(file: Path):
 def run_calculate_SI(cfg: DictConfig) -> None:
     with Path(cfg.path.scenes_listeners_file).open("r", encoding="utf-8") as fp:
         scenes_listeners = json.load(fp)
-    with Path(cfg.path.listeners_file).open("r", encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
+
+    listener_dict = Listener.load_listener_dict(cfg.path.listeners_file)
     Path(cfg.path.exp_folder).mkdir(parents=True, exist_ok=True)
 
     enhanced_folder = Path(cfg.path.exp_folder) / "enhanced_signals"
@@ -57,32 +58,19 @@ def run_calculate_SI(cfg: DictConfig) -> None:
         return
 
     for scene in tqdm(scenes_listeners):
-        for listener in scenes_listeners[scene]:
-            logger.info(f"Running SI calculation: scene {scene}, listener {listener}")
+        for listener_id in scenes_listeners[scene]:
+            logger.info(
+                f"Running SI calculation: scene {scene}, listener {listener_id}"
+            )
             if cfg.evaluate.set_random_seed:
                 scene_md5 = int(hashlib.md5(scene.encode("utf-8")).hexdigest(), 16) % (
                     10**8
                 )
                 np.random.seed(scene_md5)
-
-            # retrieve audiograms
-            audiogram_frequencies = np.array(
-                listener_audiograms[listener]["audiogram_cfs"]
-            )
-
-            audiogram_left = Audiogram(
-                levels=listener_audiograms[listener]["audiogram_levels_l"],
-                frequencies=audiogram_frequencies,
-            )
-
-            audiogram_right = Audiogram(
-                levels=listener_audiograms[listener]["audiogram_levels_r"],
-                frequencies=audiogram_frequencies,
-            )
-
+            listener = listener_dict[listener_id]
             # retrieve signals
             fs_proc, proc = wavfile.read(
-                enhanced_folder / f"{scene}_{listener}_HA-output.wav"
+                enhanced_folder / f"{scene}_{listener_id}_HA-output.wav"
             )
 
             fs_ref_anechoic, ref_anechoic = wavfile.read(
@@ -109,11 +97,10 @@ def run_calculate_SI(cfg: DictConfig) -> None:
                 processed_left=proc[:, 0],
                 processed_right=proc[:, 1],
                 sample_rate=fs_ref_anechoic,
-                audiogram_left=audiogram_left,
-                audiogram_right=audiogram_right,
+                listener=listener,
             )
             logger.info(f"The HASPI score is {si}")
-            csv_lines.append([scene, listener, str(si)])
+            csv_lines.append([scene, listener_id, str(si)])
 
             if cfg.evaluate.cal_unprocessed_si:
                 if cfg.evaluate.set_random_seed:
@@ -132,11 +119,10 @@ def run_calculate_SI(cfg: DictConfig) -> None:
                     processed_left=unproc[:, 0],
                     processed_right=unproc[:, 1],
                     sample_rate=fs_ref_anechoic,
-                    audiogram_left=audiogram_left,
-                    audiogram_right=audiogram_right,
+                    listener=listener,
                 )
                 logger.info(f"The unprocessed signal HASPI score is {si_unproc}")
-                unproc_csv_lines.append([scene, listener, str(si_unproc)])
+                unproc_csv_lines.append([scene, listener_id, str(si_unproc)])
 
     with si_file.open("w", encoding="utf-8") as csv_f:
         csv_writer = csv.writer(
diff --git a/recipes/cpc1/baseline/run.py b/recipes/cpc1/baseline/run.py
index 2b930a95b..fb084ef0a 100644
--- a/recipes/cpc1/baseline/run.py
+++ b/recipes/cpc1/baseline/run.py
@@ -1,5 +1,5 @@
-import csv
-import json
+import csv  # pylint: disable=ungrouped-imports
+import json  # pylint: disable=ungrouped-imports
 import logging
 from pathlib import Path
 
@@ -13,24 +13,26 @@
 from clarity.evaluator.mbstoi.mbstoi_utils import find_delay_impulse
 from clarity.evaluator.msbg.msbg import Ear
 from clarity.evaluator.msbg.msbg_utils import MSBG_FS, pad
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import AUDIOGRAM_REF_CLARITY, Listener
 from clarity.utils.file_io import read_signal, write_signal
 
 logger = logging.getLogger(__name__)
 
 
-def listen(ear, signal, audiogram_l, audiogram_r):
+def listen(ear, signal, listener: Listener):
     """
     Generate MSBG processed signal
     :param ear: MSBG ear
-    :param wav: binaural signal
+    :param signal: binaural signal
+    :param listener: listener - the listener characteristics to simulate
     :return: binaural signal
     """
-    ear.set_audiogram(audiogram_l)
+    ear.set_audiogram(listener.audiogram_left)
     out_l = ear.process(signal[:, 0])
 
-    ear.set_audiogram(audiogram_r)
+    ear.set_audiogram(listener.audiogram_right)
     out_r = ear.process(signal[:, 1])
+
     if len(out_l[0]) != len(out_r[0]):
         diff = len(out_l[0]) - len(out_r[0])
         if diff > 0:
@@ -45,8 +47,8 @@ def run_HL_processing(cfg, path):
     output_path.mkdir(parents=True, exist_ok=True)
     with open(path.scenes_file, encoding="utf-8") as fp:
         scenes = json.load(fp)
-    with open(path.listeners_file, encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
+    listener_dict = Listener.load_listener_dict(path.listeners_file)
+
     enhanced_folder = Path(path.scenes_folder)
 
     # initialize ear
@@ -54,12 +56,12 @@ def run_HL_processing(cfg, path):
 
     for scene_dict in tqdm(scenes):
         scene = scene_dict["scene"]
-        listener = scene_dict["listener"]
+        listener_id = scene_dict["listener"]
         system = scene_dict["system"]
-        signal_file = enhanced_folder / f"{scene}_{listener}_{system}.wav"
+        signal_file = enhanced_folder / f"{scene}_{listener_id}_{system}.wav"
 
         # signals to write
-        outfile_stem = f"{output_path}/{scene}_{listener}_{system}"
+        outfile_stem = f"{output_path}/{scene}_{listener_id}_{system}"
         signal_files_to_write = [
             f"{output_path}/{scene}_flat0dB_HL-output.wav",
             f"{outfile_stem}_HL-output.wav",
@@ -74,25 +76,18 @@ def run_HL_processing(cfg, path):
         mixture_signal = read_signal(Path(path.ref_folder) / f"{scene}_mixed_CH0.wav")
 
         # retrieve audiograms
-        cfs = np.array(listener_audiograms[listener]["audiogram_cfs"])
-        audiogram_left = np.array(listener_audiograms[listener]["audiogram_levels_l"])
-        left_audiogram = Audiogram(frequencies=cfs, levels=audiogram_left)
-        audiogram_right = np.array(listener_audiograms[listener]["audiogram_levels_r"])
-        right_audiogram = Audiogram(frequencies=cfs, levels=audiogram_right)
+        listener = listener_dict[listener_id]
 
         # Create discrete delta function (DDF) signal for time alignment
         ddf_signal = np.zeros(np.shape(signal))
         ddf_signal[:, 0] = unit_impulse(len(signal), int(MSBG_FS / 2))
         ddf_signal[:, 1] = unit_impulse(len(signal), int(MSBG_FS / 2))
-
-        # Get flat-0dB ear audiograms
-        flat0dB_audiogram = Audiogram(frequencies=cfs, levels=np.zeros(np.shape(cfs)))
-
+        reference_listener = Listener(AUDIOGRAM_REF_CLARITY, AUDIOGRAM_REF_CLARITY)
         signals_to_write = [
-            listen(ear, ddf_signal, flat0dB_audiogram, flat0dB_audiogram),
-            listen(ear, signal, left_audiogram, right_audiogram),
-            listen(ear, ddf_signal, left_audiogram, right_audiogram),
-            listen(ear, mixture_signal, left_audiogram, right_audiogram),
+            listen(ear, ddf_signal, reference_listener),
+            listen(ear, signal, listener),
+            listen(ear, ddf_signal, listener),
+            listen(ear, mixture_signal, listener),
         ]
         for signal, signal_file in zip(signals_to_write, signal_files_to_write):
             write_signal(
diff --git a/recipes/cpc1/e029_sheffield/prepare_data.py b/recipes/cpc1/e029_sheffield/prepare_data.py
index 8d5cf1e7c..b14bf109d 100644
--- a/recipes/cpc1/e029_sheffield/prepare_data.py
+++ b/recipes/cpc1/e029_sheffield/prepare_data.py
@@ -14,7 +14,7 @@
 
 from clarity.evaluator.msbg.msbg import Ear
 from clarity.evaluator.msbg.msbg_utils import MSBG_FS, pad
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Listener
 from clarity.utils.file_io import read_signal, write_signal
 
 logger = logging.getLogger(__name__)
@@ -54,17 +54,18 @@ def run_data_split(cfg, track):
         json.dump(scene_dev_list, fp)
 
 
-def listen(ear, signal, audiogram_l, audiogram_r):
+def listen(ear, signal, listener: Listener):
     """
     Generate MSBG processed signal
     :param ear: MSBG ear
     :param wav: binaural signal
+    :param listener: listener object
     :return: binaural signal
     """
-    ear.set_audiogram(audiogram_l)
+    ear.set_audiogram(listener.audiogram_left)
     out_l = ear.process(signal[:, 0])
 
-    ear.set_audiogram(audiogram_r)
+    ear.set_audiogram(listener.audiogram_right)
     out_r = ear.process(signal[:, 1])
     if len(out_l[0]) != len(out_r[0]):
         diff = len(out_l[0]) - len(out_r[0])
@@ -84,47 +85,30 @@ def run_msbg_simulation(cfg, track):
         with file_path.open("r", encoding="utf-8") as fp:
             scenes = json.load(fp)
         if split == "train":
-            file_path = dataset_folder / "metadata/listeners.CPC1_train.json"
-            with file_path.open("r", encoding="utf-8") as fp:
-                listener_audiograms = json.load(fp)
+            file_path = Path(dataset_folder) / "metadata/listeners.CPC1_train.json"
         else:
             file_path = Path(dataset_folder) / "metadata/listeners.CPC1_all.json"
-            with file_path.open("r", encoding="utf-8") as fp:
-                listener_audiograms = json.load(fp)
+        listener_dict = Listener.load_listener_dict(file_path)
 
         # initialize ear
         ear = Ear(**cfg["MSBGEar"])
 
         for scene_dict in tqdm(scenes):
             scene = scene_dict["scene"]
-            listener = scene_dict["listener"]
+            listener_id = scene_dict["listener"]
             system = scene_dict["system"]
-            signal_file = output_path / f"{scene}_{listener}_{system}.wav"
+            signal_file = output_path / f"{scene}_{listener_id}_{system}.wav"
 
             # signals to write
-            outfile_stem = output_path / f"{scene}_{listener}_{system}"
-            signal_files_to_write = [
-                Path(f"{outfile_stem}_HL-output.wav"),
-            ]
+            outfile_stem = output_path / f"{scene}_{listener_id}_{system}"
+            signal_files_to_write = [Path(f"{outfile_stem}_HL-output.wav")]
             # if all signals to write exist, pass
             if all(f.exists() for f in signal_files_to_write):
                 continue
             signal = read_signal(signal_file)
 
-            # retrieve audiograms
-            cfs = np.array(listener_audiograms[listener]["audiogram_cfs"])
-            audiogram_left = np.array(
-                listener_audiograms[listener]["audiogram_levels_l"]
-            )
-            left_audiogram = Audiogram(frequencies=cfs, levels=audiogram_left)
-            audiogram_right = np.array(
-                listener_audiograms[listener]["audiogram_levels_r"]
-            )
-            right_audiogram = Audiogram(frequencies=cfs, levels=audiogram_right)
-
-            signals_to_write = [
-                listen(ear, signal, left_audiogram, right_audiogram),
-            ]
+            listener = listener_dict[listener_id]
+            signals_to_write = [listen(ear, signal, listener)]
 
             for signal, signal_file in zip(signals_to_write, signal_files_to_write):
                 write_signal(
diff --git a/recipes/cpc1/e032_sheffield/prepare_data.py b/recipes/cpc1/e032_sheffield/prepare_data.py
index 123f65dd8..c2bdbb95c 100644
--- a/recipes/cpc1/e032_sheffield/prepare_data.py
+++ b/recipes/cpc1/e032_sheffield/prepare_data.py
@@ -14,7 +14,7 @@
 
 from clarity.evaluator.msbg.msbg import Ear
 from clarity.evaluator.msbg.msbg_utils import MSBG_FS, pad
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Listener
 from clarity.utils.file_io import read_signal, write_signal
 
 logger = logging.getLogger(__name__)
@@ -54,17 +54,18 @@ def run_data_split(cfg, track):
         json.dump(scene_dev_list, fp)
 
 
-def listen(ear, signal, audiogram_l, audiogram_r):
+def listen(ear, signal, listener: Listener):
     """
     Generate MSBG processed signal
     :param ear: MSBG ear
     :param wav: binaural signal
+    :param listener: listener object
     :return: binaural signal
     """
-    ear.set_audiogram(audiogram_l)
+    ear.set_audiogram(listener.audiogram_left)
     out_l = ear.process(signal[:, 0])
 
-    ear.set_audiogram(audiogram_r)
+    ear.set_audiogram(listener.audiogram_right)
     out_r = ear.process(signal[:, 1])
     if len(out_l[0]) != len(out_r[0]):
         diff = len(out_l[0]) - len(out_r[0])
@@ -85,24 +86,21 @@ def run_msbg_simulation(cfg, track):
             scenes = json.load(fp)
         if split == "train":
             file_path = dataset_folder / "metadata/listeners.CPC1_train.json"
-            with file_path.open("r", encoding="utf-8") as fp:
-                listener_audiograms = json.load(fp)
         else:
             file_path = Path(dataset_folder) / "metadata/listeners.CPC1_all.json"
-            with file_path.open("r", encoding="utf-8") as fp:
-                listener_audiograms = json.load(fp)
+        listener_dict = Listener.load_listener_dict(file_path)
 
         # initialize ear
         ear = Ear(**cfg["MSBGEar"])
 
         for scene_dict in tqdm(scenes):
             scene = scene_dict["scene"]
-            listener = scene_dict["listener"]
+            listener_id = scene_dict["listener"]
             system = scene_dict["system"]
-            signal_file = output_path / f"{scene}_{listener}_{system}.wav"
+            signal_file = output_path / f"{scene}_{listener_id}_{system}.wav"
 
             # signals to write
-            outfile_stem = output_path / f"{scene}_{listener}_{system}"
+            outfile_stem = output_path / f"{scene}_{listener_id}_{system}"
             signal_files_to_write = [
                 Path(f"{outfile_stem}_HL-output.wav"),
             ]
@@ -112,19 +110,9 @@ def run_msbg_simulation(cfg, track):
             signal = read_signal(signal_file)
 
             # retrieve audiograms
-            cfs = np.array(listener_audiograms[listener]["audiogram_cfs"])
-            audiogram_left = np.array(
-                listener_audiograms[listener]["audiogram_levels_l"]
-            )
-            left_audiogram = Audiogram(frequencies=cfs, levels=audiogram_left)
-            audiogram_right = np.array(
-                listener_audiograms[listener]["audiogram_levels_r"]
-            )
-            right_audiogram = Audiogram(frequencies=cfs, levels=audiogram_right)
+            listener = listener_dict[listener_id]
 
-            signals_to_write = [
-                listen(ear, signal, left_audiogram, right_audiogram),
-            ]
+            signals_to_write = [listen(ear, signal, listener)]
 
             for signal, signal_file in zip(signals_to_write, signal_files_to_write):
                 write_signal(
diff --git a/recipes/cpc2/baseline/compute_haspi.py b/recipes/cpc2/baseline/compute_haspi.py
index 5e3c6aed4..6150996b0 100644
--- a/recipes/cpc2/baseline/compute_haspi.py
+++ b/recipes/cpc2/baseline/compute_haspi.py
@@ -13,7 +13,7 @@
 from tqdm import tqdm
 
 from clarity.evaluator.haspi import haspi_v2_be
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Listener
 from clarity.utils.file_io import read_jsonl, write_jsonl
 
 logger = logging.getLogger(__name__)
@@ -45,12 +45,13 @@ def compute_haspi_for_signal(signal_name: str, path: dict) -> float:
         float: HASPI score
     """
 
-    scene, listener, _ = parse_cec2_signal_name(signal_name)
+    scene, listener_id, _ = parse_cec2_signal_name(signal_name)
 
-    # Retrieve audiograms
-    with open(Path(path["metadata_dir"]) / "listeners.json", encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
-        audiogram = listener_audiograms[listener]
+    # Retrieve the listeners
+    listener_dict = Listener.load_listener_dict(
+        Path(path["metadata_dir"]) / "listeners.json"
+    )
+    listener = listener_dict[listener_id]
 
     # Retrieve signals and convert to float32 between -1 and 1
     sr_proc, proc = wavfile.read(Path(path["signal_dir"]) / f"{signal_name}.wav")
@@ -60,12 +61,6 @@ def compute_haspi_for_signal(signal_name: str, path: dict) -> float:
     proc = proc / 32768.0
     ref = ref / 32768.0
 
-    audiogram_left = Audiogram(
-        levels=audiogram["audiogram_levels_l"], frequencies=audiogram["audiogram_cfs"]
-    )
-    audiogram_right = Audiogram(
-        levels=audiogram["audiogram_levels_r"], frequencies=audiogram["audiogram_cfs"]
-    )
     # Compute haspi score using library code
     haspi_score = haspi_v2_be(
         reference_left=ref[:, 0],
@@ -73,8 +68,7 @@ def compute_haspi_for_signal(signal_name: str, path: dict) -> float:
         processed_left=proc[:, 0],
         processed_right=proc[:, 1],
         sample_rate=sr_proc,
-        audiogram_left=audiogram_left,
-        audiogram_right=audiogram_right,
+        listener=listener,
     )
 
     return haspi_score
diff --git a/recipes/icassp_2023/baseline/enhance.py b/recipes/icassp_2023/baseline/enhance.py
index 0edda9734..7576e1147 100644
--- a/recipes/icassp_2023/baseline/enhance.py
+++ b/recipes/icassp_2023/baseline/enhance.py
@@ -9,6 +9,7 @@
 from scipy.io import wavfile
 from tqdm import tqdm
 
+from clarity.utils.audiogram import Listener
 from recipes.icassp_2023.baseline.evaluate import make_scene_listener_list
 
 logger = logging.getLogger(__name__)
@@ -24,16 +25,14 @@ def enhance(cfg: DictConfig) -> None:
     with open(cfg.path.scenes_listeners_file, encoding="utf-8") as fp:
         scenes_listeners = json.load(fp)
 
-    # pylint: disable=unused-variable
-    with open(cfg.path.listeners_file, encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)  # noqa: F841
+    listener_dict = Listener.load_listener_dict(cfg.path.listeners_file)
 
     # Make list of all scene listener pairs that will be run
     scene_listener_pairs = make_scene_listener_list(
         scenes_listeners, cfg.evaluate.small_test
     )
 
-    for scene, listener in tqdm(scene_listener_pairs):
+    for scene, listener_id in tqdm(scene_listener_pairs):
         sample_rate, signal = wavfile.read(
             pathlib.Path(cfg.path.scenes_folder) / f"{scene}_mix_CH1.wav"
         )
@@ -41,23 +40,17 @@ def enhance(cfg: DictConfig) -> None:
         # Convert to 32-bit floating point scaled between -1 and 1
         signal = (signal / 32768.0).astype(np.float32)
 
-        # # Audiograms can read like this, but they are not needed for the baseline
-        #
-        # cfs = np.array(listener_audiograms[listener]["audiogram_cfs"])
-        #
-        # audiogram_left = np.array(
-        #    listener_audiograms[listener]["audiogram_levels_l"]
-        # )
-        # audiogram_right = np.array(
-        #    listener_audiograms[listener]["audiogram_levels_r"]
-        # )
+        # pylint: disable=unused-variable
+        listener = listener_dict[listener_id]  # noqa: F841
+
+        # Note: The audiograms are stored in the listener object,
+        # but they are not needed for the baseline
 
         # Baseline just reads the signal from the front microphone pair
         # and write it out as the enhanced signal
-        #
 
         wavfile.write(
-            enhanced_folder / f"{scene}_{listener}_enhanced.wav", sample_rate, signal
+            enhanced_folder / f"{scene}_{listener_id}_enhanced.wav", sample_rate, signal
         )
 
 
diff --git a/recipes/icassp_2023/baseline/evaluate.py b/recipes/icassp_2023/baseline/evaluate.py
index 5454cc1b0..2b84c05e9 100644
--- a/recipes/icassp_2023/baseline/evaluate.py
+++ b/recipes/icassp_2023/baseline/evaluate.py
@@ -7,6 +7,7 @@
 
 import hydra
 import numpy as np
+from numpy import ndarray
 from omegaconf import DictConfig
 from scipy.io import wavfile
 from tqdm import tqdm
@@ -15,15 +16,14 @@
 from clarity.enhancer.nalr import NALR
 from clarity.evaluator.haspi import haspi_v2_be
 from clarity.evaluator.hasqi import hasqi_v2_better_ear
+from clarity.utils.audiogram import Audiogram, Listener
 
 logger = logging.getLogger(__name__)
 
 
-def amplify_signal(signal, bin_audiogram, ear, enhancer, compressor):
-    """Amplify signal for either left (l) or right (r) ear"""
-    cfs = np.array(bin_audiogram["audiogram_cfs"])
-    audiogram = np.array(bin_audiogram[f"audiogram_levels_{ear}"])
-    nalr_fir, _ = enhancer.build(audiogram, cfs)
+def amplify_signal(signal, audiogram: Audiogram, enhancer, compressor):
+    """Amplify signal for a given audiogram"""
+    nalr_fir, _ = enhancer.build(audiogram)
     out = enhancer.apply(nalr_fir, signal)
     out, _, _ = compressor.process(out)
     return out
@@ -36,7 +36,9 @@ def set_scene_seed(scene):
     np.random.seed(scene_md5)
 
 
-def compute_metric(metric, signal, ref, audiogram, sample_rate):
+def compute_metric(
+    metric, signal: ndarray, ref: ndarray, listener: Listener, sample_rate: float
+):
     """Compute HASPI or HASQI metric"""
     score = metric(
         reference_left=ref[:, 0],
@@ -44,9 +46,7 @@ def compute_metric(metric, signal, ref, audiogram, sample_rate):
         processed_left=signal[:, 0],
         processed_right=signal[:, 1],
         sample_rate=sample_rate,
-        audiogram_left=audiogram["audiogram_levels_l"],
-        audiogram_right=audiogram["audiogram_levels_r"],
-        audiogram_frequencies=audiogram["audiogram_cfs"],
+        listener=listener,
     )
     return score
 
@@ -64,7 +64,9 @@ def write_header(self):
             )
             csv_writer.writerow(["scene", "listener", "combined", "haspi", "hasqi"])
 
-    def add_result(self, scene, listener, score, haspi, hasqi):
+    def add_result(
+        self, scene: str, listener: str, score: float, haspi: float, hasqi: float
+    ):
         """Add a result to the CSV file"""
 
         logger.info(f"The combined score is {score} (haspi {haspi}, hasqi {hasqi})")
@@ -99,9 +101,7 @@ def run_calculate_si(cfg: DictConfig) -> None:
     with open(cfg.path.scenes_listeners_file, encoding="utf-8") as fp:
         scenes_listeners = json.load(fp)
 
-    with open(cfg.path.listeners_file, encoding="utf-8") as fp:
-        listener_audiograms = json.load(fp)
-
+    listeners_dict = Listener.load_listener_dict(cfg.path.listeners_file)
     enhancer = NALR(**cfg.nalr)
     compressor = Compressor(**cfg.compressor)
 
@@ -119,8 +119,8 @@ def run_calculate_si(cfg: DictConfig) -> None:
     results_file = ResultsFile("scores.csv")
     results_file.write_header()
 
-    for scene, listener in tqdm(scene_listener_pairs):
-        logger.info(f"Running evaluation: scene {scene}, listener {listener}")
+    for scene, listener_id in tqdm(scene_listener_pairs):
+        logger.info(f"Running evaluation: scene {scene}, listener {listener_id}")
 
         if cfg.evaluate.set_random_seed:
             set_scene_seed(scene)
@@ -128,7 +128,7 @@ def run_calculate_si(cfg: DictConfig) -> None:
         # Read signals
 
         sr_signal, signal = wavfile.read(
-            enhanced_folder / f"{scene}_{listener}_enhanced.wav"
+            enhanced_folder / f"{scene}_{listener_id}_enhanced.wav"
         )
         sr_ref_anechoic, ref_anechoic = wavfile.read(
             scenes_folder / f"{scene}_target_anechoic_CH1.wav"
@@ -142,17 +142,21 @@ def run_calculate_si(cfg: DictConfig) -> None:
         assert sr_ref_anechoic == sr_ref_target == sr_signal == cfg.nalr.sample_rate
 
         # amplify left and right ear signals
-        audiogram = listener_audiograms[listener]
+        listener = listeners_dict[listener_id]
 
-        out_l = amplify_signal(signal[:, 0], audiogram, "l", enhancer, compressor)
-        out_r = amplify_signal(signal[:, 1], audiogram, "r", enhancer, compressor)
+        out_l = amplify_signal(
+            signal[:, 0], listener.audiogram_left, enhancer, compressor
+        )
+        out_r = amplify_signal(
+            signal[:, 1], listener.audiogram_right, enhancer, compressor
+        )
         amplified = np.stack([out_l, out_r], axis=1)
 
         if cfg.soft_clip:
             amplified = np.tanh(amplified)
 
         wavfile.write(
-            amplified_folder / f"{scene}_{listener}_HA-output.wav",
+            amplified_folder / f"{scene}_{listener_id}_HA-output.wav",
             sr_signal,
             amplified.astype(np.float32),
         )
@@ -163,14 +167,14 @@ def run_calculate_si(cfg: DictConfig) -> None:
         rms_anechoic = np.mean(ref_anechoic**2, axis=0) ** 0.5
         ref = ref_anechoic * rms_target / rms_anechoic
 
-        haspi_score = compute_metric(haspi_v2_be, amplified, ref, audiogram, sr_signal)
+        haspi_score = compute_metric(haspi_v2_be, amplified, ref, listener, sr_signal)
         hasqi_score = compute_metric(
-            hasqi_v2_better_ear, amplified, ref, audiogram, sr_signal
+            hasqi_v2_better_ear, amplified, ref, listener, sr_signal
         )
         score = 0.5 * (hasqi_score + haspi_score)
 
         results_file.add_result(
-            scene, listener, score=score, haspi=haspi_score, hasqi=hasqi_score
+            scene, listener_id, score=score, haspi=haspi_score, hasqi=hasqi_score
         )
 
 


Test patch:
diff --git a/tests/enhancer/gha/test_gha_interface.py b/tests/enhancer/gha/test_gha_interface.py
index 04a8a41d0..59573f163 100644
--- a/tests/enhancer/gha/test_gha_interface.py
+++ b/tests/enhancer/gha/test_gha_interface.py
@@ -5,7 +5,7 @@
 import pytest
 
 from clarity.enhancer.gha.gha_interface import GHAHearingAid
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 from clarity.utils.file_io import read_signal, write_signal
 
 
@@ -85,7 +85,7 @@ def test_process_files(mocker, tmp_path):
     levels = np.array([45, 45, 35, 45, 60, 65])
     frequencies = np.array([250, 500, 1000, 2000, 4000, 6000])
     audiogram = Audiogram(levels=levels, frequencies=frequencies)
-
+    listener = Listener(audiogram_left=audiogram, audiogram_right=audiogram)
     # Mock the subprocess.run function as OpenMHA is not installed
     m = mocker.patch("clarity.enhancer.gha.gha_interface.subprocess.run")
 
@@ -103,9 +103,7 @@ def test_process_files(mocker, tmp_path):
     gha_hearing_aid.process_files(
         infile_names=infile_names,
         outfile_name=outfile_name,
-        audiogram_left=audiogram,
-        audiogram_right=audiogram,
-        listener=None,
+        listener=listener,
     )
     # Check that the subprocess.run function was called
     m.assert_called_once()
diff --git a/tests/enhancer/test_nalr.py b/tests/enhancer/test_nalr.py
index 9499c4335..93ad1b83c 100644
--- a/tests/enhancer/test_nalr.py
+++ b/tests/enhancer/test_nalr.py
@@ -3,6 +3,7 @@
 import pytest
 
 from clarity.enhancer.nalr import NALR
+from clarity.utils.audiogram import Audiogram
 
 cfg_nalr = {"nfir": 220, "sample_rate": 44100}
 
@@ -11,24 +12,25 @@
 
 
 @pytest.mark.parametrize(
-    "audiogram, cfs, expected",
+    "levels, cfs, expected",
     [
         (
             np.array([45, 45, 35, 45, 60, 65]),
             np.array([250, 500, 1000, 1500, 4000, 6000]),  # <-- non default cfs
-            29.49383298688706,
+            29.808253441926134,  # was 29.493833... (New behaviour due to log freq axis)
         ),
         (
             np.array([45, 45, 35, 45, 60, 65]),
-            None,
+            np.array([250, 500, 1000, 2000, 4000, 6000]),
             28.846583644263408,
         ),
     ],
 )
-def test_nalr(audiogram: np.ndarray, cfs: np.ndarray, expected: float) -> None:
+def test_nalr(levels: np.ndarray, cfs: np.ndarray, expected: float) -> None:
     """Test that the NALR filter is built correctly."""
     enhancer = NALR(**cfg_nalr)
-    nalr_fir, _ = enhancer.build(audiogram, cfs)
+    audiogram = Audiogram(levels=levels, frequencies=cfs)
+    nalr_fir, _ = enhancer.build(audiogram)
     assert np.sum(np.abs(nalr_fir)) == pytest.approx(
         expected, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
     )
@@ -37,8 +39,9 @@ def test_nalr(audiogram: np.ndarray, cfs: np.ndarray, expected: float) -> None:
 def test_nalr_default_cfs() -> None:
     """Test that the NALR filter is the same for the default and standard cfs."""
     enhancer = NALR(**cfg_nalr)
-    nalr_fir1, _ = enhancer.build(np.array([45, 45, 35, 45, 60, 65]))
-    nalr_fir2, _ = enhancer.build(np.array([45, 45, 35, 45, 60, 65]), STANDARD_CFS)
+    nalr_fir1, _ = enhancer.build(
+        Audiogram(np.array([45, 45, 35, 45, 60, 65]), STANDARD_CFS)
+    )
     assert nalr_fir1 == pytest.approx(
-        nalr_fir2, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
+        nalr_fir1, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
     )
diff --git a/tests/evaluator/haspi/test_haspi.py b/tests/evaluator/haspi/test_haspi.py
index d11c708b3..e1c5b930b 100644
--- a/tests/evaluator/haspi/test_haspi.py
+++ b/tests/evaluator/haspi/test_haspi.py
@@ -3,7 +3,7 @@
 import pytest
 
 from clarity.evaluator.haspi import haspi_v2, haspi_v2_be
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 
 
 def test_haspi_v2() -> None:
@@ -58,14 +58,17 @@ def test_haspi_v2_better_ear(hl_left, hl_right, freqs, expected_score) -> None:
     proc_left = np.random.uniform(-1, 1, int(sample_rate * 0.5))  # i.e. 500 ms of audio
     proc_right = np.random.uniform(-1, 1, int(sample_rate * 0.5))
 
+    audiogram_left = Audiogram(levels=hl_left, frequencies=freqs)
+    audiogram_right = Audiogram(levels=hl_right, frequencies=freqs)
+
+    listener = Listener(audiogram_left=audiogram_left, audiogram_right=audiogram_right)
     score = haspi_v2_be(
         reference_left=ref_left,
         reference_right=ref_right,
         processed_left=proc_left + ref_left,
         processed_right=proc_right,
         sample_rate=sample_rate,
-        audiogram_left=Audiogram(levels=hl_left, frequencies=freqs),
-        audiogram_right=Audiogram(levels=hl_right, frequencies=freqs),
+        listener=listener,
         level=100,
     )
 
@@ -104,14 +107,17 @@ def test_haspi_v2_better_ear_non_standard(
     proc_left = np.random.uniform(-1, 1, int(sample_rate * 0.5))  # i.e. 500 ms of audio
     proc_right = np.random.uniform(-1, 1, int(sample_rate * 0.5))
 
+    audiogram_left = Audiogram(levels=hl_left, frequencies=freqs)
+    audiogram_right = Audiogram(levels=hl_right, frequencies=freqs)
+    listener = Listener(audiogram_left=audiogram_left, audiogram_right=audiogram_right)
+
     score = haspi_v2_be(
         reference_left=ref_left,
         reference_right=ref_right,
         processed_left=proc_left + ref_left,
         processed_right=proc_right,
         sample_rate=sample_rate,
-        audiogram_left=Audiogram(levels=hl_left, frequencies=freqs),
-        audiogram_right=Audiogram(levels=hl_right, frequencies=freqs),
+        listener=listener,
         level=100,
     )
     assert score == pytest.approx(
diff --git a/tests/evaluator/hasqi/test_hasqi.py b/tests/evaluator/hasqi/test_hasqi.py
index a68956beb..959319479 100644
--- a/tests/evaluator/hasqi/test_hasqi.py
+++ b/tests/evaluator/hasqi/test_hasqi.py
@@ -3,7 +3,7 @@
 import pytest
 
 from clarity.evaluator.hasqi import hasqi_v2, hasqi_v2_better_ear
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 
 
 def test_hasqi_v2() -> None:
@@ -53,6 +53,7 @@ def test_hasqi_v2_better_ear(hl_left, hl_right, freqs, expected_score) -> None:
 
     audiogram_left = Audiogram(levels=hl_left, frequencies=freqs)
     audiogram_right = Audiogram(levels=hl_right, frequencies=freqs)
+    listener = Listener(audiogram_left=audiogram_left, audiogram_right=audiogram_right)
 
     ref_left = np.random.uniform(-1, 1, int(sample_rate * 0.5))  # i.e. 500 ms of audio
     ref_right = np.random.uniform(-1, 1, int(sample_rate * 0.5))
@@ -65,8 +66,7 @@ def test_hasqi_v2_better_ear(hl_left, hl_right, freqs, expected_score) -> None:
         processed_left=proc_left + ref_left,
         processed_right=proc_right,
         sample_rate=sample_rate,
-        audiogram_left=audiogram_left,
-        audiogram_right=audiogram_right,
+        listener=listener,
         level=100,
     )
 
diff --git a/tests/recipes/cad1/task1/baseline/test_enhance_task1.py b/tests/recipes/cad1/task1/baseline/test_enhance_task1.py
index ff54be13a..b8cf8e8db 100644
--- a/tests/recipes/cad1/task1/baseline/test_enhance_task1.py
+++ b/tests/recipes/cad1/task1/baseline/test_enhance_task1.py
@@ -9,7 +9,7 @@
 
 from clarity.enhancer.compressor import Compressor
 from clarity.enhancer.nalr import NALR
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 
 # pylint: disable=import-error, no-name-in-module
 from recipes.cad1.task1.baseline.enhance import (
@@ -79,14 +79,15 @@ def test_decompose_signal(separation_model):
     )
     # Call the decompose_signal function and check that the output has the expected keys
     cfs = np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000])
+    audiogram = Audiogram(levels=np.ones(9), frequencies=cfs)
+    listener = Listener(audiogram, audiogram)
     output = decompose_signal(
         config,
         model,
         signal,
         sample_rate,
         device,
-        left_audiogram=Audiogram(levels=np.ones(9), frequencies=cfs),
-        right_audiogram=Audiogram(levels=np.ones(9), frequencies=cfs),
+        listener,
     )
     expected_results = np.load(
         RESOURCES / f"test_enhance.test_decompose_signal_{separation_model}.npy",
@@ -136,6 +137,7 @@ def test_process_stems_for_listener():
         levels=np.ones(9),
         frequencies=np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000]),
     )
+    listener = Listener(audiogram_left=audiogram, audiogram_right=audiogram)
     # Create mock objects for enhancer and compressor
     enhancer = NALR(nfir=220, sample_rate=16000)
     compressor = Compressor(
@@ -144,7 +146,7 @@ def test_process_stems_for_listener():
 
     # Call the process_stems_for_listener function and check output is as expected
     output_stems = process_stems_for_listener(
-        stems, enhancer, compressor, audiogram_left=audiogram, audiogram_right=audiogram
+        stems, enhancer, compressor, listener=listener
     )
     expected_results = np.load(
         RESOURCES / "test_enhance.test_process_stems_for_listener.npy",
diff --git a/tests/recipes/cad1/task1/baseline/test_evaluate.py b/tests/recipes/cad1/task1/baseline/test_evaluate.py
index 91bec1f80..5b8a06120 100644
--- a/tests/recipes/cad1/task1/baseline/test_evaluate.py
+++ b/tests/recipes/cad1/task1/baseline/test_evaluate.py
@@ -6,6 +6,7 @@
 from omegaconf import DictConfig
 from scipy.io import wavfile
 
+from clarity.utils.audiogram import Audiogram, Listener
 from recipes.cad1.task1.baseline.evaluate import (
     ResultsFile,
     _evaluate_song_listener,
@@ -20,7 +21,7 @@ def test_results_file(tmp_path):
     result_file = ResultsFile(results_file.as_posix())
     result_file.write_header()
     result_file.add_result(
-        listener="My listener",
+        listener_id="My listener",
         song="My favorite song",
         score=0.9,
         instruments_scores={
@@ -80,7 +81,7 @@ def test_make_song_listener_list():
 
 
 @pytest.mark.parametrize(
-    "song,listener,config,split_dir,listener_audiograms,expected_results",
+    "song,listener_id,config,split_dir,listener_audiograms,expected_results",
     [
         (
             "punk_is_not_dead",
@@ -113,11 +114,24 @@ def test_make_song_listener_list():
     ],
 )
 def test_evaluate_song_listener(
-    song, listener, config, split_dir, listener_audiograms, expected_results, tmp_path
+    song,
+    listener_id,
+    config,
+    split_dir,
+    listener_audiograms,
+    expected_results,
+    tmp_path,
 ):
     """Test the function _evaluate_song_listener returns correct results given input"""
     np.random.seed(2023)
-
+    listener_data = listener_audiograms[listener_id]
+    audiogram_left = Audiogram(
+        listener_data["audiogram_levels_l"], listener_data["audiogram_cfs"]
+    )
+    audiogram_right = Audiogram(
+        listener_data["audiogram_levels_r"], listener_data["audiogram_cfs"]
+    )
+    listener = Listener(audiogram_left, audiogram_right, id=listener_id)
     # Generate reference and enhanced wav files
     enhanced_folder = tmp_path / "enhanced"
     enhanced_folder.mkdir()
@@ -132,9 +146,9 @@ def test_evaluate_song_listener(
         # enhanced signals are mono
         enh_file = (
             enhanced_folder
-            / f"{listener}"
+            / f"{listener.id}"
             / f"{song}"
-            / f"{listener}_{song}_{lr_instrument}.wav"
+            / f"{listener.id}_{song}_{lr_instrument}.wav"
         )
         enh_file.parent.mkdir(exist_ok=True, parents=True)
 
@@ -161,7 +175,6 @@ def test_evaluate_song_listener(
         listener,
         config,
         split_dir,
-        listener_audiograms[listener],
         enhanced_folder,
     )
 
diff --git a/tests/recipes/cad1/task1/test_enhance_task1.py b/tests/recipes/cad1/task1/test_enhance_task1.py
deleted file mode 100644
index f5ef6ba19..000000000
--- a/tests/recipes/cad1/task1/test_enhance_task1.py
+++ /dev/null
@@ -1,238 +0,0 @@
-"""Tests for the enhance module"""
-from pathlib import Path
-
-import numpy as np
-import pytest
-import torch
-from omegaconf import DictConfig
-from torchaudio.pipelines import HDEMUCS_HIGH_MUSDB
-
-from clarity.enhancer.compressor import Compressor
-from clarity.enhancer.nalr import NALR
-from clarity.utils.audiogram import Audiogram
-
-# pylint: disable=import-error, no-name-in-module
-from recipes.cad1.task1.baseline.enhance import (
-    apply_baseline_ha,
-    clip_signal,
-    decompose_signal,
-    get_device,
-    map_to_dict,
-    process_stems_for_listener,
-    separate_sources,
-    to_16bit,
-)
-
-BASE_DIR = Path.cwd()
-RESOURCES = BASE_DIR / "tests" / "resources" / "recipes" / "cad1" / "task1"
-
-
-def test_map_to_dict():
-    """Test that the map_to_dict returns the expected mapping"""
-    sources = np.array([[1, 2], [3, 4], [5, 6]])
-    sources_list = ["a", "b", "c"]
-    output = map_to_dict(sources, sources_list)
-    expected_output = {
-        "left_a": 1,
-        "right_a": 2,
-        "left_b": 3,
-        "right_b": 4,
-        "left_c": 5,
-        "right_c": 6,
-    }
-    assert output == expected_output
-
-
-@pytest.mark.parametrize(
-    "separation_model",
-    [
-        pytest.param("demucs"),
-        pytest.param("openunmix", marks=pytest.mark.slow),
-    ],
-)
-def test_decompose_signal(separation_model):
-    """Takes a signal and decomposes it into VDBO sources using the HDEMUCS model"""
-    np.random.seed(123456789)
-    # Load Separation Model
-    if separation_model == "demucs":
-        model = HDEMUCS_HIGH_MUSDB.get_model().double()
-    elif separation_model == "openunmix":
-        model = torch.hub.load("sigsep/open-unmix-pytorch", "umxhq").double()
-
-    device = torch.device("cpu")
-    model.to(device)
-
-    # Create a mock signal to decompose
-    sample_rate = 44100
-    duration = 0.5
-    signal = np.random.uniform(size=(1, 2, int(sample_rate * duration)))
-
-    # config
-    config = DictConfig(
-        {
-            "sample_rate": sample_rate,
-            "separator": {
-                "model": "demucs",
-                "sources": ["drums", "bass", "other", "vocals"],
-            },
-        }
-    )
-    # Call the decompose_signal function and check that the output has the expected keys
-    output = decompose_signal(
-        config,
-        model,
-        signal,
-        sample_rate,
-        device,
-        left_audiogram=Audiogram(
-            levels=np.ones(9),
-            frequencies=np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000]),
-        ),
-        right_audiogram=Audiogram(
-            levels=np.ones(9),
-            frequencies=np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000]),
-        ),
-    )
-    expected_results = np.load(
-        RESOURCES / f"test_enhance.test_decompose_signal_{separation_model}.npy",
-        allow_pickle=True,
-    )[()]
-
-    for key, item in output.items():
-        np.testing.assert_array_almost_equal(item, expected_results[key])
-
-
-def test_apply_baseline_ha():
-    """Test the behaviour of the CAD1 - Task1 - baseline hearing aid"""
-    np.random.seed(987654321)
-    # Create mock inputs
-    signal = np.random.normal(size=44100)
-    listener_audiogram = Audiogram(
-        levels=np.ones(9),
-        frequencies=np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000]),
-    )
-
-    # Create mock objects for enhancer and compressor
-    enhancer = NALR(nfir=220, sample_rate=44100)
-    compressor = Compressor(
-        threshold=0.35, attenuation=0.1, attack=50, release=1000, rms_buffer_size=0.064
-    )
-
-    # Call the apply_nalr function and check that the output is as expected
-    output = apply_baseline_ha(enhancer, compressor, signal, listener_audiogram)
-
-    expected_results = np.load(
-        RESOURCES / "test_enhance.test_apply_baseline_ha.npy",
-        allow_pickle=True,
-    )
-    np.testing.assert_array_almost_equal(output, expected_results)
-
-
-def test_process_stems_for_listener():
-    """Takes 2 stems and applies the baseline processing using a listeners audiograms"""
-    np.random.seed(12357)
-    # Create mock inputs
-    stems = {
-        "l_source1": np.random.normal(size=16000),
-        "r_source1": np.random.normal(size=16000),
-    }
-    cfs = np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000])
-    audiogram_left = Audiogram(levels=np.ones(9), frequencies=cfs)
-    audiogram_right = Audiogram(levels=np.ones(9), frequencies=cfs)
-
-    # Create mock objects for enhancer and compressor
-    enhancer = NALR(nfir=220, sample_rate=16000)
-    compressor = Compressor(
-        threshold=0.35, attenuation=0.1, attack=50, release=1000, rms_buffer_size=0.064
-    )
-
-    # Call the process_stems_for_listener function and check output is as expected
-    output_stems = process_stems_for_listener(
-        stems, enhancer, compressor, audiogram_left, audiogram_right
-    )
-    expected_results = np.load(
-        RESOURCES / "test_enhance.test_process_stems_for_listener.npy",
-        allow_pickle=True,
-    )[()]
-
-    for key, item in output_stems.items():
-        np.testing.assert_array_almost_equal(item, expected_results[key])
-
-
-def test_separate_sources():
-    """Test that the separate_sources function returns the expected output"""
-    np.random.seed(123456789)
-
-    # Create a dummy model
-    class DummyModel(torch.nn.Module):  # pylint: disable=too-few-public-methods
-        """Dummy source separation model"""
-
-        def __init__(self, sources):
-            """dummy init"""
-            super().__init__()
-            self.sources = sources
-
-        def forward(self, x):
-            """dummy forward"""
-            return torch.Tensor(
-                np.random.uniform(size=(x.shape[0], len(self.sources), *x.shape[1:]))
-            )
-
-    # Set up some dummy input data
-    batch_size = 1
-    num_channels = 1
-    length = 1
-    sample_rate = 16000
-    sources = ["vocals", "drums", "bass", "other"]
-    mix = np.random.randn(batch_size, num_channels, length * sample_rate)
-    device = torch.device("cpu")
-
-    # Create a dummy model
-    model = DummyModel(sources)
-
-    # Call separate_sources
-    output = separate_sources(model, mix, sample_rate, device=device)
-
-    expected_results = np.load(
-        RESOURCES / "test_enhance.test_separate_sources.npy",
-        allow_pickle=True,
-    )
-    # Check that the output has the correct shape
-    assert output.shape == expected_results.shape
-    np.testing.assert_array_almost_equal(output, expected_results)
-
-
-def test_get_device():
-    """Test the correct device selection given the inputs"""
-    # Test default case (no argument passed)
-    device, device_type = get_device(None)
-    assert (
-        device == torch.device("cuda")
-        if torch.cuda.is_available()
-        else torch.device("cpu")
-    )
-    assert device_type == "cuda" if torch.cuda.is_available() else "cpu"
-
-
-def test_to_16bit():
-    # Generate a random signal
-    signal = np.random.uniform(low=-1.0, high=1.0, size=50)
-    signal_16bit = to_16bit(signal)
-
-    assert np.all(np.abs(signal_16bit) <= 32768)
-
-
-def test_clip_signal():
-    # Generate a random signal
-    np.random.seed(0)
-    signal = np.random.uniform(low=-2.0, high=2.0, size=50)
-
-    # Test with soft clipping
-    clipped_signal, n_clipped = clip_signal(signal, soft_clip=True)
-    assert max(np.abs(clipped_signal)) <= 1.0
-    assert n_clipped == 0
-
-    # Test without soft clipping
-    clipped_signal, n_clipped = clip_signal(signal, soft_clip=False)
-    assert max(np.abs(clipped_signal)) <= 1.0
-    assert n_clipped == 22
diff --git a/tests/recipes/cad1/task2/baseline/test_enhance_task2.py b/tests/recipes/cad1/task2/baseline/test_enhance_task2.py
index 03b0d3e0f..27e31d830 100644
--- a/tests/recipes/cad1/task2/baseline/test_enhance_task2.py
+++ b/tests/recipes/cad1/task2/baseline/test_enhance_task2.py
@@ -6,6 +6,7 @@
 import pytest
 from omegaconf import DictConfig
 
+from clarity.utils.audiogram import Audiogram, Listener
 from recipes.cad1.task2.baseline.enhance import enhance_song
 
 BASE_DIR = Path.cwd()
@@ -25,12 +26,12 @@ def test_enhance_song():
             "enhance": {"min_level": -11, "max_level": -19, "average_level": -14},
         }
     )
-    listener = {
-        "audiogram_levels_l": [20, 30, 35, 45, 50, 60, 65, 60],
-        "audiogram_levels_r": [20, 30, 35, 45, 50, 60, 65, 60],
-        "audiogram_cfs": [250, 500, 1000, 2000, 3000, 4000, 6000, 8000],
-    }
 
+    levels = np.array([20, 30, 35, 45, 50, 60, 65, 60])
+    frequencies = np.array([250, 500, 1000, 2000, 3000, 4000, 6000, 8000])
+    audiogram = Audiogram(levels=levels, frequencies=frequencies)
+
+    listener = Listener(audiogram, audiogram)
     # Create a sample waveform
     waveform = np.random.rand(2, int(config.sample_rate * duration))
 
diff --git a/tests/recipes/cpc1/baseline/test_run.py b/tests/recipes/cpc1/baseline/test_run.py
index f388ff0cd..6d43f53c9 100644
--- a/tests/recipes/cpc1/baseline/test_run.py
+++ b/tests/recipes/cpc1/baseline/test_run.py
@@ -13,7 +13,7 @@
 
 import recipes
 from clarity.evaluator.msbg.msbg import Ear
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 from clarity.utils.file_io import read_signal
 from recipes.cpc1.baseline.run import listen, run, run_calculate_SI, run_HL_processing
 
@@ -61,10 +61,11 @@ def test_listen(hydra_cfg):
     signal = np.random.rand(2000, 2)
     audiogram_mild = Audiogram(frequencies=cfs, levels=levels_1)
     audiogram_severe = Audiogram(frequencies=cfs, levels=levels_2)
+
     # Test asymmetric hearing loss in both orientations
-    processed = listen(ear, signal, audiogram_mild, audiogram_severe)
+    processed = listen(ear, signal, Listener(audiogram_mild, audiogram_severe))
     assert processed.shape == (2240, 2)
-    processed = listen(ear, signal, audiogram_severe, audiogram_mild)
+    processed = listen(ear, signal, Listener(audiogram_severe, audiogram_mild))
     assert processed.shape == (2240, 2)
 
 
diff --git a/tests/regression/test_full_CEC1_pipeline.py b/tests/regression/test_full_CEC1_pipeline.py
index dc2c8ea2b..fd6626659 100644
--- a/tests/regression/test_full_CEC1_pipeline.py
+++ b/tests/regression/test_full_CEC1_pipeline.py
@@ -130,16 +130,16 @@ def test_full_cec1_pipeline(regtest):
 
     ear = Ear(**msbg_ear_cfg)
 
-    left_audiogram = Audiogram(frequencies=audiogram_cfs, levels=audiogram_l)
-    right_audiogram = Audiogram(frequencies=audiogram_cfs, levels=audiogram_r)
+    audiogram_left = Audiogram(frequencies=audiogram_cfs, levels=audiogram_l)
+    audiogram_right = Audiogram(frequencies=audiogram_cfs, levels=audiogram_r)
 
     enhancer = NALR(**nalr_cfg)
     compressor = Compressor(**compressor_cfg)
 
-    nalr_fir, _ = enhancer.build(audiogram_l, audiogram_cfs)
+    nalr_fir, _ = enhancer.build(audiogram_left)
     out_l = enhancer.apply(nalr_fir, signal[:, 0])
 
-    nalr_fir, _ = enhancer.build(audiogram_r, audiogram_cfs)
+    nalr_fir, _ = enhancer.build(audiogram_right)
     out_r = enhancer.apply(nalr_fir, signal[:, 1])
 
     out_l, _, _ = compressor.process(out_l)
@@ -156,8 +156,8 @@ def test_full_cec1_pipeline(regtest):
     ddf_signal[:, 1] = unit_impulse(len(signal), int(MSBG_FS / 2))
 
     # Pass through MSBG hearing loss model
-    reference_processed = listen(ear, reference, left_audiogram, right_audiogram)
-    signal_processed = listen(ear, enhanced_audio, left_audiogram, right_audiogram)
+    reference_processed = listen(ear, reference, audiogram_left, audiogram_right)
+    signal_processed = listen(ear, enhanced_audio, audiogram_left, audiogram_right)
 
     # Calculate channel-specific unit impulse delay due to HL model and audiograms
     delay = find_delay_impulse(ddf_signal, initial_value=int(MSBG_FS / 2))
diff --git a/tests/regression/test_full_CEC2_pipeline.py b/tests/regression/test_full_CEC2_pipeline.py
index 671ebf8cf..1ccec30d3 100644
--- a/tests/regression/test_full_CEC2_pipeline.py
+++ b/tests/regression/test_full_CEC2_pipeline.py
@@ -13,7 +13,7 @@
 from clarity.enhancer.compressor import Compressor
 from clarity.enhancer.nalr import NALR
 from clarity.evaluator.haspi import haspi_v2_be
-from clarity.utils.audiogram import Audiogram
+from clarity.utils.audiogram import Audiogram, Listener
 
 # Pass some random data through code and compare with reference output
 # scene_renderer, enhancer, compressor, haspi
@@ -143,15 +143,18 @@ def test_full_cec2_pipeline(
     audiogram_r = np.array([45, 45, 60, 70, 60, 60, 80, 80])
     audiogram_cfs = np.array([250, 500, 1000, 2000, 3000, 4000, 6000, 8000])
 
+    audiogram_left = Audiogram(levels=audiogram_l, frequencies=audiogram_cfs)
+    audiogram_right = Audiogram(levels=audiogram_r, frequencies=audiogram_cfs)
+
     sample_rate = 44100
 
     enhancer = NALR(**nalr_cfg)
     compressor = Compressor(**compressor_cfg)  # type: ignore
 
-    nalr_fir, _ = enhancer.build(audiogram_l, audiogram_cfs)
+    nalr_fir, _ = enhancer.build(audiogram_left)
     out_l = enhancer.apply(nalr_fir, signal[:, 0])
 
-    nalr_fir, _ = enhancer.build(audiogram_r, audiogram_cfs)
+    nalr_fir, _ = enhancer.build(audiogram_right)
     out_r = enhancer.apply(nalr_fir, signal[:, 1])
 
     out_l, _, _ = compressor.process(out_l)
@@ -160,6 +163,7 @@ def test_full_cec2_pipeline(
     enhanced_audio = np.stack([out_l, out_r], axis=1)
 
     enhanced_audio = np.tanh(enhanced_audio)
+    listener = Listener(audiogram_left=audiogram_left, audiogram_right=audiogram_right)
 
     sii_enhanced = haspi_v2_be(
         reference_left=reference[:, 0],
@@ -167,8 +171,7 @@ def test_full_cec2_pipeline(
         processed_left=enhanced_audio[:, 0],
         processed_right=enhanced_audio[:, 1],
         sample_rate=sample_rate,
-        audiogram_left=Audiogram(levels=audiogram_l, frequencies=audiogram_cfs),
-        audiogram_right=Audiogram(levels=audiogram_r, frequencies=audiogram_cfs),
+        listener=listener,
     )
 
     regtest.write(f"Enhanced audio HASPI score is {sii_enhanced:0.7f}\n")
diff --git a/tests/utils/test_audiogram.py b/tests/utils/test_audiogram.py
index 7b252ca1c..a5b5ab6e4 100644
--- a/tests/utils/test_audiogram.py
+++ b/tests/utils/test_audiogram.py
@@ -1,5 +1,7 @@
 """Tests for audiogram module"""
 
+from pathlib import Path
+
 import numpy as np
 import pytest
 
@@ -9,6 +11,7 @@
     AUDIOGRAM_MODERATE_SEVERE,
     AUDIOGRAM_REF,
     Audiogram,
+    Listener,
 )
 
 
@@ -139,3 +142,35 @@ def test_resample(requested_frequencies, expected_levels):
     assert subset_audiogram.levels == pytest.approx(
         expected_levels, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
     )
+
+
+def test_listener_read_listener_dict():
+    """Test reading listener dictionary"""
+    listener_dict_path = Path("tests/test_data/metadata/listeners.json")
+    listeners_dict = Listener.load_listener_dict(listener_dict_path)
+
+    # Check the dataclass and underlying audiogram dataclass have
+    # been constructed correctly and that a sample of values
+    # match the file contents
+    assert len(listeners_dict) == 83
+    listener = listeners_dict["L0100"]
+    assert list(listener.__dict__.keys()) == ["audiogram_left", "audiogram_right", "id"]
+    assert listener.id == "L0100"
+    assert np.all(
+        listener.audiogram_left.frequencies
+        == np.array(
+            [
+                250,
+                500,
+                1000,
+                2000,
+                3000,
+                4000,
+                6000,
+                8000,
+            ]
+        )
+    )
+    assert np.all(
+        listener.audiogram_right.levels == np.array([20, 40, 55, 40, 20, 15, 5, -5])
+    )