Repository: claritychallenge/clarity (language: python)

Issue:
[pre-commit.ci] pre-commit-autoupdate
<!--pre-commit.ci start-->
updates:
- [github.com/psf/black: 23.11.0 → 23.12.1](https://github.com/psf/black/compare/23.11.0...23.12.1)
- [github.com/DavidAnson/markdownlint-cli2: v0.11.0 → v0.12.1](https://github.com/DavidAnson/markdownlint-cli2/compare/v0.11.0...v0.12.1)
- [github.com/pycqa/flake8.git: 6.1.0 → 7.0.0](https://github.com/pycqa/flake8.git/compare/6.1.0...7.0.0)
- [github.com/pycqa/isort: 5.12.0 → 5.13.2](https://github.com/pycqa/isort/compare/5.12.0...5.13.2)
- [github.com/nbQA-dev/nbQA: 1.7.0 → 1.7.1](https://github.com/nbQA-dev/nbQA/compare/1.7.0...1.7.1)
- [github.com/pre-commit/mirrors-mypy: v1.7.0 → v1.8.0](https://github.com/pre-commit/mirrors-mypy/compare/v1.7.0...v1.8.0)
- [github.com/astral-sh/ruff-pre-commit: v0.1.6 → v0.1.13](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.6...v0.1.13)
- [github.com/pycqa/pylint: v3.0.1 → v3.0.3](https://github.com/pycqa/pylint/compare/v3.0.1...v3.0.3)
<!--pre-commit.ci end-->
Bring CAD1 recipes improvements to main
The improvements done to recipes for CAD1, released in v0.3.2 aren't in the main branch.
Need to implement these in the main. cherry peak won't suffice are main diverged from v0.3.2



Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 7167117a7..954d048ab 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v4.4.0 # Use the ref you want to point at
+    rev: v4.5.0 # Use the ref you want to point at
     hooks:
       - id: trailing-whitespace
         types: [file, text]
@@ -20,39 +20,39 @@ repos:
       - id: check-toml
 
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.9.0
+    rev: v3.15.0
     hooks:
       - id: pyupgrade
         args: [--py38-plus]
 
   - repo: https://github.com/psf/black
-    rev: 23.7.0
+    rev: 23.12.1
     hooks:
       - id: black
         types: [python]
         additional_dependencies: ["click==8.0.4"]
 
   - repo: https://github.com/DavidAnson/markdownlint-cli2
-    rev: v0.8.1
+    rev: v0.12.1
     hooks:
       - id: markdownlint-cli2
 
   - repo: https://github.com/pycqa/flake8.git
-    rev: 6.0.0
+    rev: 7.0.0
     hooks:
       - id: flake8
         additional_dependencies: [flake8-print, Flake8-pyproject]
         types: [python]
 
   - repo: https://github.com/pycqa/isort
-    rev: 5.12.0
+    rev: 5.13.2
     hooks:
       - id: isort
         name: isort (python)
         args: ["--profile", "black"]
 
   - repo: https://github.com/nbQA-dev/nbQA
-    rev: 1.7.0
+    rev: 1.7.1
     hooks:
       - id: nbqa-black
       - id: nbqa-flake8
@@ -65,7 +65,7 @@ repos:
       - id: nbstripout
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.4.1
+    rev: v1.8.0
     hooks:
       - id: mypy
         args:
@@ -73,13 +73,13 @@ repos:
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
     # Ruff version.
-    rev: "v0.0.280"
+    rev: "v0.1.13"
     hooks:
       - id: ruff
 
   # Serious pylint errors that will be enforced by CI
   - repo: https://github.com/pycqa/pylint
-    rev: v3.0.0a6
+    rev: v3.0.3
     hooks:
       - id: pylint
         args:
diff --git a/README.md b/README.md
index 2d6eef2ad..43f5934d6 100644
--- a/README.md
+++ b/README.md
@@ -18,9 +18,11 @@
 [![pre-commit.ci status](https://results.pre-commit.ci/badge/github/claritychallenge/clarity/main.svg)](https://results.pre-commit.ci/latest/github/claritychallenge/clarity/main)
 [![Downloads](https://pepy.tech/badge/pyclarity)](https://pepy.tech/project/pyclarity)
 
-[![PyPI](https://img.shields.io/static/v1?label=CAD1%20and%20CPC2%20Challenges%20-%20pypi&message=v0.3.3&color=orange)](https://pypi.org/project/pyclarity/0.3.3/)
+[![PyPI](https://img.shields.io/static/v1?label=ICASSP%202024%20Cadenza%20Challenge%20-%20pypi&message=v0.4.1&color=orange)](https://pypi.org/project/pyclarity/0.4.1/)
+[![PyPI](https://img.shields.io/static/v1?label=CAD1%20and%20CPC2%20Challenges%20-%20pypi&message=v0.3.4&color=orange)](https://pypi.org/project/pyclarity/0.3.4/)
 [![PyPI](https://img.shields.io/static/v1?label=ICASSP%202023%20Challenge%20-%20pypi&message=v0.2.1&color=orange)](https://pypi.org/project/pyclarity/0.2.1/)
 [![PyPI](https://img.shields.io/static/v1?label=CEC2%20Challenge%20-%20pypi&message=v0.1.1&color=orange)](https://pypi.org/project/pyclarity/0.1.1/)
+
 [![ORDA](https://img.shields.io/badge/ORDA--DOI-10.15131%2Fshef.data.23230694.v.1-lightgrey)](https://figshare.shef.ac.uk/articles/software/clarity/23230694/1)
 </p>
 
@@ -34,10 +36,11 @@ In this repository, you will find code to support all Clarity and Cadenza Challe
 
 ## Current Events
 
-- The first Cadenza Challenge (CAD1) is now open. :fire::fire:
+- The ICASSP 2024 Cadenza Challenge (CAD_ICASSP_2024) is now open. :fire::fire:
   - Join the [Cadenza Challenge Group](https://groups.google.com/g/cadenza-challenge) to keep up-to-date on developments.
   - Visit the Cadenenza Challenge [website](https://cadenzachallenge.org/) for more details.
-  - Evaluation tools and a baseline system are available in the [CAD1 recipes](./recipes/cad1) folder. :new:
+- The first Cadenza Challenge (CAD1) is closed.
+  - Subjective Evaluation is underway. :new:
 - The 2nd Clarity Prediction Challenge (CPC2) is now open.   :fire::fire:
   - Join the [Clarity Challenge Group](https://groups.google.com/g/clarity-challenge) to keep up-to-date on developments.
   - Visit the Clarity Challenge [website](https://claritychallenge.org/) for more details.
@@ -86,11 +89,12 @@ pip install -e git+https://github.com/claritychallenge/clarity.git@main
 
 Current challenge
 
-- [The 1st Cadenza Challenge (CAD1)](./recipes/cad1)
-- [The 2nd Clarity Prediction Challenge (CPC2)](./recipes/cpc2)
+- [The ICASSP 2024 Cadenza Challenge](./recipes/cad_icassp_2024)
 
 Previous challenges
 
+- [The 1st Cadenza Challenge (CAD1)](./recipes/cad1)
+- [The 2nd Clarity Prediction Challenge (CPC2)](./recipes/cpc2)
 - [The ICASSP 2023 Enhancement Challenge](./recipes/icassp_2023)
 - [The 2nd Clarity Enhancement Challenge (CEC2)](./recipes/cec2)
 - [The 1st Clarity Prediction Challenge (CPC1)](./recipes/cpc1)
diff --git a/clarity/evaluator/haaqi/haaqi.py b/clarity/evaluator/haaqi/haaqi.py
index f4d19662c..b3ba3b035 100644
--- a/clarity/evaluator/haaqi/haaqi.py
+++ b/clarity/evaluator/haaqi/haaqi.py
@@ -178,8 +178,9 @@ def haaqi_v1(
 def compute_haaqi(
     processed_signal: ndarray,
     reference_signal: ndarray,
+    processed_sample_rate: float,
+    reference_sample_rate: float,
     audiogram: Audiogram,
-    sample_rate: float,
     equalisation: int = 1,
     level1: float = 65.0,
 ) -> float:
@@ -191,8 +192,9 @@ def compute_haaqi(
         reference_signal (np.ndarray): Input reference speech signal with no noise
             or distortion. If a hearing loss is specified, NAL-R equalization
             is optional
+        processed_sample_rate (float): Sampling rate in Hz for processed signal.
+        reference_sample_rate (float): Sampling rate in Hz for reference signal.
         audiogram (Audiogram): Audiogram object.
-        sample_rate (int): Sample rate in Hz.
         equalisation (int): hearing loss equalization mode for reference signal:
             1 = no EQ has been provided, the function will add NAL-R
             2 = NAL-R EQ has already been added to the reference signal
@@ -211,9 +213,9 @@ def compute_haaqi(
 
     score, _, _, _ = haaqi_v1(
         reference=reference_signal,
-        reference_freq=sample_rate,
+        reference_freq=reference_sample_rate,
         processed=processed_signal,
-        processed_freq=sample_rate,
+        processed_freq=processed_sample_rate,
         audiogram=audiogram,
         equalisation=equalisation,
         level1=level1,
diff --git a/clarity/evaluator/haspi/eb.py b/clarity/evaluator/haspi/eb.py
index 32692db92..415afe2cc 100644
--- a/clarity/evaluator/haspi/eb.py
+++ b/clarity/evaluator/haspi/eb.py
@@ -1288,13 +1288,10 @@ def env_smooth(envelopes: np.ndarray, segment_size: int, sample_rate: float) ->
     """
 
     # Compute the window
-    n_samples = int(
-        np.around(segment_size * (0.001 * sample_rate))
-    )  # Segment size in samples
-    test = n_samples - 2 * np.floor(n_samples / 2)  # 0=even, 1=odd
-    if test > 0:
-        # Force window length to be even
-        n_samples = n_samples + 1
+    # Segment size in samples
+    n_samples = int(np.around(segment_size * (0.001 * sample_rate)))
+    n_samples += n_samples % 2
+
     window = np.hanning(n_samples)  # Raised cosine von Hann window
     wsum = np.sum(window)  # Sum for normalization
 
@@ -1848,6 +1845,7 @@ def bm_covary(
         correlation = correlation[
             int(len(reference_seg) - 1 - maxlag) : int(maxlag + len(reference_seg))
         ]
+
         unbiased_cross_correlation = np.max(np.abs(correlation * half_corr))
         if (ref_mean_square > small) and (proc_mean_squared > small):
             # Normalize cross-covariance
@@ -1877,6 +1875,7 @@ def bm_covary(
             correlation = correlation[
                 int(len(reference_seg) - 1 - maxlag) : int(maxlag + len(reference_seg))
             ]
+
             unbiased_cross_correlation = np.max(np.abs(correlation * win_corr))
             if (ref_mean_square > small) and (proc_mean_squared > small):
                 # Normalize cross-covariance
@@ -1900,7 +1899,7 @@ def bm_covary(
         ref_mean_square = np.sum(reference_seg**2) * halfsum2
         proc_mean_squared = np.sum(processed_seg**2) * halfsum2
 
-        correlation = np.correlate(reference_seg, processed_seg, "full")
+        correlation = correlate(reference_seg, processed_seg, "full")
         correlation = correlation[
             int(len(reference_seg) - 1 - maxlag) : int(maxlag + len(reference_seg))
         ]
diff --git a/clarity/evaluator/msbg/smearing.py b/clarity/evaluator/msbg/smearing.py
index 29c59500d..a3f72b287 100644
--- a/clarity/evaluator/msbg/smearing.py
+++ b/clarity/evaluator/msbg/smearing.py
@@ -124,10 +124,11 @@ def smear3(f_smear: ndarray, inbuffer: ndarray) -> ndarray:
         winwave = np.zeros(FFT_SIZE)
         winwave[0:FRAME_SIZE] = window * inwave.flatten()
         spectrum = np.fft.fft(winwave, FFT_SIZE)
-        power = spectrum[0:nyquist] * np.conj(spectrum[0:nyquist])
-        mag = np.sqrt(power)
+
+        mag = np.abs(spectrum[0:nyquist])
         phasor = spectrum[0:nyquist] / (mag + (mag == 0))
-        smeared = np.dot(f_smear, power)
+
+        smeared = np.dot(f_smear, (mag**2).astype(complex))
         spectrum[0:nyquist] = np.sqrt(smeared) * phasor
         spectrum[nyquist] = 0
         spectrum[(nyquist + 1) : FFT_SIZE] = np.conj(spectrum[nyquist - 1 : 0 : -1])
diff --git a/clarity/utils/flac_encoder.py b/clarity/utils/flac_encoder.py
new file mode 100644
index 000000000..55718a98c
--- /dev/null
+++ b/clarity/utils/flac_encoder.py
@@ -0,0 +1,262 @@
+"""
+Class for encoding and decoding audio signals
+    using flac compression.
+"""
+from __future__ import annotations
+
+import logging
+import tempfile
+
+# pylint: disable=import-error, protected-access
+from pathlib import Path
+
+import numpy as np
+import pyflac as pf
+import soundfile as sf
+
+logger = logging.getLogger(__name__)
+
+
+class WavEncoder(pf.encoder._Encoder):
+    """
+    Class offers an adaptation of the pyflac.encoder.FileEncoder
+    to work directly with WAV signals as input.
+
+    """
+
+    def __init__(
+        self,
+        signal: np.ndarray,
+        sample_rate: int,
+        output_file: str | Path | None = None,
+        compression_level: int = 5,
+        blocksize: int = 0,
+        streamable_subset: bool = True,
+        verify: bool = False,
+    ) -> None:
+        """
+        Initialise the encoder.
+
+        Args:
+            signal (np.ndarray): The raw audio data to be encoded.
+            sample_rate (int): The sample rate of the audio data.
+            output_file (str | Path | None): Path to the output FLAC file,
+                a temporary file will be created if unspecified.
+            compression_level (int): The compression level parameter that
+                varies from 0 (fastest) to 8 (slowest). The default setting
+                is 5, see https://en.wikipedia.org/wiki/FLAC for more details.
+            blocksize (int): The size of the block to be returned in the
+                callback. The default is 0 which allows libFLAC to determine
+                the best block size.
+            streamable_subset (bool): Whether to use the streamable subset for encoding.
+                If true the encoder will check settings for compatibility. If false, the
+                settings may take advantage of the full range that the format allows.
+            verify (bool): If `True`, the encoder will verify it's own
+                encoded output by feeding it through an internal decoder and
+                comparing the original signal against the decoded signal.
+                If a mismatch occurs, the `process` method will raise a
+                `EncoderProcessException`.  Note that this will slow the
+                encoding process by the extra time required for decoding and comparison.
+        """
+        super().__init__()
+
+        self.__raw_audio = signal
+        self._sample_rate = sample_rate
+
+        if output_file:
+            self.__output_file = (
+                Path(output_file) if isinstance(output_file, str) else output_file
+            )
+        else:
+            with tempfile.NamedTemporaryFile(suffix=".flac") as ofile:
+                self.__output_file = Path(ofile.name)
+
+        self._blocksize = blocksize
+        self._compression_level = compression_level
+        self._streamable_subset = streamable_subset
+        self._verify = verify
+        self._initialised = False
+
+    def _init(self):
+        """
+        Initialise the encoder to write to a file.
+
+        Raises:
+            EncoderInitException: if initialisation fails.
+        """
+        c_output_filename = pf.encoder._ffi.new(
+            "char[]", str(self.__output_file).encode("utf-8")
+        )
+        rc = pf.encoder._lib.FLAC__stream_encoder_init_file(
+            self._encoder,
+            c_output_filename,
+            pf.encoder._lib._progress_callback,
+            self._encoder_handle,
+        )
+        pf.encoder._ffi.release(c_output_filename)
+        if rc != pf.encoder._lib.FLAC__STREAM_ENCODER_INIT_STATUS_OK:
+            raise pf.EncoderInitException(rc)
+
+        self._initialised = True
+
+    def process(self) -> bytes:
+        """
+        Process the audio data from the WAV file.
+
+        Returns:
+            (bytes): The FLAC encoded bytes.
+
+        Raises:
+            EncoderProcessException: if an error occurs when processing the samples
+        """
+        super().process(self.__raw_audio)
+        self.finish()
+        with open(self.__output_file, "rb") as f:
+            return f.read()
+
+
+class FileDecoder(pf.decoder.FileDecoder):
+    def process(self) -> tuple[np.ndarray, int]:
+        """
+        Overwritten version of the process method from the pyflac decoder.
+        Original process returns stereo signals in float64 format.
+
+        In this version, the data is returned using the original number
+        of channels and in in16 format.
+
+        Returns:
+            (tuple): A tuple of the decoded numpy audio array, and the sample rate
+                of the audio data.
+
+        Raises:
+            DecoderProcessException: if any fatal read, write, or memory allocation
+                error occurred (meaning decoding must stop)
+        """
+        result = pf.decoder._lib.FLAC__stream_decoder_process_until_end_of_stream(
+            self._decoder
+        )
+        if self.state != pf.decoder.DecoderState.END_OF_STREAM and not result:
+            raise pf.DecoderProcessException(str(self.state))
+
+        self.finish()
+        self.__output.close()
+        return sf.read(str(self.__output_file), always_2d=False, dtype="int16")
+
+
+class FlacEncoder:
+    """
+    Class for encoding and decoding audio signals using FLAC
+
+    It uses the pyflac library to encode and decode the audio data.
+    And offers convenient methods for encoding and decoding audio data.
+    """
+
+    def __init__(self, compression_level: int = 5) -> None:
+        """
+        Initialise the compressor.
+
+        Args:
+            compression_level (int): The compression level parameter that
+                varies from 0 (fastest) to 8 (slowest). The default setting
+                is 5, see https://en.wikipedia.org/wiki/FLAC for more details.
+        """
+        self.compression_level = compression_level
+
+    def encode(
+        self,
+        signal: np.ndarray,
+        sample_rate: int,
+        output_file: str | Path | None = None,
+    ) -> bytes:
+        """
+        Method to encode the audio data using FLAC compressor.
+
+        It creates a WavEncoder object and uses it to encode the audio data.
+
+        Args:
+            signal (np.ndarray): The raw audio data to be compressed.
+            sample_rate (int): The sample rate of the audio data.
+            output_file (str | Path): Path to where to
+                save the output FLAC file. If not specified, a temporary file
+                will be created.
+
+        Returns:
+            (bytes): The FLAC encoded audio signal.
+
+        Raises:
+            ValueError: If the audio signal is not in `np.int16` format.
+        """
+        if signal.dtype != np.int16:
+            logger.error(
+                f"FLAC encoder only supports 16-bit integer signals, "
+                f"but got {signal.dtype}"
+            )
+            raise ValueError(
+                f"FLAC encoder only supports 16-bit integer signals, "
+                f"but got {signal.dtype}"
+            )
+
+        wav_encoder = WavEncoder(
+            signal=signal,
+            sample_rate=sample_rate,
+            compression_level=self.compression_level,
+            output_file=output_file,
+        )
+        return wav_encoder.process()
+
+    @staticmethod
+    def decode(input_filename: Path | str) -> tuple[np.ndarray, float]:
+        """
+        Method to decode a flac file to wav audio data.
+
+        It uses the pyflac library to decode the flac file.
+
+        Args:
+            input_filename (pathlib.Path | str): Path to the input FLAC file.
+
+        Returns:
+            (np.ndarray): The raw audio data.
+
+        Raises:
+            FileNotFoundError: If the flac file to decode does not exist.
+        """
+        input_filename = (
+            Path(input_filename) if isinstance(input_filename, str) else input_filename
+        )
+
+        if not input_filename.exists():
+            logger.error(f"File {input_filename} not found.")
+            raise FileNotFoundError(f"File {input_filename} not found.")
+
+        decoder = FileDecoder(input_filename)
+        signal, sample_rate = decoder.process()
+
+        return signal, float(sample_rate)
+
+
+def read_flac_signal(filename: Path) -> tuple[np.ndarray, float]:
+    """Read a FLAC signal and return it as a numpy array
+
+    Args:
+        filename (Path): The path to the FLAC file to read.
+
+    Returns:
+        signal (np.ndarray): The decoded signal.
+        sample_rate (float): The sample rate of the signal.
+    """
+    # Create encoder object
+    flac_encoder = FlacEncoder()
+
+    # Decode FLAC file
+    signal, sample_rate = flac_encoder.decode(
+        filename,
+    )
+    signal = (signal / 32768.0).astype(np.float32)
+
+    # Load scale factor
+    if filename.with_suffix(".txt").exists():
+        with open(filename.with_suffix(".txt"), encoding="utf-8") as fp:
+            max_value = float(fp.read())
+            # Scale signal
+            signal *= max_value
+    return signal, sample_rate
diff --git a/clarity/utils/results_support.py b/clarity/utils/results_support.py
new file mode 100644
index 000000000..e77ffe7fa
--- /dev/null
+++ b/clarity/utils/results_support.py
@@ -0,0 +1,63 @@
+"""Dataclass to save challenges results to a CSV file."""
+from __future__ import annotations
+
+# pylint: disable=import-error
+import csv
+from dataclasses import dataclass
+from pathlib import Path
+
+
+@dataclass
+class ResultsFile:
+    """A utility class for writing results to a CSV file.
+
+    Attributes:
+        file_name (str | Path): The name of the file to write results to.
+        header_columns (list[str]): The columns to write to the CSV file.
+        append_results (bool): Whether to append results to an existing file.
+            If False, a new file will be created and the header row will be written.
+            Defaults to False.
+    """
+
+    file_name: str | Path
+    header_columns: list[str]
+    append_results: bool = False
+
+    def __post_init__(self):
+        """Write the header row to the CSV file."""
+        if isinstance(self.file_name, str):
+            self.file_name = Path(self.file_name)
+
+        if self.append_results:
+            if not Path(self.file_name).exists():
+                raise FileNotFoundError(
+                    "Cannot append results to non-existent file "
+                    f"{self.file_name.as_posix()}"
+                    " - please set append_results=False"
+                )
+        else:
+            with open(self.file_name, "w", encoding="utf-8", newline="") as csv_file:
+                csv_writer = csv.writer(
+                    csv_file, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
+                )
+                csv_writer.writerow(self.header_columns)
+
+    def add_result(
+        self,
+        row_values: dict[str, str | float],
+    ):
+        """Add a result to the CSV file.
+
+        Args:
+            row_values (dict[str, str | float]): The values to write to the CSV file.
+        """
+
+        with open(self.file_name, "a", encoding="utf-8", newline="") as csv_file:
+            csv_writer = csv.writer(
+                csv_file, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
+            )
+            row = []
+            for column in self.header_columns:
+                row.append(row_values[column])
+
+            csv_writer.writerow(row)
diff --git a/clarity/utils/signal_processing.py b/clarity/utils/signal_processing.py
index 0e9078ee6..0589ce267 100644
--- a/clarity/utils/signal_processing.py
+++ b/clarity/utils/signal_processing.py
@@ -1,12 +1,33 @@
 """Signal processing utilities."""
+# pylint: disable=import-error
 from __future__ import annotations
 
+# pylint: disable=import-error
 import numpy as np
 import scipy
 import soxr
 from numpy import ndarray
 
 
+def clip_signal(signal: np.ndarray, soft_clip: bool = False) -> tuple[np.ndarray, int]:
+    """Clip the signal.
+
+    Args:
+        signal (np.ndarray): Signal to be clipped and saved.
+        soft_clip (bool): Whether to use soft clipping.
+
+    Returns:
+        signal (np.ndarray): Clipped signal.
+        n_clipped (int): Number of samples clipped.
+    """
+
+    if soft_clip:
+        signal = np.tanh(signal)
+    n_clipped = np.sum(np.abs(signal) > 1.0)
+    signal = np.clip(signal, -1.0, 1.0)
+    return signal, int(n_clipped)
+
+
 def compute_rms(signal: ndarray) -> float:
     """Compute RMS of signal
 
@@ -86,3 +107,17 @@ def resample(
         )
 
     raise ValueError(f"Unknown resampling method: {method}")
+
+
+def to_16bit(signal: np.ndarray) -> np.ndarray:
+    """Convert the signal to 16 bit.
+
+    Args:
+        signal (np.ndarray): Signal to be converted.
+
+    Returns:
+        signal (np.ndarray): Converted signal.
+    """
+    signal = signal * 32768.0
+    signal = np.clip(signal, -32768.0, 32767.0)
+    return signal.astype(np.dtype("int16"))
diff --git a/clarity/utils/source_separation_support.py b/clarity/utils/source_separation_support.py
new file mode 100644
index 000000000..0b4279fd7
--- /dev/null
+++ b/clarity/utils/source_separation_support.py
@@ -0,0 +1,96 @@
+"""Module that contains functions for source separation."""
+from __future__ import annotations
+
+# pylint: disable=import-error
+import torch
+from torchaudio.transforms import Fade
+
+
+def separate_sources(
+    model: torch.nn.Module,
+    mix: torch.Tensor,
+    sample_rate: int,
+    segment: float = 10.0,
+    overlap: float = 0.1,
+    device: torch.device | str | None = None,
+):
+    """
+    Apply model to a given mixture.
+    Use fade, and add segments together in order to add model segment by segment.
+
+    Args:
+        model (torch.nn.Module): model to use for separation
+        mix (torch.Tensor): mixture to separate, shape (batch, channels, time)
+        sample_rate (int): sampling rate of the mixture
+        segment (float): segment length in seconds
+        overlap (float): overlap between segments, between 0 and 1
+        device (torch.device, str, or None): if provided, device on which to
+            execute the computation, otherwise `mix.device` is assumed.
+            When `device` is different from `mix.device`, only local computations will
+            be on `device`, while the entire tracks will be stored on `mix.device`.
+
+    Returns:
+        torch.Tensor: estimated sources
+
+    Based on https://pytorch.org/audio/main/tutorials/hybrid_demucs_tutorial.html
+    """
+    device = mix.device if device is None else torch.device(device)
+    mix = torch.as_tensor(mix, device=device)
+
+    if mix.ndim == 2:
+        # one track and stereo audio
+        mix = mix.unsqueeze(0)
+
+    batch, channels, length = mix.shape
+
+    chunk_len = int(sample_rate * segment * (1 + overlap))
+    start = 0
+    end = chunk_len
+    overlap_frames = overlap * sample_rate
+    fade = Fade(fade_in_len=0, fade_out_len=int(overlap_frames), fade_shape="linear")
+
+    final = torch.zeros(batch, 4, channels, length, device=device)
+
+    while start < length - overlap_frames:
+        chunk = mix[:, :, start:end]
+        with torch.no_grad():
+            out = model.forward(chunk)
+        out = fade(out)
+        final[:, :, :, start:end] += out
+        if start == 0:
+            fade.fade_in_len = int(overlap_frames)
+            start += int(chunk_len - overlap_frames)
+        else:
+            start += chunk_len
+        end += chunk_len
+        if end >= length:
+            fade.fade_out_len = 0
+
+    return final.cpu().detach().numpy()
+
+
+def get_device(device: str) -> tuple:
+    """Get the Torch device.
+
+    Args:
+        device (str): device type, e.g. "cpu", "gpu0", "gpu1", etc.
+
+    Returns:
+        torch.device: torch.device() appropiate to the hardware available.
+        str: device type selected, e.g. "cpu", "cuda".
+    """
+    if device is None:
+        if torch.cuda.is_available():
+            return torch.device("cuda"), "cuda"
+        return torch.device("cpu"), "cpu"
+
+    if device.startswith("gpu"):
+        device_index = int(device.replace("gpu", ""))
+        if device_index > torch.cuda.device_count():
+            raise ValueError(f"GPU device index {device_index} is not available.")
+        return torch.device(f"cuda:{device_index}"), "cuda"
+
+    if device == "cpu":
+        return torch.device("cpu"), "cpu"
+
+    raise ValueError(f"Unsupported device type: {device}")
diff --git a/notebooks/03_Running_the_CEC2_baseline_from_python.ipynb b/notebooks/03_Running_the_CEC2_baseline_from_python.ipynb
index 396b0b2f7..8d3d455cf 100644
--- a/notebooks/03_Running_the_CEC2_baseline_from_python.ipynb
+++ b/notebooks/03_Running_the_CEC2_baseline_from_python.ipynb
@@ -147,7 +147,7 @@
     "from omegaconf import DictConfig, OmegaConf\n",
     "\n",
     "cfg = OmegaConf.load(\"clarity/recipes/cec2/baseline/config.yaml\")\n",
-    "assert type(cfg) == DictConfig"
+    "assert isinstance(cfg, DictConfig)"
    ]
   },
   {
diff --git a/pyproject.toml b/pyproject.toml
index 5380d9f05..1f9fa9d24 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,103 +1,92 @@
 [build-system]
-requires = [
-  "setuptools >= 45",
-  "setuptools_scm[toml]>=6.2",
-  "wheel",
-  ]
+requires = ["setuptools >= 45", "setuptools_scm[toml]>=6.2", "wheel"]
 build-backend = "setuptools.build_meta"
 
 [project]
 name = "pyclarity"
 description = "Tools for the Clarity Challenge"
 readme = "README.md"
-license = {text = "MIT"}
+license = { text = "MIT" }
 dynamic = ["version"]
 authors = [
-  {name = "The PyClarity Team", email = "claritychallengecontact@gmail.com"},
+    { name = "The PyClarity Team", email = "claritychallengecontact@gmail.com" },
 ]
 classifiers = [
-  "Intended Audience :: Developers",
-  "Intended Audience :: Education",
-  "Intended Audience :: Science/Research",
-  "License :: OSI Approved :: MIT License",
-  "Operating System :: OS Independent",
-  "Programming Language :: Python :: 3",
-  "Natural Language :: English",
-]
-keywords = [
-  "hearing",
-  "signal processing",
-  "clarity challenge"
-]
+    "Intended Audience :: Developers",
+    "Intended Audience :: Education",
+    "Intended Audience :: Science/Research",
+    "License :: OSI Approved :: MIT License",
+    "Operating System :: OS Independent",
+    "Programming Language :: Python :: 3",
+    "Natural Language :: English",
+]
+keywords = ["hearing", "signal processing", "clarity challenge"]
 requires-python = ">=3.8"
 dependencies = [
-  "audioread>=2.1.9",
-  "gdown",
-  "hydra-core>=1.1.1",
-  "hydra-submitit-launcher>=1.1.6",
-  "importlib-metadata",
-  "librosa>=0.8.1",
-  "matplotlib",
-  "numba>=0.57.0rc",
-  "numpy>=1.21.6",
-  "omegaconf>=2.1.1",
-  "pandas>=1.3.5",
-  "pyloudnorm>=0.1.0",
-  "pystoi",
-  "pytorch-lightning",
-  "resampy",
-  "scikit-learn>=1.0.2",
-  "scipy>=1.7.3",
-  "SoundFile>=0.10.3.post1",
-  "soxr",
-  "torch>=2",
-  "torchaudio",
-  "tqdm>=4.62.3",
-  "typing_extensions",
+    "audioread>=2.1.9",
+    "gdown",
+    "hydra-core>=1.1.1",
+    "hydra-submitit-launcher>=1.1.6",
+    "importlib-metadata",
+    "librosa>=0.8.1",
+    "matplotlib",
+    "numba>=0.57.0rc",
+    "numpy>=1.21.6",
+    "omegaconf>=2.1.1",
+    "pandas>=1.3.5",
+    "pyflac",
+    "pyloudnorm>=0.1.0",
+    "pystoi",
+    "pytorch-lightning",
+    "resampy",
+    "scikit-learn>=1.0.2",
+    "scipy>=1.7.3",
+    "SoundFile>=0.10.3.post1",
+    "soxr",
+    "torch>=2",
+    "torchaudio",
+    "tqdm>=4.62.3",
+    "typing_extensions",
 ]
 
 [project.optional-dependencies]
 tests = [
-  "coverage",
-  "isort",
-  "flake8",
-  "flake8-print",
-  "Flake8-pyproject",
-  "mypy",
-  "pre-commit",
-  "py",
-  "py-cpuinfo",
-  "pytest",
-  "pytest-cov",
-  "pytest-mock",
-  "pytest-mpl",
-  "pytest-regtest",
-  "pytest-skip-slow",
-  "pytest-xdist",
-  "yamllint",
-]
-docs =[
-  "sphinx",
-  "myst_parser",
-  "pydata_sphinx_theme",
-  "sphinx_markdown_tables",
-  "sphinx_rtd_theme",
-  "sphinxcontrib-mermaid",
-  "sphinxcontrib-napoleon",
+    "coverage",
+    "isort",
+    "flake8",
+    "flake8-print",
+    "Flake8-pyproject",
+    "mypy",
+    "pre-commit",
+    "py",
+    "py-cpuinfo",
+    "pytest",
+    "pytest-cov",
+    "pytest-mock",
+    "pytest-mpl",
+    "pytest-regtest",
+    "pytest-skip-slow",
+    "pytest-xdist",
+    "yamllint",
+]
+docs = [
+    "sphinx",
+    "myst_parser",
+    "pydata_sphinx_theme",
+    "sphinx_markdown_tables",
+    "sphinx_rtd_theme",
+    "sphinxcontrib-mermaid",
+    "sphinxcontrib-napoleon",
 ]
 dev = [
-  "black",
-  "pre-commit",
-  "pycodestyle",
-  "pylint",
-  "pylint-pytest",
-  "yamllint",
-]
-pypi =[
-  "build",
-  "wheel",
-  "setuptools_scm[toml]"
-]
+    "black",
+    "pre-commit",
+    "pycodestyle",
+    "pylint",
+    "pylint-pytest",
+    "yamllint",
+]
+pypi = ["build", "wheel", "setuptools_scm[toml]"]
 
 [project.urls]
 Source = "https://github.com/claritychallenge/clarity"
@@ -117,17 +106,8 @@ exclude = ["tests*"]
 namespaces = false
 
 [tool.setuptools.package-data]
-clarity = [
-  "*.json",
-  "*.mat",
-  "*.yaml"
-]
-recipes = [
-  "*.csv",
-  "*.json",
-  "*.mat",
-  "*.yaml"
-]
+clarity = ["*.json", "*.mat", "*.yaml"]
+recipes = ["*.csv", "*.json", "*.mat", "*.yaml"]
 
 
 [tool.setuptools_scm]
@@ -139,21 +119,12 @@ git_describe_command = "git describe --tags"
 [tool.pytest.ini_options]
 minversion = "7.0"
 addopts = "--cov clarity"
-testpaths = [
-    "tests",
-]
-filterwarnings = [
-    "ignore::UserWarning"
-]
+testpaths = ["tests"]
+filterwarnings = ["ignore::UserWarning"]
 
 [tool.coverage.run]
 source = ["clarity"]
-omit = [
-  "*conftest.py",
-  "*tests*",
-  "**/__init__*",
-  "clarity/_version.py",
-]
+omit = ["*conftest.py", "*tests*", "**/__init__*", "clarity/_version.py"]
 
 [tool.black]
 line-length = 88
@@ -170,23 +141,11 @@ exclude = '''
 '''
 
 [flake8]
-ignore =[
-  "E203",
-  "E501",
-  "W503",
-]
+ignore = ["E203", "E501", "W503"]
 # docstring-convention = ""
 max-line-length = 88
 max-complexity = 18
-select = [
-  "B",
-  "C",
-  "E",
-  "F",
-  "W",
-  "T4",
-  "B9"
-]
+select = ["B", "C", "E", "F", "W", "T4", "B9"]
 
 [tool.ruff]
 exclude = [
@@ -220,7 +179,4 @@ fixable = ["A", "B", "C", "D", "E", "F", "R", "S", "W", "U"]
 
 [tool.mypy]
 ignore_missing_imports = true
-exclude = [
-    "docs/*",
-    "build/*"
-]
+exclude = ["docs/*", "build/*"]
diff --git a/recipes/cad1/README.md b/recipes/cad1/README.md
index dca206dca..f40609ba6 100644
--- a/recipes/cad1/README.md
+++ b/recipes/cad1/README.md
@@ -21,37 +21,42 @@ The performance of each system on the validation set is reported below.
 
 ### Task 1 - Listening music via headphones
 
-**The overall HAAQI score is 0.3608.**
+The overall HAAQI score is:
+
+- Demucs: **0.2592**
+- Open-Unmix: **0.2273**
 
 #### Average HAAQI score per song
 
-| Song                                            |   HAAQI    |
-|:------------------------------------------------|:----------:|
-| Actions - One Minute Smile                      |   0.3066   |
-| Alexander Ross - Goodbye Bolero                 |   0.4257   |
-| ANiMAL - Rockshow                               |   0.2389   |
-| Clara Berry And Wooldog - Waltz For My Victims  |   0.4202   |
-| Fergessen - Nos Palpitants                      |   0.4554   |
-| James May - On The Line                         |   0.3889   |
-| Johnny Lokke - Promises & Lies                  |   0.3395   |
-| Leaf - Summerghost                              |   0.3595   |
-| Meaxic - Take A Step                            |   0.3470   |
-| Patrick Talbot - A Reason To Leave              |   0.4545   |
-| Skelpolu - Human Mistakes                       |   0.3055   |
-| Triviul - Angelsaint                            |   0.2883   |
+| Song                                           | Demucs | Open-UnMix |
+|:-----------------------------------------------|:------:|:----------:|
+| Actions - One Minute Smile                     | 0.2485 |   0.2257   |
+| Alexander Ross - Goodbye Bolero                | 0.3084 |   0.2574   |
+| ANiMAL - Rockshow                              | 0.1843 |   0.1864   |
+| Clara Berry And Wooldog - Waltz For My Victims | 0.3094 |   0.2615   |
+| Fergessen - Nos Palpitants                     | 0.3542 |   0.2592   |
+| James May - On The Line                        | 0.2778 |   0.2398   |
+| Johnny Lokke - Promises & Lies                 | 0.2544 |   0.2261   |
+| Leaf - Summerghost                             | 0.2513 |   0.2105   |
+| Meaxic - Take A Step                           | 0.2455 |   0.2239   |
+| Patrick Talbot - A Reason To Leave             | 0.2673 |   0.2331   |
+| Skelpolu - Human Mistakes                      | 0.2123 |   0.1951   |
+| Traffic Experiment - Sirens                    | 0.2558 |   0.2339   |
+| Triviul - Angelsaint                           | 0.2101 |   0.1955   |
+| Young Griffo - Pennies                         | 0.2499 |   0.2297   |
 
 ### Task 2 - Listening music in a car with presence of noise
 
-**The overall HAAQI score is 0.1248.**
+**The overall HAAQI score is 0.1423.**
 
 #### Average HAAQI score per genre
 
-| Genre          |   HAAQI    |
-|:---------------|:----------:|
-| Classical      |   0.1240   |
-| Hip-Hop        |   0.1271   |
-| Instrumental   |   0.1250   |
-| International  |   0.1267   |
-| Orchestral     |   0.1121   |
-| Pop            |   0.1339   |
-| Rock           |   0.1252   |
+| Genre          | HAAQI  |
+|:---------------|:------:|
+| Classical      | 0.1365 |
+| Hip-Hop        | 0.1462 |
+| Instrumental   | 0.1416 |
+| International  | 0.1432 |
+| Orchestral     | 0.1329 |
+| Pop            | 0.1498 |
+| Rock           | 0.1460 |
diff --git a/recipes/cad1/task1/baseline/README.md b/recipes/cad1/task1/baseline/README.md
index 7732be324..a8f082706 100644
--- a/recipes/cad1/task1/baseline/README.md
+++ b/recipes/cad1/task1/baseline/README.md
@@ -15,8 +15,8 @@ To download the data, please visit [here](https://forms.gle/UQkuCxqQVxZtGggPA).
 Alternatively, you can download the MUSDB18-HQ dataset from the official [SigSep website](https://sigsep.github.io/datasets/musdb.html#musdb18-hq-uncompressed-wav).
 If you opt for this alternative, be sure to download the uncompressed wav version. Note that you will need both packages to run the baseline system.
 
-If you need additional music data for training your model, please restrict to the use of [MedleyDB](https://medleydb.weebly.com/) [4] [5],
-[BACH10](https://labsites.rochester.edu/air/resource.html) [6] and [FMA-small](https://github.com/mdeff/fma) [7].
+If you need additional music data for training your model, please restrict to the use of [MedleyDB](https://medleydb.weebly.com/) [[4](#4-references)] [[5](#4-references)],
+[BACH10](https://labsites.rochester.edu/air/resource.html) [[6](#4-references)] and [FMA-small](https://github.com/mdeff/fma) [[7](#4-references)].
 Theses are shared as `cadenza_cad1_task1_augmentation_medleydb.tar.gz`, `cadenza_cad1_task1_augmentation_bach10.tar.gz`
 and `cadenza_cad1_task1_augmentation_fma_small.tar.gz`.
 **Keeping the augmentation data restricted to these datasets will ensure that the evaluation is fair for all participants**.
@@ -56,7 +56,7 @@ cadenza_data
 
 ### 1.2 Additional optional data
 
-* **MedleyDB** contains both MedleyDB versions 1 [[4](#references)] and 2 [[5](#references)] datasets.
+* **MedleyDB** contains both MedleyDB versions 1 [[4](#4-references)] and 2 [[5](#4-references)] datasets.
 
 Tracks from the MedleyDB dataset are not included in the evaluation set.
 However, is your responsibility to exclude any song that may be already contained in the training set.
@@ -70,7 +70,7 @@ cadenza_data
             └───Metadata
 ```
 
-* **BACH10** contains the BACH10 dataset [[6](#references)].
+* **BACH10** contains the BACH10 dataset [[6](#4-references)].
 
 Tracks from the BACH10 dataset are not included in MUSDB18-HQ and can all be used as training augmentation data.
 
@@ -84,7 +84,7 @@ cadenza_data
             ├───...
 ```
 
-* **FMA Small** contains the FMA small subset of the FMA dataset [[7](references)].
+* **FMA Small** contains the FMA small subset of the FMA dataset [[7](#4-references)].
 
 Tracks from the FMA small dataset are not included in the MUSDB18-HQ.
 This dataset does not provide independent stems but only the full mix.
@@ -123,18 +123,26 @@ Note that we use [hydra](https://hydra.cc/docs/intro/) for config handling.
 
 ### 2.1 Enhancement
 
-The baseline enhance simply takes the out-of-the-box [Hybrid Demucs](https://github.com/facebookresearch/demucs) [1]
+We offer two baseline systems:
+
+1. Using the out-of-the-box time-domain [Hybrid Demucs](https://github.com/facebookresearch/demucs) [[1](#4-references)]
 source separation model distributed on [TorchAudio](https://pytorch.org/audio/main/tutorials/hybrid_demucs_tutorial.html)
-and applies a simple NAL-R [2] fitting amplification to each VDBO (`vocals`, `drums`, `bass` and `others`) stem.
+2. Using the out-of-the-box spectrogram-based [Open-Unmix](https://github.com/sigsep/open-unmix-pytorch)
+source separation model (version `umxhq`) distributed through [PyTorch Hub](https://pytorch.org/hub/)
 
-The remixing is performed by summing the amplified VDBO stems.
+Both system use the same enhancement strategy; using the music separation model, the baseline system estimates the
+VDBO (`vocals`, `drums`, `bass` and `others`) stems. Then, they apply a simple NAL-R [[2](#4-references)] fitting amplification to each of them.
+These results on eight mono signals (four from the left channel and four from the right channel). Finally, each signal is downsampled to 24000 Hertz, convert to 16bit precision and
+encoded using the lossless FLAC compression. These eight signal are then used for the objective evaluation (HAAQI).
 
-The baseline generates a left and right signal for each VDBO stem and a remixed signal, totalling 9 signals per song-listener.
+The baselines also provide a remixing strategy to generate a stereo signal for each listener. This is done by summing
+the amplified VDBO stems, where each channel (left and right in stereo) is composed of the addition of the corresponding
+four stems. This stereo remixed signal is then used for subjective evaluation (listening panel).
 
 To run the baseline enhancement system first, make sure that `paths.root` in `config.yaml` points to
 where you have installed the Cadenza data. This parameter defaults to the working directory.
-You can also define your own `path.exp_folder` to store enhanced
-signals and evaluated results.
+You can also define your own `path.exp_folder` to store the enhanced signals and evaluated results and select what
+music separation model you want to employ.
 
 Then run:
 
@@ -158,9 +166,8 @@ The folder `enhanced_signals` will appear in the `exp` folder.
 
 ### 2.2 Evaluation
 
-The `evaluate.py` simply takes the signals stored in `enhanced_signals` and computes the HAAQI [[3](#references)] score
-for each of the eight left and right VDBO stems.
-The average of these eight scores is computed and returned for each signal.
+The `evaluate.py` script takes the eight VDBO signals stored in `enhanced_signals` and computes the
+HAAQI [[3](#4-references)] score. The final score for the sample is the average of the scores of each stem.
 
 To run the evaluation stage, make sure that `path.root` is set in the `config.yaml` file and then run
 
@@ -172,13 +179,19 @@ A csv file containing the eight HAAQI scores and the combined score will be gene
 
 To check the HAAQI code, see [here](../../../../clarity/evaluator/haaqi).
 
-Please note: you will not get identical HAAQI scores for the same signals if the random seed is not defined
-(in the given recipe, the random seed for each signal is set as the last eight digits of the song md5).
-As there are random noises generated within HAAQI, but the differences should be sufficiently small.
+Please note: you will not get identical HAAQI scores for the same signals if the random seed is not defined.
+This is due to the  random noises generated within HAAQI, but the differences should be sufficiently small.
+For reproducibility, in the given recipe, the random seed for each signal is set as the last eight digits
+of the song md5.
+
+## 3. Results
+
+The overall HAAQI score for each baseline is:
 
-The score for the baseline is 0.3608 HAAQI overall.
+* Demucs: **0.2592**
+* Open-Unmix: **0.2273**
 
-## References
+## 4. References
 
 * [1] Défossez, A. "Hybrid Spectrogram and Waveform Source Separation". Proceedings of the ISMIR 2021 Workshop on Music Source Separation. [doi:10.48550/arXiv.2111.03600](https://arxiv.org/abs/2111.03600)
 * [2] Byrne, Denis, and Harvey Dillon. "The National Acoustic Laboratories'(NAL) new procedure for selecting the gain and frequency response of a hearing aid." Ear and hearing 7.4 (1986): 257-265. [doi:10.1097/00003446-198608000-00007](https://doi.org/10.1097/00003446-198608000-00007)
diff --git a/recipes/cad1/task1/baseline/config.yaml b/recipes/cad1/task1/baseline/config.yaml
index 88de32bf8..bfac09086 100644
--- a/recipes/cad1/task1/baseline/config.yaml
+++ b/recipes/cad1/task1/baseline/config.yaml
@@ -2,18 +2,20 @@ path:
   root: ../../cadenza_data_demo/cad1/task1
   metadata_dir: ${path.root}/metadata
   music_dir: ${path.root}/audio/musdb18hq
-  music_train_file: ${path.metadata_dir}/musdb18.train.json
-  music_valid_file: ${path.metadata_dir}/musdb18.valid.json
-  listeners_train_file: ${path.metadata_dir}/listeners.train.json
-  listeners_valid_file: ${path.metadata_dir}/listeners.valid.json
-  exp_folder: ./exp # folder to store enhanced signals and final results
+  music_file: ${path.metadata_dir}/musdb18.valid.json
+  listeners_file: ${path.metadata_dir}/listeners.valid.json
+  music_segments_test_file: ${path.metadata_dir}/musdb18.segments.test.json
+  exp_folder: ./exp_${separator.model} # folder to store enhanced signals and final results
 
+team_id: T001
 
-sample_rate: 44100
+sample_rate: 44100       # sample rate of the input mixture
+stem_sample_rate: 24000  # sample rate output stems
+remix_sample_rate: 32000 # sample rate for output remixed signal
 
 nalr:
   nfir: 220
-  fs: ${sample_rate}
+  sample_rate: ${sample_rate}
 
 apply_compressor: False
 compressor:
@@ -27,7 +29,6 @@ soft_clip: True
 
 separator:
   model: demucs   # demucs or openunmix
-  sources: [drums, bass, other, vocals]
   device: ~
 
 evaluate:
diff --git a/recipes/cad1/task1/baseline/enhance.py b/recipes/cad1/task1/baseline/enhance.py
index 1daf24a92..610ebd67d 100644
--- a/recipes/cad1/task1/baseline/enhance.py
+++ b/recipes/cad1/task1/baseline/enhance.py
@@ -15,12 +15,19 @@
 from omegaconf import DictConfig
 from scipy.io import wavfile
 from torchaudio.pipelines import HDEMUCS_HIGH_MUSDB
-from torchaudio.transforms import Fade, Resample
+from torchaudio.transforms import Fade
 
 from clarity.enhancer.compressor import Compressor
 from clarity.enhancer.nalr import NALR
 from clarity.utils.audiogram import Audiogram, Listener
-from clarity.utils.signal_processing import denormalize_signals, normalize_signal
+from clarity.utils.flac_encoder import FlacEncoder
+from clarity.utils.signal_processing import (
+    clip_signal,
+    denormalize_signals,
+    normalize_signal,
+    resample,
+    to_16bit,
+)
 from recipes.cad1.task1.baseline.evaluate import make_song_listener_list
 
 logger = logging.getLogger(__name__)
@@ -28,7 +35,7 @@
 
 def separate_sources(
     model: torch.nn.Module,
-    mix: torch.Tensor,
+    mix: torch.Tensor | ndarray,
     sample_rate: int,
     segment: float = 10.0,
     overlap: float = 0.1,
@@ -119,11 +126,11 @@ def get_device(device: str) -> tuple:
     raise ValueError(f"Unsupported device type: {device}")
 
 
-def map_to_dict(sources: np.ndarray, sources_list: list[str]) -> dict:
+def map_to_dict(sources: ndarray, sources_list: list[str]) -> dict:
     """Map sources to a dictionary separating audio into left and right channels.
 
     Args:
-       sources (np.ndarray): Signal to be mapped to dictionary.
+       sources (ndarray): Signal to be mapped to dictionary.
        sources_list (list): List of strings used to index dictionary.
 
     Returns:
@@ -142,13 +149,15 @@ def map_to_dict(sources: np.ndarray, sources_list: list[str]) -> dict:
 
 # pylint: disable=unused-argument
 def decompose_signal(
-    config: DictConfig,
     model: torch.nn.Module,
-    signal: np.ndarray,
-    sample_rate: int,
+    model_sample_rate: int,
+    signal: ndarray,
+    signal_sample_rate: int,
     device: torch.device,
+    sources_list: list[str],
     listener: Listener,
-) -> dict[str, np.ndarray]:
+    normalise: bool = True,
+) -> dict[str, ndarray]:
     """
     Decompose signal into 8 stems.
 
@@ -158,47 +167,46 @@ def decompose_signal(
     HDEMUCS model trained on the MUSDB18 dataset.
 
     Args:
-        config (DictConfig): Configuration object.
         model (torch.nn.Module): Torch model.
-        signal (np.ndarray): Signal to be decomposed.
-        sample_rate (int): Sample frequency.
+        model_sample_rate (int): Sample rate of the model.
+        signal (ndarray): Signal to be decomposed.
+        signal_sample_rate (int): Sample frequency.
         device (torch.device): Torch device to use for processing.
-        listener (Listener).
+        sources_list (list): List of strings used to index dictionary.
+        listener (Listener): Listener object.
+        normalise (bool): Whether to normalise the signal.
 
      Returns:
          Dictionary: Indexed by sources with the associated model as values.
     """
 
-    if config.separator.model == "demucs":
-        signal, ref = normalize_signal(signal)
-
-    model_sample_rate = (
-        model.sample_rate if config.separator.model == "openunmix" else 44100
-    )
+    # Resample mixture signal to model sample rate
+    if signal_sample_rate != model_sample_rate:
+        signal = resample(signal, signal_sample_rate, model_sample_rate)
 
-    if sample_rate != model_sample_rate:
-        resampler = Resample(sample_rate, model_sample_rate)
-        signal = resampler(signal)
+    if normalise:
+        signal, ref = normalize_signal(signal)
 
     sources = separate_sources(
-        model, torch.from_numpy(signal), sample_rate, device=device
+        model, torch.from_numpy(signal), signal_sample_rate, device=device
     )
     # only one element in the batch
     sources = sources[0]
-    if config.separator.model == "demucs":
+
+    if normalise:
         sources = denormalize_signals(sources, ref)
 
-    signal_stems = map_to_dict(sources, config.separator.sources)
+    signal_stems = map_to_dict(sources, sources_list)
     return signal_stems
 
 
 def apply_baseline_ha(
     enhancer: NALR,
     compressor: Compressor,
-    signal: np.ndarray,
+    signal: ndarray,
     audiogram: Audiogram,
     apply_compressor: bool = False,
-) -> np.ndarray:
+) -> ndarray:
     """
     Apply NAL-R prescription hearing aid to a signal.
 
@@ -206,14 +214,12 @@ def apply_baseline_ha(
         enhancer: A NALR object that enhances the signal.
         compressor: A Compressor object that compresses the signal.
         signal: An ndarray representing the audio signal.
-        listener_audiogram: An ndarray representing the listener's audiogram.
-        cfs: An ndarray of center frequencies.
+        audiogram: An Audiogram object representing the listener's audiogram.
         apply_compressor: A boolean indicating whether to include the compressor.
 
     Returns:
         An ndarray representing the processed signal.
     """
-    print("XXX", audiogram)
     nalr_fir, _ = enhancer.build(audiogram)
     proc_signal = enhancer.apply(nalr_fir, signal)
     if apply_compressor:
@@ -234,8 +240,7 @@ def process_stems_for_listener(
         stems (dict) : Dictionary of stems
         enhancer (NALR) : NAL-R prescription hearing aid
         compressor (Compressor) : Compressor
-        listener: Listener object
-        cfs (np.ndarray) : Center frequencies
+        listener (Listener) : Listener object.
         apply_compressor (bool) : Whether to apply the compressor
     Returns:
         processed_sources (dict) : Dictionary of processed stems
@@ -261,27 +266,84 @@ def process_stems_for_listener(
     return processed_stems
 
 
-def clip_signal(signal: np.ndarray, soft_clip: bool = False) -> tuple[np.ndarray, int]:
-    """Clip and save the processed stems.
+def remix_signal(stems: dict) -> ndarray:
+    """
+    Function to remix signal. It takes the eight stems
+    and combines them into a stereo signal.
 
     Args:
-        signal (np.ndarray): Signal to be clipped and saved.
-        soft_clip (bool): Whether to use soft clipping.
+        stems (dict) : Dictionary of stems
 
     Returns:
-        signal (np.ndarray): Clipped signal.
-        n_clipped (int): Number of samples clipped.
+        (ndarray) : Remixed signal
+
+    """
+    n_samples = stems[list(stems.keys())[0]].shape[0]
+    out_left, out_right = np.zeros(n_samples), np.zeros(n_samples)
+    for stem_str, stem_signal in stems.items():
+        if stem_str.startswith("l"):
+            out_left += stem_signal
+        else:
+            out_right += stem_signal
+
+    return np.stack([out_left, out_right], axis=1)
+
+
+def save_flac_signal(
+    signal: ndarray,
+    filename: Path,
+    signal_sample_rate: int,
+    output_sample_rate: int,
+    do_clip_signal: bool = False,
+    do_soft_clip: bool = False,
+    do_scale_signal: bool = False,
+) -> None:
     """
+    Function to save output signals.
+
+    - The output signal will be resample to ``output_sample_rate``
+    - The output signal will be clipped to [-1, 1] if ``do_clip_signal`` is True
+        and use soft clipped if ``do_soft_clip`` is True. Note that if
+        ``do_clip_signal`` is False, ``do_soft_clip`` will be ignored.
+        Note that if ``do_clip_signal`` is True, ``do_scale_signal`` will be ignored.
+    - The output signal will be scaled to [-1, 1] if ``do_scale_signal`` is True.
+        If signal is scale, the scale factor will be saved in a TXT file.
+        Note that if ``do_clip_signal`` is True, ``do_scale_signal`` will be ignored.
+    - The output signal will be saved as a FLAC file.
 
-    if soft_clip:
-        signal = np.tanh(signal)
-    n_clipped = np.sum(np.abs(signal) > 1.0)
-    np.clip(signal, -1.0, 1.0, out=signal)
-    return signal, int(n_clipped)
+    Args:
+        signal (np.ndarray) : Signal to save
+        filename (Path) : Path to save signal
+        signal_sample_rate (int) : Sample rate of the input signal
+        output_sample_rate (int) : Sample rate of the output signal
+        do_clip_signal (bool) : Whether to clip signal
+        do_soft_clip (bool) : Whether to apply soft clipping
+        do_scale_signal (bool) : Whether to scale signal
+    """
+    # Resample signal to expected output sample rate
+    if signal_sample_rate != output_sample_rate:
+        signal = resample(signal, signal_sample_rate, output_sample_rate)
+
+    if do_scale_signal:
+        # Scale stem signal
+        max_value = np.max(np.abs(signal))
+        signal = signal / max_value
+
+        # Save scale factor
+        with open(filename.with_suffix(".txt"), "w", encoding="utf-8") as file:
+            file.write(f"{max_value}")
+
+    elif do_clip_signal:
+        # Clip the signal
+        signal, n_clipped = clip_signal(signal, do_soft_clip)
+        if n_clipped > 0:
+            logger.warning(f"Writing {filename}: {n_clipped} samples clipped")
 
+    # Convert signal to 16-bit integer
+    signal = to_16bit(signal)
 
-def to_16bit(signal: np.ndarray) -> np.ndarray:
-    return (32768.0 * signal).astype(np.int16)
+    # Create flac encoder object to compress and save the signal
+    FlacEncoder().encode(signal, output_sample_rate, filename)
 
 
 @hydra.main(config_path="", config_name="config")
@@ -298,47 +360,39 @@ def enhance(config: DictConfig) -> None:
         - right channel vocal, drums, bass, and other stems
     """
 
+    if config.separator.model not in ["demucs", "openunmix"]:
+        raise ValueError(f"Separator model {config.separator.model} not supported.")
+
     enhanced_folder = Path("enhanced_signals")
     enhanced_folder.mkdir(parents=True, exist_ok=True)
 
-    # Training stage
-    #
-    # The baseline is using an off-the-shelf model trained on the MUSDB18 dataset
-    # Training listeners and song are not necessary in this case.
-    #
-    # Training songs and audiograms can be read like this:
-    #
-    #  with open(config.path.listeners_train_file, "r", encoding="utf-8") as file:
-    #        listener_train_audiograms = json.load(file)
-    #
-    #  with open(config.path.music_train_file, "r", encoding="utf-8") as file:
-    #        song_data = json.load(file)
-    #  songs_train = pd.DataFrame.from_dict(song_data)
-    #
-    # train_song_listener_pairs = make_song_listener_list(
-    #     songs_train['Track Name'], listener_train_audiograms
-    # )
-
     if config.separator.model == "demucs":
         separation_model = HDEMUCS_HIGH_MUSDB.get_model()
-    else:
+        model_sample_rate = HDEMUCS_HIGH_MUSDB.sample_rate
+        sources_order = separation_model.sources
+        normalise = True
+    elif config.separator.model == "openunmix":
         separation_model = torch.hub.load("sigsep/open-unmix-pytorch", "umxhq", niter=0)
+        model_sample_rate = separation_model.sample_rate
+        sources_order = ["vocals", "drums", "bass", "other"]
+        normalise = False
+    else:
+        raise ValueError(f"Separator model {config.separator.model} not supported.")
+
     device, _ = get_device(config.separator.device)
     separation_model.to(device)
 
     # Processing Validation Set
     # Load listener audiograms and songs
-    listener_dict = Listener.load_listener_dict(config.path.listeners_valid_file)
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
 
-    with open(config.path.music_valid_file, encoding="utf-8") as file:
+    with open(config.path.music_file, encoding="utf-8") as file:
         song_data = json.load(file)
-    songs_valid = pd.DataFrame.from_dict(song_data)
+    songs_df = pd.DataFrame.from_dict(song_data)
 
-    valid_song_listener_pairs = make_song_listener_list(
-        songs_valid["Track Name"], listener_dict
-    )
+    song_listener_pairs = make_song_listener_list(songs_df["Track Name"], listener_dict)
     # Select a batch to process
-    valid_song_listener_pairs = valid_song_listener_pairs[
+    song_listener_pairs = song_listener_pairs[
         config.evaluate.batch :: config.evaluate.batch_size
     ]
 
@@ -348,8 +402,8 @@ def enhance(config: DictConfig) -> None:
     # Decompose each song into left and right vocal, drums, bass, and other stems
     # and process each stem for the listener
     prev_song_name = None
-    num_song_list_pair = len(valid_song_listener_pairs)
-    for idx, song_listener in enumerate(valid_song_listener_pairs, 1):
+    num_song_list_pair = len(song_listener_pairs)
+    for idx, song_listener in enumerate(song_listener_pairs, 1):
         song_name, listener_name = song_listener
         logger.info(
             f"[{idx:03d}/{num_song_list_pair:03d}] "
@@ -361,14 +415,15 @@ def enhance(config: DictConfig) -> None:
         # Find the music split directory
         split_directory = (
             "test"
-            if songs_valid.loc[songs_valid["Track Name"] == song_name, "Split"].iloc[0]
+            if songs_df.loc[songs_df["Track Name"] == song_name, "Split"].iloc[0]
             == "test"
             else "train"
         )
 
-        # Read the mixture signal
-        # Convert to 32-bit floating point and transpose
-        # from [samples, channels] to [channels, samples]
+        # Baseline Steps
+        # 1. Decompose the mixture signal into vocal, drums, bass, and other stems
+        #    We validate if 2 consecutive signals are the same to avoid
+        #    decomposing the same song multiple times
         if prev_song_name != song_name:
             # Decompose song only once
             prev_song_name = song_name
@@ -383,17 +438,20 @@ def enhance(config: DictConfig) -> None:
             assert sample_rate == config.sample_rate
 
             stems: dict[str, ndarray] = decompose_signal(
-                config,
                 separation_model,
+                model_sample_rate,
                 mixture_signal,
                 sample_rate,
                 device,
+                sources_order,
                 listener,
+                normalise,
             )
 
-        # Baseline applies NALR prescription to each stem instead of using the
-        # listener's audiograms in the decomposition. This stem can be skipped
-        # if the listener's audiograms are used in the decomposition
+        # 2. Apply NAL-R prescription to each stem
+        #     Baseline applies NALR prescription to each stem instead of using the
+        #     listener's audiograms in the decomposition. This step can be skipped
+        #     if the listener's audiograms are used in the decomposition
         processed_stems = process_stems_for_listener(
             stems,
             enhancer,
@@ -402,42 +460,41 @@ def enhance(config: DictConfig) -> None:
             config.apply_compressor,
         )
 
-        # save processed stems
-        n_samples = processed_stems[list(processed_stems.keys())[0]].shape[0]
-        output_left, output_right = np.zeros(n_samples), np.zeros(n_samples)
+        # 3. Save processed stems
         for stem_str, stem_signal in processed_stems.items():
-            if stem_str.startswith("l"):
-                output_left += stem_signal
-            else:
-                output_right += stem_signal
-
             filename = (
                 enhanced_folder
                 / f"{listener.id}"
                 / f"{song_name}"
-                / f"{listener.id}_{song_name}_{stem_str}.wav"
+                / f"{listener.id}_{song_name}_{stem_str}.flac"
             )
             filename.parent.mkdir(parents=True, exist_ok=True)
+            save_flac_signal(
+                signal=stem_signal,
+                filename=filename,
+                signal_sample_rate=config.sample_rate,
+                output_sample_rate=config.stem_sample_rate,
+                do_scale_signal=True,
+            )
 
-            # Clip and save stem signals
-            clipped_signal, n_clipped = clip_signal(stem_signal, config.soft_clip)
-            if n_clipped > 0:
-                logger.warning(f"Writing {filename}: {n_clipped} samples clipped")
-            wavfile.write(filename, config.sample_rate, to_16bit(clipped_signal))
+        # 4. Remix Signal
+        enhanced = remix_signal(processed_stems)
 
-        enhanced = np.stack([output_left, output_right], axis=1)
+        # 5. Save enhanced (remixed) signal
         filename = (
             enhanced_folder
             / f"{listener.id}"
             / f"{song_name}"
-            / f"{listener.id}_{song_name}_remix.wav"
+            / f"{listener.id}_{song_name}_remix.flac"
+        )
+        save_flac_signal(
+            signal=enhanced,
+            filename=filename,
+            signal_sample_rate=config.sample_rate,
+            output_sample_rate=config.remix_sample_rate,
+            do_clip_signal=True,
+            do_soft_clip=config.soft_clip,
         )
-
-        # clip and save enhanced signal
-        clipped_signal, n_clipped = clip_signal(enhanced, config.soft_clip)
-        if n_clipped > 0:
-            logger.warning(f"Writing {filename}: {n_clipped} samples clipped")
-        wavfile.write(filename, config.sample_rate, to_16bit(clipped_signal))
 
 
 # pylint: disable = no-value-for-parameter
diff --git a/recipes/cad1/task1/baseline/evaluate.py b/recipes/cad1/task1/baseline/evaluate.py
index 469513bc6..1543a9b96 100644
--- a/recipes/cad1/task1/baseline/evaluate.py
+++ b/recipes/cad1/task1/baseline/evaluate.py
@@ -3,7 +3,6 @@
 
 # pylint: disable=too-many-locals
 # pylint: disable=import-error
-import csv
 import hashlib
 import itertools
 import json
@@ -18,87 +17,13 @@
 
 from clarity.evaluator.haaqi import compute_haaqi
 from clarity.utils.audiogram import Listener
-from clarity.utils.signal_processing import compute_rms
+from clarity.utils.flac_encoder import read_flac_signal
+from clarity.utils.results_support import ResultsFile
+from clarity.utils.signal_processing import compute_rms, resample
 
 logger = logging.getLogger(__name__)
 
 
-class ResultsFile:
-    """A utility class for writing results to a CSV file.
-
-    Attributes:
-        file_name (str): The name of the file to write results to.
-    """
-
-    def __init__(self, file_name: str):
-        """Initialize the ResultsFile instance.
-
-        Args:
-            file_name (str): The name of the file to write results to.
-        """
-        self.file_name = file_name
-
-    def write_header(self):
-        """Write the header row to the CSV file."""
-        with open(self.file_name, "w", encoding="utf-8", newline="") as csv_file:
-            csv_writer = csv.writer(
-                csv_file, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
-            )
-            csv_writer.writerow(
-                [
-                    "song",
-                    "listener",
-                    "score",
-                    "left_bass",
-                    "right_bass",
-                    "left_drums",
-                    "right_drums",
-                    "left_other",
-                    "right_other",
-                    "left_vocals",
-                    "right_vocals",
-                ]
-            )
-
-    def add_result(
-        self,
-        listener_id: str,
-        song: str,
-        score: float,
-        instruments_scores: dict[str, float],
-    ):
-        """Add a result to the CSV file.
-
-        Args:
-            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
-                channel in the result.
-        """
-        logger.info(f"The combined score is {score}")
-
-        with open(self.file_name, "a", encoding="utf-8", newline="") as csv_file:
-            csv_writer = csv.writer(
-                csv_file, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
-            )
-            csv_writer.writerow(
-                [
-                    song,
-                    listener_id,
-                    str(score),
-                    str(instruments_scores["left_bass"]),
-                    str(instruments_scores["right_bass"]),
-                    str(instruments_scores["left_drums"]),
-                    str(instruments_scores["right_drums"]),
-                    str(instruments_scores["left_other"]),
-                    str(instruments_scores["right_other"]),
-                    str(instruments_scores["left_vocals"]),
-                    str(instruments_scores["right_vocals"]),
-                ]
-            )
-
-
 def set_song_seed(song: str) -> None:
     """Set a seed that is unique for the given song"""
     song_encoded = hashlib.md5(song.encode("utf-8")).hexdigest()
@@ -161,45 +86,63 @@ def _evaluate_song_listener(
         )
         reference_signal = (reference_signal / 32768.0).astype(np.float32)
 
-        # Load enhanced instrument signals
-        # Load left channel
-        sample_rate_left_enhanced_signal, left_enhanced_signal = wavfile.read(
+        # Read left instrument enhanced
+        left_enhanced_signal, sample_rate_left_enhanced_signal = read_flac_signal(
             enhanced_folder
             / f"{listener.id}"
             / f"{song}"
-            / f"{listener.id}_{song}_left_{instrument}.wav"
+            / f"{listener.id}_{song}_left_{instrument}.flac"
         )
-        left_enhanced_signal = (left_enhanced_signal / 32768.0).astype(np.float32)
 
-        # Load right channel
-        sample_rate_right_enhanced_signal, right_enhanced_signal = wavfile.read(
+        # Read right instrument enhanced
+        right_enhanced_signal, sample_rate_right_enhanced_signal = read_flac_signal(
             enhanced_folder
             / f"{listener.id}"
             / f"{song}"
-            / f"{listener.id}_{song}_right_{instrument}.wav"
+            / f"{listener.id}_{song}_right_{instrument}.flac"
         )
-        right_enhanced_signal = (right_enhanced_signal / 32768.0).astype(np.float32)
 
-        assert (
-            sample_rate_reference_signal
-            == sample_rate_left_enhanced_signal
-            == sample_rate_right_enhanced_signal
-            == config.sample_rate
-        )
+        if (
+            sample_rate_left_enhanced_signal
+            != sample_rate_right_enhanced_signal
+            != config.stem_sample_rate
+        ):
+            raise ValueError(
+                "The sample rates of the left and right enhanced signals are not "
+                "the same"
+            )
+
+        if sample_rate_reference_signal != config.sample_rate:
+            raise ValueError(
+                f"The sample rate of the reference signal is not {config.sample_rate}"
+            )
 
         per_instrument_score[f"left_{instrument}"] = compute_haaqi(
-            left_enhanced_signal,
-            reference_signal[:, 0],
-            listener.audiogram_left,
-            config.sample_rate,
-            65 - 20 * np.log10(compute_rms(reference_signal[:, 0])),
+            processed_signal=left_enhanced_signal,
+            reference_signal=resample(
+                reference_signal[:, 0],
+                sample_rate_reference_signal,
+                config.stem_sample_rate,
+            ),
+            processed_sample_rate=config.stem_sample_rate,
+            reference_sample_rate=config.stem_sample_rate,
+            audiogram=listener.audiogram_left,
+            equalisation=1,
+            level1=65 - 20 * np.log10(compute_rms(reference_signal[:, 0])),
         )
+
         per_instrument_score[f"right_{instrument}"] = compute_haaqi(
-            right_enhanced_signal,
-            reference_signal[:, 1],
-            listener.audiogram_right,
-            config.sample_rate,
-            65 - 20 * np.log10(compute_rms(reference_signal[:, 1])),
+            processed_signal=right_enhanced_signal,
+            reference_signal=resample(
+                reference_signal[:, 1],
+                sample_rate_reference_signal,
+                config.stem_sample_rate,
+            ),
+            processed_sample_rate=config.stem_sample_rate,
+            reference_sample_rate=config.stem_sample_rate,
+            audiogram=listener.audiogram_right,
+            equalisation=1,
+            level1=65 - 20 * np.log10(compute_rms(reference_signal[:, 1])),
         )
 
     # Compute the combined score
@@ -212,23 +155,43 @@ def _evaluate_song_listener(
 def run_calculate_aq(config: DictConfig) -> None:
     """Evaluate the enhanced signals using the HAAQI-RMS metric."""
     # Load test songs
-    with open(config.path.music_valid_file, encoding="utf-8") as fp:
+    with open(config.path.music_file, encoding="utf-8") as fp:
         songs = json.load(fp)
-    songs = pd.DataFrame.from_dict(songs)
+    songs_df = pd.DataFrame.from_dict(songs)
 
     # Load listener data
-    listener_dict = Listener.load_listener_dict(config.path.listeners_valid_file)
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
 
     enhanced_folder = Path("enhanced_signals")
     logger.info(f"Evaluating from {enhanced_folder} directory")
 
+    scores_headers = [
+        "song",
+        "listener",
+        "score",
+        "left_bass",
+        "right_bass",
+        "left_drums",
+        "right_drums",
+        "left_other",
+        "right_other",
+        "left_vocals",
+        "right_vocals",
+    ]
+
+    results_file_name = "scores.csv"
+    if config.evaluate.batch_size > 1:
+        results_file_name = (
+            f"scores_{config.evaluate.batch + 1}-{config.evaluate.batch_size}.csv"
+        )
+
     results_file = ResultsFile(
-        f"scores_{config.evaluate.batch + 1}-{config.evaluate.batch_size}.csv"
+        file_name=results_file_name,
+        header_columns=scores_headers,
     )
-    results_file.write_header()
 
     song_listener_pair = make_song_listener_list(
-        songs["Track Name"].tolist(), listener_dict, config.evaluate.small_test
+        songs_df["Track Name"].tolist(), listener_dict, config.evaluate.small_test
     )
 
     song_listener_pair = song_listener_pair[
@@ -237,21 +200,23 @@ def run_calculate_aq(config: DictConfig) -> None:
 
     for song, listener_id in song_listener_pair:
         split_dir = "train"
-        if songs[songs["Track Name"] == song]["Split"].tolist()[0] == "test":
+        if songs_df[songs_df["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,
-            enhanced_folder,
+            song=song,
+            listener=listener,
+            config=config,
+            split_dir=split_dir,
+            enhanced_folder=enhanced_folder,
         )
         results_file.add_result(
-            listener.id,
-            song,
-            score=combined_score,
-            instruments_scores=per_instrument_score,
+            {
+                "song": song,
+                "listener": listener.id,
+                "score": combined_score,
+                **per_instrument_score,
+            }
         )
 
 
diff --git a/recipes/cad1/task2/baseline/README.md b/recipes/cad1/task2/baseline/README.md
index 0dfe7aaaa..bcb910074 100644
--- a/recipes/cad1/task2/baseline/README.md
+++ b/recipes/cad1/task2/baseline/README.md
@@ -8,8 +8,8 @@ For more information please visit the [challenge website](https://cadenzachallen
 
 ### 1.1 Obtaining the CAD1 - Task2 data
 
-The music dataset for the First Cadenza Challenge - Task 2 is based on the small subset of the FMA [2] dataset
-(FMA-small) and the MTG-Jamendo dataset [4]. The dataset contains 1000 samples from seven musical genres,
+The music dataset for the First Cadenza Challenge - Task 2 is based on the small subset of the FMA [[2](#4-references)] dataset
+(FMA-small) and the MTG-Jamendo dataset [[4](#4-references)]. The dataset contains 1000 samples from seven musical genres,
 totalling 7000 songs with a distribution of 80% / 10% / 10% for `train`, `valid` and `test`.
 
 From FMA small:
@@ -82,17 +82,18 @@ If you have an Anaconda or Miniconda environment, you can install them as:
 * conda install -c conda-forge ffmpeg
 * conda install -c conda-forge libsndfile
 
-```bash
-
 ### 2.1 Enhancement
 
 The objective of the enhancement stage is takes a song and optimise it to a listener hearing characteristics
-knowing metadata information about the car noise scenario (you won't have access to noise signal), head
+knowing metadata information about the car noise scenario (note that you won't have access to noise signal), head
 rotation of the listener and the SNR of the enhanced music and the noise at the hearing aid microphones.
 
-In the baseline, we simply attenuate the song according to the average hearing loss and save it in 16-bit PCM WAV format.
+In the baseline, we attenuate the song according to the average hearing loss. The output are stereo signals
+that we save usi ng 32000 Hertz sample rate, 16bit precision, and we encoded it using the lossless FLAC compression.
 This attenuation prevents some clipping in the hearing aid output signal.
 
+The resulting signals are used for both, the objective (HAAQI) and subjective (listening panel) evaluation.
+
 To run the baseline enhancement system first, make sure that `paths.root` in `config.yaml` points to
 where you have installed the Cadenza data foer the task2. This parameter defaults to one level above the recipe
 for the demo data. You can also define your own `path.exp_folder` to store enhanced and evaluated signal results.
@@ -120,8 +121,9 @@ The folder `enhanced_signals` will appear in the `exp` folder.
 ### 2.2 Evaluation
 
 The `evaluate.py` module takes the enhanced signals and adds the room impulses and the car noise using
-the expected SNR. It then passes that signal through a fixed hearing aid. The hearing aid output and
-the reference song are used to compute the HAAQI [2] score.
+the expected SNR. It then passes that signal through a fixed hearing aid. The hearing aid is composed of
+NAL-R [[1](#4-references)] prescription and compression. The hearing aid output signal and
+the reference song are used to compute the HAAQI [[2](#4-references)] score.
 
 To run the evaluation stage, make sure that `path.root` is set in the `config.yaml` file and then run
 
@@ -138,9 +140,9 @@ Please note: you will not get identical HAAQI scores for the same signals if the
 (in the given recipe, the random seed for each signal is set as the last eight digits of the song md5).
 As there are random noises generated within HAAQI, but the differences should be sufficiently small.
 
-The overall HAAQI score for baseline is 0.1248.
+The overall HAAQI score for baseline is **0.1423**.
 
-## References
+## 4. References
 
 * [1] Byrne, Denis, and Harvey Dillon. "The National Acoustic Laboratories'(NAL) new procedure for selecting the gain and frequency response of a hearing aid." Ear and hearing 7.4 (1986): 257-265. [doi:10.1097/00003446-198608000-00007](https://doi.org/10.1097/00003446-198608000-00007)
 * [2] Kates J M, Arehart K H. "The Hearing-Aid Audio Quality Index (HAAQI)". IEEE/ACM transactions on audio, speech, and language processing, 24(2), 354–365. [doi:10.1109/TASLP.2015.2507858](https://doi.org/10.1109%2FTASLP.2015.2507858)
diff --git a/recipes/cad1/task2/baseline/baseline_utils.py b/recipes/cad1/task2/baseline/baseline_utils.py
index fe67c02f3..5a458fc31 100644
--- a/recipes/cad1/task2/baseline/baseline_utils.py
+++ b/recipes/cad1/task2/baseline/baseline_utils.py
@@ -1,6 +1,7 @@
 """Utility functions for the baseline model."""
 from __future__ import annotations
 
+# pylint: disable=import-error
 import json
 import logging
 import warnings
@@ -13,9 +14,6 @@
 
 from clarity.utils.audiogram import Listener
 
-# pylint: disable=import-error
-
-
 logger = logging.getLogger(__name__)
 
 
@@ -40,7 +38,7 @@ def read_mp3(
                 str(file_path),
                 sr=sample_rate,
                 mono=False,
-                res_type="kaiser_best",
+                res_type="soxr_hq",
                 dtype=np.float32,
             )
     except Exception as error:
@@ -95,12 +93,9 @@ def load_listeners_and_scenes(
         df_scenes = pd.read_json(fp, orient="index")
 
     # Load audiograms and scene data for the corresponding split
-    if config.evaluate.split == "train":
-        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":
-        listeners = Listener.load_listener_dict(config.path.listeners_valid_file)
-        scenes = df_scenes[df_scenes["split"] == "valid"].to_dict("index")
+    listeners = Listener.load_listener_dict(config.path.listeners_file)
+    if config.evaluate.split in ["train", "valid", "test"]:
+        scenes = df_scenes[df_scenes["split"] == config.evaluate.split].to_dict("index")
     else:
         raise ValueError(f"Unknown split {config.evaluate.split}")
 
diff --git a/recipes/cad1/task2/baseline/car_scene_acoustics.py b/recipes/cad1/task2/baseline/car_scene_acoustics.py
index 9fe8cc3e1..8bf5510ac 100644
--- a/recipes/cad1/task2/baseline/car_scene_acoustics.py
+++ b/recipes/cad1/task2/baseline/car_scene_acoustics.py
@@ -112,9 +112,7 @@ def apply_hearing_aid(self, signal: np.ndarray, audiogram: Audiogram) -> np.ndar
 
         Args:
             signal (np.ndarray): The audio signal to be enhanced.
-            audiogram (np.ndarray): An audiogram used to configure the NALR object.
-            center_frequencies (np.ndarray): An array of center frequencies
-                used to configure the NALR object.
+            audiogram (Audiogram): The audiogram of the listener.
 
         Returns:
             np.ndarray: The enhanced audio signal.
diff --git a/recipes/cad1/task2/baseline/config.yaml b/recipes/cad1/task2/baseline/config.yaml
index 36a5ceb2f..ff84fe766 100644
--- a/recipes/cad1/task2/baseline/config.yaml
+++ b/recipes/cad1/task2/baseline/config.yaml
@@ -4,18 +4,20 @@ path:
   metadata_dir: ${path.root}/metadata
   music_dir: ${path.audio_dir}/music
   hrtf_dir: ${path.audio_dir}/eBrird
-  listeners_train_file: ${path.metadata_dir}/listeners.train.json
-  listeners_valid_file: ${path.metadata_dir}/listeners.valid.json
+  listeners_file: ${path.metadata_dir}/listeners.valid.json
   scenes_file: ${path.metadata_dir}/scenes.json
   scenes_listeners_file: ${path.metadata_dir}/scenes_listeners.json
   hrtf_file: ${path.metadata_dir}/eBrird_BRIR.json
   exp_folder: ./exp # folder to store enhanced signals and final results
 
-sample_rate: 44100
+team_id: T001
+
+sample_rate: 44100          # sample rate of the input signal
+enhanced_sample_rate: 32000 # sample rate for the enhanced output signal
 
 nalr:
   nfir: 220
-  fs: ${sample_rate}
+  sample_rate: ${sample_rate}
 
 compressor:
   threshold: 0.7
diff --git a/recipes/cad1/task2/baseline/enhance.py b/recipes/cad1/task2/baseline/enhance.py
index eda3ac47e..6c85a6b88 100644
--- a/recipes/cad1/task2/baseline/enhance.py
+++ b/recipes/cad1/task2/baseline/enhance.py
@@ -12,10 +12,11 @@
 import numpy as np
 import pyloudnorm as pyln
 from omegaconf import DictConfig
-from scipy.io import wavfile
 from tqdm import tqdm
 
 from clarity.utils.audiogram import Listener
+from clarity.utils.flac_encoder import FlacEncoder
+from clarity.utils.signal_processing import clip_signal, resample, to_16bit
 from recipes.cad1.task2.baseline.baseline_utils import (
     make_scene_listener_list,
     read_mp3,
@@ -30,7 +31,7 @@ def compute_average_hearing_loss(listener: Listener) -> float:
     Compute the average hearing loss of a listener.
 
     Args:
-        listener (dict): The audiogram of the listener.
+        listener (Listener): The listener.
 
     Returns:
         average_hearing_loss (float): The average hearing loss of the listener.
@@ -56,7 +57,7 @@ def enhance_song(
 
     Args:
         waveform (np.ndarray): The waveform of the song.
-        listener_dict (dict): The audiograms of the listener.
+        listener (Listener): The listener.
         config (dict): Dictionary of configuration options for enhancing music.
 
     Returns:
@@ -110,6 +111,7 @@ def enhance(config: DictConfig) -> None:
         config.evaluate.batch :: config.evaluate.batch_size
     ]
 
+    flac_encoder = FlacEncoder()
     for scene_id, listener_id in tqdm(scene_listener_pairs):
         current_scene = scenes[scene_id]
         listener = listener_dict[listener_id]
@@ -121,23 +123,26 @@ def enhance(config: DictConfig) -> None:
         out_l, out_r = enhance_song(
             waveform=song_waveform, listener=listener, config=config
         )
-
         enhanced = np.stack([out_l, out_r], axis=1)
-        filename = f"{scene_id}_{listener.id}_{current_scene['song']}.wav"
 
+        # Save the enhanced song
         enhanced_folder_listener = enhanced_folder / f"{listener.id}"
         enhanced_folder_listener.mkdir(parents=True, exist_ok=True)
+        filename = (
+            enhanced_folder_listener
+            / f"{scene_id}_{listener.id}_{current_scene['song']}.flac"
+        )
 
-        # Clip and save
-        if config.soft_clip:
-            enhanced = np.tanh(enhanced)
-        n_clipped = np.sum(np.abs(enhanced) > 1.0)
+        # - Resample to 32 kHz sample rate
+        # - Clip signal
+        # - Convert to 16bit
+        # - Compress using flac
+        enhanced = resample(enhanced, config.sample_rate, config.enhanced_sample_rate)
+        clipped_signal, n_clipped = clip_signal(enhanced, config.soft_clip)
         if n_clipped > 0:
             logger.warning(f"Writing {filename}: {n_clipped} samples clipped")
-        np.clip(enhanced, -1.0, 1.0, out=enhanced)
-        signal_16 = (32768.0 * enhanced).astype(np.int16)
-        wavfile.write(
-            enhanced_folder_listener / filename, config.sample_rate, signal_16
+        flac_encoder.encode(
+            to_16bit(clipped_signal), config.enhanced_sample_rate, filename
         )
 
 
diff --git a/recipes/cad1/task2/baseline/evaluate.py b/recipes/cad1/task2/baseline/evaluate.py
index b4413ef9f..b3a202b6c 100644
--- a/recipes/cad1/task2/baseline/evaluate.py
+++ b/recipes/cad1/task2/baseline/evaluate.py
@@ -3,7 +3,6 @@
 # pylint: disable=import-error
 from __future__ import annotations
 
-import csv
 import hashlib
 import logging
 from pathlib import Path
@@ -11,11 +10,12 @@
 import hydra
 import numpy as np
 from omegaconf import DictConfig
-from scipy.io import wavfile
 from tqdm import tqdm
 
 from clarity.evaluator.haaqi import compute_haaqi
 from clarity.utils.audiogram import Listener
+from clarity.utils.flac_encoder import read_flac_signal
+from clarity.utils.results_support import ResultsFile
 from recipes.cad1.task2.baseline.audio_manager import AudioManager
 from recipes.cad1.task2.baseline.baseline_utils import (
     load_hrtf,
@@ -28,81 +28,6 @@
 logger = logging.getLogger(__name__)
 
 
-class ResultsFile:
-    """A utility class for writing results to a CSV file.
-
-    Attributes:
-        file_name (str): The name of the file to write results to.
-    """
-
-    def __init__(self, file_name):
-        """Initialize the ResultsFile instance.
-
-        Args:
-            file_name (str): The name of the file to write results to.
-        """
-        self.file_name = file_name
-
-    def write_header(self):
-        """Write the header row to the CSV file."""
-        with open(self.file_name, "w", encoding="utf-8") as csv_f:
-            csv_writer = csv.writer(
-                csv_f, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
-            )
-            csv_writer.writerow(
-                [
-                    "scene",
-                    "song",
-                    "genre",
-                    "listener",
-                    "score",
-                    "haaqi_left",
-                    "haaqi_right",
-                ]
-            )
-
-    # pylint: disable=too-many-arguments
-    def add_result(
-        self,
-        scene: str,
-        song: str,
-        genre: str,
-        listener: str,
-        score: float,
-        haaqi_left: float,
-        haaqi_right: float,
-    ):
-        """Add a result to the CSV file.
-
-        Args:
-            scene (str): The name of the scene that the result is for.
-            song (str): The name of the song that the result is for.
-            genre (str): The genre of the song that the result is for.
-            listener (str): The name of the listener who submitted the result.
-            score (float): The combined score for the result.
-            haaqi_left (float): The HAAQI score for the left channel.
-            haaqi_right (float): The HAAQI score for the right channel.
-        """
-
-        logger.info(f"The combined score for scene {scene}: {score:.4f}")
-
-        with open(self.file_name, "a", encoding="utf-8") as csv_f:
-            csv_writer = csv.writer(
-                csv_f, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
-            )
-            csv_writer.writerow(
-                [
-                    scene,
-                    song,
-                    genre,
-                    listener,
-                    str(score),
-                    str(haaqi_left),
-                    str(haaqi_right),
-                ]
-            )
-
-
 def set_scene_seed(scene: str):
     """Set a seed that is unique for the given song
     based on the last 8 characters of the 'md5'
@@ -196,14 +121,16 @@ def evaluate_scene(
     aq_score_l = compute_haaqi(
         processed_signal[0, :],
         ref_signal[0, :],
-        listener.audiogram_left,
         sample_rate,
+        sample_rate,
+        listener.audiogram_left,
     )
     aq_score_r = compute_haaqi(
         processed_signal[1, :],
         ref_signal[1, :],
-        listener.audiogram_right,
         sample_rate,
+        sample_rate,
+        listener.audiogram_right,
     )
     return aq_score_l, aq_score_r
 
@@ -226,10 +153,26 @@ def run_calculate_audio_quality(config: DictConfig) -> None:
     enhanced_folder = Path("enhanced_signals")
     logger.info(f"Evaluating from {enhanced_folder} directory")
 
+    scores_headers = [
+        "scene",
+        "song",
+        "genre",
+        "listener",
+        "score",
+        "haaqi_left",
+        "haaqi_right",
+    ]
+
+    results_file_name = "scores.csv"
+    if config.evaluate.batch_size > 1:
+        results_file_name = (
+            f"scores_{config.evaluate.batch + 1}-{config.evaluate.batch_size}.csv"
+        )
+
     results_file = ResultsFile(
-        f"scores_{config.evaluate.batch}-{config.evaluate.batch_size}.csv"
+        file_name=results_file_name,
+        header_columns=scores_headers,
     )
-    results_file.write_header()
 
     # Initialize acoustic scene model
     car_scene_acoustic = CarSceneAcoustics(
@@ -262,15 +205,14 @@ 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.id}_{current_scene['song']}"
-        enhanced_song_path = (
-            enhanced_folder / f"{listener.id}" / f"{enhanced_song_id}.wav"
+        # Read WAV enhanced signal using scipy.io.wavfile
+        enhanced_signal, enhanced_sample_rate = read_flac_signal(
+            enhanced_folder
+            / f"{listener.id}"
+            / f"{scene_id}_{listener.id}_{current_scene['song']}.flac"
         )
 
-        # Read WAV enhanced signal using scipy.io.wavfile
-        enhanced_sample_rate, enhanced_signal = wavfile.read(enhanced_song_path)
-        enhanced_signal = enhanced_signal / 32768.0
-        assert enhanced_sample_rate == config.sample_rate
+        assert enhanced_sample_rate == config.enhanced_sample_rate
 
         # Evaluate scene
         aq_score_l, aq_score_r = evaluate_scene(
@@ -288,13 +230,15 @@ def run_calculate_audio_quality(config: DictConfig) -> None:
         # Compute combined score and save
         score = np.mean([aq_score_r, aq_score_l])
         results_file.add_result(
-            scene_id,
-            current_scene["song"],
-            current_scene["song_path"].split("/")[-2],
-            listener.id,
-            score=float(score),
-            haaqi_left=aq_score_l,
-            haaqi_right=aq_score_r,
+            {
+                "scene": scene_id,
+                "song": current_scene["song"],
+                "genre": current_scene["song_path"].split("/")[-2],
+                "listener": listener.id,
+                "score": float(score),
+                "haaqi_left": aq_score_l,
+                "haaqi_right": aq_score_r,
+            }
         )
 
 
diff --git a/recipes/cad_icassp_2024/__init__.py b/recipes/cad_icassp_2024/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad_icassp_2024/baseline/README.md b/recipes/cad_icassp_2024/baseline/README.md
new file mode 100644
index 000000000..c5cd04c58
--- /dev/null
+++ b/recipes/cad_icassp_2024/baseline/README.md
@@ -0,0 +1,166 @@
+# The ICASSP 2024 Cadenza Challenge (CAD_ICASSP_2024)
+
+Cadenza challenge code for the ICASSP 2024 Cadenza Challenge.
+
+For more information please visit the [challenge website](https://cadenzachallenge.org/docs/icassp_2024/intro).
+
+## 1. Data structure
+
+The ICASSP 2024 Cadenza Challenge dataset is based on the MUSDB18-HQ dataset.
+To download the data, please visit [Download data and software](https://cadenzachallenge.org/docs/icassp_2024/take_part/download)
+webpage.
+
+The data is split into four packages: `cad_icassp_2024_core.v1.1.tgz`,
+`cad_icassp_2024_train.v1.0.tgz`, and `cad_icassp_2024_medleydb.tgz`.
+
+Unpack packages under the same root directory using
+
+```bash
+tar -xvzf <PACKAGE_NAME>
+```
+
+### 1.1 Necessary data
+
+* **Core** contains the metadata and HRTFs signals to generate the ICASSP 2024 dataset.
+
+```text
+cadenza_data
+├───audio
+|   └───hrtf (336 kB)
+|       |  BTE_fr-VP_E1-n22.5.wav
+|       |  BTE_fr-VP_E1-n30.0.wav
+|       |  ...
+|
+└───metadata  (328 kB)
+    |  gains.json
+    |  head_positions.json
+    |  listeners.train.json
+    |  listeners.valid.json
+    |  musdb18.train.json
+    |  scene_listeners.train.json
+    |  scenes.train.json
+    |  ...
+
+```
+
+* **train** contains the MUSDB18 train split signals to generate the ICASSP 2024 dataset.
+
+```text
+cadenza_data
+└───audio
+    └───music (22 GB)
+        └─── Train
+             ├─── A Classic Education - NightOwl
+             |    | Bass.wav
+             |    | Drums.wav
+             |    | Other.wav
+             |    | Vocals.wav
+             |    | Mixture.wav
+             ├─── ...
+```
+
+### 1.2 Additional optional data
+
+If you need additional music data for training your model, please restrict to the use of
+[MedleyDB](https://medleydb.weebly.com/) [[5](#references)] [[6](#references)],
+[BACH10](https://labsites.rochester.edu/air/resource.html) [7] and [FMA-small](https://github.com/mdeff/fma) [7].
+
+**Keeping the augmentation data restricted to these datasets will ensure that the evaluation is fair for all participants**.
+
+* **MedleyDB** contains both MedleyDB versions 1 [[5](#references)] and 2 [[6](#references)] datasets.
+
+```text
+cadenza_data
+└───audio
+    └───MedleyDB (164 GB)
+        ├───Audio
+        └───Metadata
+```
+
+* **BACH10** [[7](#references)].
+
+Bach10 dataset can be downloaded from
+[Download data and software](https://cadenzachallenge.org/docs/icassp_2024/take_part/download#b1-download-the-packages)
+on the Challenge website.
+
+* **FMA Small** contains the FMA small subset of the FMA dataset [[8](references)].
+
+FMA small dataset can be downloaded from
+[Download data and software](https://cadenzachallenge.org/docs/icassp_2024/take_part/download#b1-download-the-packages)
+on the Challenge website.
+
+Tracks from the FMA small dataset are not included in the MUSDB18-HQ.
+This dataset does not provide independent stems but only the full mix.
+However, it can be used to train an unsupervised model to better initialise a supervised model.
+
+## 2. Baseline
+
+In the `baseline/` folder, we provide code for running the baseline enhancement system and performing the objective evaluation.
+Note that we use [hydra](https://hydra.cc/docs/intro/) for config handling.
+
+### 2.1 Enhancement
+
+The baseline enhance takes an out-of-the-box source separation model and estimates
+the VDBO (vocals, drums, bass and others) stems for each song-listener pair.
+
+For each estimated stem, the baseline applies the gains and remix the signal.
+A simple NAL-R [2] fitting amplification is applied to the final remix
+
+The baseline offers 2 source separation options:
+
+1. [Hybrid Demucs](https://github.com/facebookresearch/demucs) [[1](#references)]  distributed on [TorchAudio](https://pytorch.org/audio/main/tutorials/hybrid_demucs_tutorial.html)
+2. [Open-Unmix](https://github.com/sigsep/open-unmix-pytorch) [[2](#references)]  distributed through Pytorch hub.
+
+To run the baseline enhancement system first, make sure that `paths.root` in `config.yaml` points to
+where you have installed the Cadenza data.
+You can also define your own `path.exp_folder` to store enhanced
+signals and evaluated results.
+
+Then run:
+
+```bash
+python enhance.py
+```
+
+Alternatively, you can provide the root variable on the command line, e.g.,
+
+```bash
+python enhance.py path.root=/Volumes/data/cadenza_data
+```
+
+To get a full list of the parameters, run:
+
+```bash
+python enhance.py --help
+```
+
+The folder `enhanced_signals` will appear in the `exp` folder.
+
+### 2.2 Evaluation
+
+The `evaluate.py` simply takes the signals stored in `enhanced_signals` and computes the HAAQI [[3](#references)] scores
+
+To run the evaluation stage, make sure that `path.root` is set in the `config.yaml` file and then run
+
+```bash
+python evaluate.py
+```
+
+A csv file containing the left and right channels HAAQI scores and the mean of both will be generated in the `path.exp_folder`.
+
+To check the HAAQI code, see [here](https://github.com/claritychallenge/clarity/blob/main/clarity/evaluator/haaqi/haaqi.py).
+
+Please note: you will not get identical HAAQI scores for the same signals if the random seed is not defined
+(in the given recipe, the random seed for each signal is set as the last eight digits of the song md5).
+As there are random noises generated within HAAQI, but the differences should be sufficiently small.
+
+## References
+
+* [1] Défossez, A. "Hybrid Spectrogram and Waveform Source Separation". Proceedings of the ISMIR 2021 Workshop on Music Source Separation. [doi:10.48550/arXiv.2111.03600](https://arxiv.org/abs/2111.03600)
+* [2] Stöter, F. R., Liutkus, A., Ito, N., Nakashika, T., Ono, N., & Mitsufuji, Y. (2019). "Open-Unmix: A Reference Implementation for Music Source Separation". Journal of Open Source Software, 4(41), 1667. [doi:10.21105/joss.01667](https://doi.org/10.21105/joss.01667)
+* [3] Byrne, Denis, and Harvey Dillon. "The National Acoustic Laboratories'(NAL) new procedure for selecting the gain and frequency response of a hearing aid." Ear and hearing 7.4 (1986): 257-265. [doi:10.1097/00003446-198608000-00007](https://doi.org/10.1097/00003446-198608000-00007)
+* [4] Kates J M, Arehart K H. "The Hearing-Aid Audio Quality Index (HAAQI)". IEEE/ACM transactions on audio, speech, and language processing, 24(2), 354–365. [doi:10.1109/TASLP.2015.2507858](https://doi.org/10.1109%2FTASLP.2015.2507858)
+* [5] R. Bittner, J. Salamon, M. Tierney, M. Mauch, C. Cannam and J. P. Bello, "MedleyDB: A Multitrack Dataset for Annotation-Intensive MIR Research", in 15th International Society for Music Information Retrieval Conference, Taipei, Taiwan, Oct. 2014. [pdf](https://archives.ismir.net/ismir2014/paper/000322.pdf)
+* [6] Rachel M. Bittner, Julia Wilkins, Hanna Yip and Juan P. Bello, "MedleyDB 2.0: New Data and a System for Sustainable Data Collection" Late breaking/demo extended abstract, 17th International Society for Music Information Retrieval (ISMIR) conference, August 2016. [pdf](https://wp.nyu.edu/ismir2016/wp-content/uploads/sites/2294/2016/08/bittner-medleydb.pdf)
+* [7] Zhiyao Duan, Bryan Pardo and Changshui Zhang, "Multiple fundamental frequency estimation by modeling spectral peaks and non-peak regions," IEEE Trans. Audio Speech  Language Process., vol. 18, no. 8, pp. 2121-2133, 2010. [doi:10.1109/TASL.2010.2042119](https://doi.org/10.1109/TASL.2010.2042119)
+* [8] Defferrard, M., Benzi, K., Vandergheynst, P., & Bresson, X. (2016). "FMA: A dataset for music analysis". arXiv preprint arXiv:1612.01840. [doi:10.48550/arXiv.1612.01840](https://doi.org/10.48550/arXiv.1612.01840)
diff --git a/recipes/cad_icassp_2024/baseline/config.yaml b/recipes/cad_icassp_2024/baseline/config.yaml
new file mode 100644
index 000000000..6038f16a0
--- /dev/null
+++ b/recipes/cad_icassp_2024/baseline/config.yaml
@@ -0,0 +1,44 @@
+path:
+  root: ???
+  metadata_dir: ${path.root}/metadata
+  music_dir: ${path.root}/audio/at_mic_music
+  gains_file: ${path.metadata_dir}/gains.json
+  head_positions_file: ${path.metadata_dir}/head_positions.json
+  listeners_file: ${path.metadata_dir}/listeners.train.json
+  music_file: ${path.metadata_dir}/at_mic_music.train.json
+  scenes_file: ${path.metadata_dir}/scenes.train.json
+  scene_listeners_file: ${path.metadata_dir}/scene_listeners.train.json
+  exp_folder: ./exp # folder to store enhanced signals and final results
+
+sample_rate: 44100
+remix_sample_rate: 32000
+HAAQI_sample_rate: 24000
+
+nalr:
+  nfir: 220
+  sample_rate: ${sample_rate}
+
+apply_compressor: False
+compressor:
+  threshold: 0.35
+  attenuation: 0.1
+  attack: 50
+  release: 1000
+  rms_buffer_size: 0.064
+
+soft_clip: False
+
+separator:
+  model: demucs   # demucs or openunmix
+  device: ~
+
+evaluate:
+  set_random_seed: True
+  small_test: False
+  batch_size: 1  # Number of batches
+  batch: 0       # Batch number to evaluate
+
+# hydra config
+hydra:
+  run:
+    dir: ${path.exp_folder}
\ No newline at end of file
diff --git a/recipes/cad_icassp_2024/baseline/enhance.py b/recipes/cad_icassp_2024/baseline/enhance.py
new file mode 100644
index 000000000..569389afc
--- /dev/null
+++ b/recipes/cad_icassp_2024/baseline/enhance.py
@@ -0,0 +1,313 @@
+""" Run the dummy enhancement. """
+from __future__ import annotations
+
+import json
+import logging
+from pathlib import Path
+
+# pylint: disable=import-error
+import hydra
+import numpy as np
+import torch
+from numpy import ndarray
+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 Listener
+from clarity.utils.file_io import read_signal
+from clarity.utils.flac_encoder import FlacEncoder
+from clarity.utils.signal_processing import (
+    clip_signal,
+    denormalize_signals,
+    normalize_signal,
+    resample,
+    to_16bit,
+)
+from clarity.utils.source_separation_support import get_device, separate_sources
+from recipes.cad_icassp_2024.baseline.evaluate import (
+    apply_gains,
+    apply_ha,
+    make_scene_listener_list,
+    remix_stems,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def save_flac_signal(
+    signal: np.ndarray,
+    filename: Path,
+    signal_sample_rate,
+    output_sample_rate,
+    do_clip_signal: bool = False,
+    do_soft_clip: bool = False,
+    do_scale_signal: bool = False,
+) -> None:
+    """
+    Function to save output signals.
+
+    - The output signal will be resample to ``output_sample_rate``
+    - The output signal will be clipped to [-1, 1] if ``do_clip_signal`` is True
+        and use soft clipped if ``do_soft_clip`` is True. Note that if
+        ``do_clip_signal`` is False, ``do_soft_clip`` will be ignored.
+        Note that if ``do_clip_signal`` is True, ``do_scale_signal`` will be ignored.
+    - The output signal will be scaled to [-1, 1] if ``do_scale_signal`` is True.
+        If signal is scale, the scale factor will be saved in a TXT file.
+        Note that if ``do_clip_signal`` is True, ``do_scale_signal`` will be ignored.
+    - The output signal will be saved as a FLAC file.
+
+    Args:
+        signal (np.ndarray) : Signal to save
+        filename (Path) : Path to save signal
+        signal_sample_rate (int) : Sample rate of the input signal
+        output_sample_rate (int) : Sample rate of the output signal
+        do_clip_signal (bool) : Whether to clip signal
+        do_soft_clip (bool) : Whether to apply soft clipping
+        do_scale_signal (bool) : Whether to scale signal
+    """
+    # Resample signal to expected output sample rate
+    if signal_sample_rate != output_sample_rate:
+        signal = resample(signal, signal_sample_rate, output_sample_rate)
+
+    if do_scale_signal:
+        # Scale stem signal
+        max_value = np.max(np.abs(signal))
+        signal = signal / max_value
+
+        # Save scale factor
+        with open(filename.with_suffix(".txt"), "w", encoding="utf-8") as file:
+            file.write(f"{max_value}")
+
+    elif do_clip_signal:
+        # Clip the signal
+        signal, n_clipped = clip_signal(signal, do_soft_clip)
+        if n_clipped > 0:
+            logger.warning(f"Writing {filename}: {n_clipped} samples clipped")
+
+    # Convert signal to 16-bit integer
+    signal = to_16bit(signal)
+
+    # Create flac encoder object to compress and save the signal
+    FlacEncoder().encode(signal, output_sample_rate, filename)
+
+
+# pylint: disable=unused-argument
+def decompose_signal(
+    model: torch.nn.Module,
+    model_sample_rate: int,
+    signal: ndarray,
+    signal_sample_rate: int,
+    device: torch.device,
+    sources_list: list[str],
+    listener: Listener,
+    normalise: bool = True,
+) -> dict[str, ndarray]:
+    """
+    Decompose signal into 8 stems.
+
+    The listener is ignored by the baseline system as it
+     is not performing personalised decomposition.
+    Instead, it performs a standard music decomposition using a pre-trained
+     model trained on the MUSDB18 dataset.
+
+    Args:
+        model (torch.nn.Module): Torch model.
+        model_sample_rate (int): Sample rate of the model.
+        signal (ndarray): Signal to be decomposed.
+        signal_sample_rate (int): Sample frequency.
+        device (torch.device): Torch device to use for processing.
+        sources_list (list): List of strings used to index dictionary.
+        listener (Listener).
+        normalise (bool): Whether to normalise the signal.
+
+     Returns:
+         Dictionary: Indexed by sources with the associated model as values.
+    """
+    if signal.shape[0] > signal.shape[1]:
+        signal = signal.T
+
+    if signal_sample_rate != model_sample_rate:
+        signal = resample(signal, signal_sample_rate, model_sample_rate)
+
+    if normalise:
+        signal, ref = normalize_signal(signal)
+
+    sources = separate_sources(
+        model,
+        torch.from_numpy(signal.astype(np.float32)),
+        model_sample_rate,
+        device=device,
+    )
+
+    # only one element in the batch
+    sources = sources[0]
+    if normalise:
+        sources = denormalize_signals(sources, ref)
+
+    sources = np.transpose(sources, (0, 2, 1))
+    return dict(zip(sources_list, sources))
+
+
+def process_remix_for_listener(
+    signal: ndarray,
+    enhancer: NALR,
+    compressor: Compressor,
+    listener: Listener,
+    apply_compressor: bool = False,
+) -> ndarray:
+    """Process the stems from sources.
+
+    Args:
+        stems (dict) : Dictionary of stems
+        sample_rate (float) : Sample rate of the signal
+        enhancer (NALR) : NAL-R prescription hearing aid
+        compressor (Compressor) : Compressor
+        listener: Listener object
+        apply_compressor (bool) : Whether to apply the compressor
+    Returns:
+        ndarray: Processed signal.
+    """
+    left_output = apply_ha(
+        enhancer, compressor, signal[:, 0], listener.audiogram_left, apply_compressor
+    )
+    right_output = apply_ha(
+        enhancer, compressor, signal[:, 1], listener.audiogram_right, apply_compressor
+    )
+
+    return np.stack([left_output, right_output], axis=1)
+
+
+@hydra.main(config_path="", config_name="config")
+def enhance(config: DictConfig) -> None:
+    """
+    Run the music enhancement.
+    The system decomposes the music into vocal, drums, bass, and other stems.
+    Then, the NAL-R prescription procedure is applied to each stem.
+    Args:
+        config (dict): Dictionary of configuration options for enhancing music.
+
+    Returns 8 stems for each song:
+        - left channel vocal, drums, bass, and other stems
+        - right channel vocal, drums, bass, and other stems
+    """
+
+    # Set the output directory where processed signals will be saved
+    enhanced_folder = Path("enhanced_signals")
+    enhanced_folder.mkdir(parents=True, exist_ok=True)
+
+    # Loading pretrained source separation model
+    if config.separator.model == "demucs":
+        separation_model = HDEMUCS_HIGH_MUSDB.get_model()
+        model_sample_rate = HDEMUCS_HIGH_MUSDB.sample_rate
+        sources_order = separation_model.sources
+        normalise = True
+    elif config.separator.model == "openunmix":
+        separation_model = torch.hub.load("sigsep/open-unmix-pytorch", "umxhq", niter=0)
+        model_sample_rate = separation_model.sample_rate
+        sources_order = ["vocals", "drums", "bass", "other"]
+        normalise = False
+    else:
+        raise ValueError(f"Separator model {config.separator.model} not supported.")
+
+    device, _ = get_device(config.separator.device)
+    separation_model.to(device)
+
+    # Load listener audiograms and songs
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
+
+    #
+    with Path(config.path.gains_file).open("r", encoding="utf-8") as file:
+        gains = json.load(file)
+
+    with Path(config.path.scenes_file).open("r", encoding="utf-8") as file:
+        scenes = json.load(file)
+
+    with Path(config.path.scene_listeners_file).open("r", encoding="utf-8") as file:
+        scenes_listeners = json.load(file)
+
+    with Path(config.path.music_file).open("r", encoding="utf-8") as file:
+        songs = json.load(file)
+
+    enhancer = NALR(**config.nalr)
+    compressor = Compressor(**config.compressor)
+
+    # Select a batch to process
+    scene_listener_pairs = make_scene_listener_list(
+        scenes_listeners, config.evaluate.small_test
+    )
+
+    scene_listener_pairs = scene_listener_pairs[
+        config.evaluate.batch :: config.evaluate.batch_size
+    ]
+
+    # Decompose each song into left and right vocal, drums, bass, and other stems
+    # and process each stem for the listener
+    previous_song = ""
+    num_scenes = len(scene_listener_pairs)
+    for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
+        scene_id, listener_id = scene_listener_pair
+
+        scene = scenes[scene_id]
+        song_name = f"{scene['music']}-{scene['head_loudspeaker_positions']}"
+
+        logger.info(
+            f"[{idx:03d}/{num_scenes:03d}] "
+            f"Processing {scene_id}: {song_name} for listener {listener_id}"
+        )
+        # Get the listener's audiogram
+        listener = listener_dict[listener_id]
+
+        # Read the mixture signal
+        # Convert to 32-bit floating point and transpose
+        # from [samples, channels] to [channels, samples]
+        if song_name != previous_song:
+            mixture_signal = read_signal(
+                filename=Path(config.path.music_dir)
+                / songs[song_name]["Path"]
+                / "mixture.wav",
+                sample_rate=config.sample_rate,
+                allow_resample=True,
+            )
+
+            stems: dict[str, ndarray] = decompose_signal(
+                model=separation_model,
+                model_sample_rate=model_sample_rate,
+                signal=mixture_signal,
+                signal_sample_rate=config.sample_rate,
+                device=device,
+                sources_list=sources_order,
+                listener=listener,
+                normalise=normalise,
+            )
+
+            stems = apply_gains(stems, config.sample_rate, gains[scene["gain"]])
+            enhanced_signal = remix_stems(stems, mixture_signal, model_sample_rate)
+
+        enhanced_signal = process_remix_for_listener(
+            signal=enhanced_signal,
+            enhancer=enhancer,
+            compressor=compressor,
+            listener=listener,
+            apply_compressor=config.apply_compressor,
+        )
+
+        filename = Path(enhanced_folder) / f"{scene_id}_{listener.id}_remix.flac"
+
+        filename.parent.mkdir(parents=True, exist_ok=True)
+        save_flac_signal(
+            signal=enhanced_signal,
+            filename=filename,
+            signal_sample_rate=config.sample_rate,
+            output_sample_rate=config.remix_sample_rate,
+            do_clip_signal=True,
+            do_soft_clip=config.soft_clip,
+        )
+
+    logger.info("Done!")
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    enhance()
diff --git a/recipes/cad_icassp_2024/baseline/evaluate.py b/recipes/cad_icassp_2024/baseline/evaluate.py
new file mode 100644
index 000000000..75b4e5787
--- /dev/null
+++ b/recipes/cad_icassp_2024/baseline/evaluate.py
@@ -0,0 +1,341 @@
+"""Evaluate the enhanced signals using the HAAQI metric."""
+from __future__ import annotations
+
+# pylint: disable=import-error
+import hashlib
+import json
+import logging
+import warnings
+from pathlib import Path
+
+import hydra
+import numpy as np
+import pyloudnorm as pyln
+from numpy import ndarray
+from omegaconf import DictConfig
+
+from clarity.enhancer.compressor import Compressor
+from clarity.enhancer.nalr import NALR
+from clarity.evaluator.haaqi import compute_haaqi
+from clarity.utils.audiogram import Audiogram, Listener
+from clarity.utils.file_io import read_signal
+from clarity.utils.flac_encoder import read_flac_signal
+from clarity.utils.results_support import ResultsFile
+from clarity.utils.signal_processing import compute_rms, resample
+
+logger = logging.getLogger(__name__)
+
+
+def apply_ha(
+    enhancer: NALR,
+    compressor: Compressor | None,
+    signal: ndarray,
+    audiogram: Audiogram,
+    apply_compressor: bool = False,
+) -> np.ndarray:
+    """
+    Apply NAL-R prescription hearing aid to a signal.
+
+    Args:
+        enhancer (NALR): A NALR object that enhances the signal.
+        compressor (Compressor | None): A Compressor object that compresses the signal.
+        signal (ndarray): An ndarray representing the audio signal.
+        audiogram (Audiogram): An Audiogram object representing the listener's
+            audiogram.
+        apply_compressor (bool): Whether to apply the compressor.
+
+    Returns:
+        An ndarray representing the processed signal.
+    """
+    nalr_fir, _ = enhancer.build(audiogram)
+    proc_signal = enhancer.apply(nalr_fir, signal)
+    if apply_compressor:
+        if compressor is None:
+            raise ValueError("Compressor must be provided to apply compressor.")
+
+        proc_signal, _, _ = compressor.process(proc_signal)
+    return proc_signal
+
+
+def apply_gains(stems: dict, sample_rate: float, gains: dict) -> dict:
+    """Apply gain to the signal by using LUFS.
+
+    Args:
+        stems (dict): Dictionary of stems.
+        sample_rate (float): Sample rate of the signal.
+        gains (dict): Dictionary of gains.
+
+    Returns:
+        dict: Dictionary of stems with applied gains.
+    """
+    meter = pyln.Meter(int(sample_rate))
+    stems_gain = {}
+    for stem_str, stem_signal in stems.items():
+        if stem_signal.shape[0] < stem_signal.shape[1]:
+            stem_signal = stem_signal.T
+
+        stem_lufs = meter.integrated_loudness(stem_signal)
+        if stem_lufs == -np.inf:
+            stem_lufs = -80
+
+        gain = stem_lufs + gains[stem_str]
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore", message="Possible clipped samples in output"
+            )
+            stems_gain[stem_str] = pyln.normalize.loudness(stem_signal, stem_lufs, gain)
+    return stems_gain
+
+
+def level_normalisation(
+    signal: ndarray, reference_signal: ndarray, sample_rate: float
+) -> ndarray:
+    """Normalise the signal to the LUFS level of the reference signal.
+
+    Args:
+        signal (ndarray): Signal to normalise.
+        reference_signal (ndarray): Reference signal.
+        sample_rate (float): Sample rate of the signal.
+
+    Returns:
+        ndarray: Normalised signal.
+    """
+    meter = pyln.Meter(int(sample_rate))
+    signal_lufs = meter.integrated_loudness(signal)
+    reference_signal_lufs = meter.integrated_loudness(reference_signal)
+
+    if signal_lufs == -np.inf:
+        signal_lufs = -80
+
+    if reference_signal_lufs == -np.inf:
+        reference_signal_lufs = -80
+
+    gain = reference_signal_lufs - signal_lufs
+
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", message="Possible clipped samples in output")
+        normed_signal = pyln.normalize.loudness(signal, signal_lufs, signal_lufs + gain)
+    return normed_signal
+
+
+def remix_stems(stems: dict, reference_signal, sample_rate: float) -> ndarray:
+    """Remix the stems into a stereo signal.
+
+    The remixing is done by summing the stems.
+    Then, the signal is normalised to the LUFS level of the reference signal.
+
+    Args:
+        stems (dict): Dictionary of stems.
+        reference_signal (ndarray): Reference signal.
+        sample_rate (float): Sample rate of the signal.
+
+    Returns:
+        ndarray: Stereo signal.
+    """
+    remix_signal = np.zeros(stems["vocals"].shape)
+    for _, stem_signal in stems.items():
+        remix_signal += stem_signal
+    return level_normalisation(remix_signal, reference_signal, sample_rate)
+
+
+def make_scene_listener_list(scenes_listeners: dict, small_test: bool = False) -> list:
+    """Make the list of scene-listener pairing to process
+
+    Args:
+        scenes_listeners (dict): Dictionary of scenes and listeners.
+        small_test (bool): Whether to use a small test set.
+
+    Returns:
+        list: List of scene-listener pairings.
+
+    """
+    scene_listener_pairs = [
+        (scene, listener)
+        for scene in scenes_listeners
+        for listener in scenes_listeners[scene]
+    ]
+
+    # Can define a standard 'small_test' with just 1/15 of the data
+    if small_test:
+        scene_listener_pairs = scene_listener_pairs[::15]
+
+    return scene_listener_pairs
+
+
+def set_song_seed(song: str) -> None:
+    """Set a seed that is unique for the given song"""
+    song_encoded = hashlib.md5(song.encode("utf-8")).hexdigest()
+    song_md5 = int(song_encoded, 16) % (10**8)
+    np.random.seed(song_md5)
+
+
+def load_reference_stems(music_dir: str | Path) -> tuple[dict[str, ndarray], ndarray]:
+    """Load the reference stems for a given scene.
+
+    Args:
+        scene (dict): The scene to load the stems for.
+        music_dir (str | Path): The path to the music directory.
+    Returns:
+        reference_stems (dict): A dictionary of reference stems.
+        original_mixture (ndarray): The original mixture.
+    """
+    reference_stems = {}
+    for instrument in ["drums", "bass", "other", "vocals"]:
+        stem = read_signal(Path(music_dir) / f"{instrument}.wav")
+        reference_stems[instrument] = stem
+
+    return reference_stems, read_signal(Path(music_dir) / "mixture.wav")
+
+
+@hydra.main(config_path="", config_name="config")
+def run_calculate_aq(config: DictConfig) -> None:
+    """Evaluate the enhanced signals using the HAAQI metric."""
+
+    enhanced_folder = Path("enhanced_signals")
+    logger.info(f"Evaluating from {enhanced_folder} directory")
+
+    # Load listener audiograms and songs
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
+
+    with Path(config.path.gains_file).open("r", encoding="utf-8") as file:
+        gains = json.load(file)
+
+    with Path(config.path.scenes_file).open("r", encoding="utf-8") as file:
+        scenes = json.load(file)
+
+    with Path(config.path.scene_listeners_file).open("r", encoding="utf-8") as file:
+        scenes_listeners = json.load(file)
+
+    with Path(config.path.music_file).open("r", encoding="utf-8") as file:
+        songs = json.load(file)
+
+    enhancer = NALR(**config.nalr)
+
+    scores_headers = [
+        "scene",
+        "song",
+        "listener",
+        "left_score",
+        "right_score",
+        "score",
+    ]
+    if config.evaluate.batch_size == 1:
+        results_file = ResultsFile(
+            "scores.csv",
+            header_columns=scores_headers,
+        )
+    else:
+        results_file = ResultsFile(
+            f"scores_{config.evaluate.batch + 1}-{config.evaluate.batch_size}.csv",
+            header_columns=scores_headers,
+        )
+    # results_file.write_header()
+
+    scene_listener_pairs = make_scene_listener_list(
+        scenes_listeners, config.evaluate.small_test
+    )
+    scene_listener_pairs = scene_listener_pairs[
+        config.evaluate.batch :: config.evaluate.batch_size
+    ]
+    num_scenes = len(scene_listener_pairs)
+    for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
+        scene_id, listener_id = scene_listener_pair
+
+        scene = scenes[scene_id]
+        song_name = f"{scene['music']}-{scene['head_loudspeaker_positions']}"
+
+        logger.info(
+            f"[{idx:03d}/{num_scenes:03d}] "
+            f"Evaluating {scene_id} for listener {listener_id}"
+        )
+
+        # Load reference signals
+        reference_stems, original_mixture = load_reference_stems(
+            Path(config.path.music_dir) / songs[song_name]["Path"]
+        )
+        reference_stems = apply_gains(
+            reference_stems, config.sample_rate, gains[scene["gain"]]
+        )
+        reference_mixture = remix_stems(
+            reference_stems, original_mixture, config.sample_rate
+        )
+
+        # Set the random seed for the scene
+        if config.evaluate.set_random_seed:
+            set_song_seed(scene_id)
+
+        # Evaluate listener
+        listener = listener_dict[listener_id]
+
+        # Load enhanced signal
+        enhanced_signal, _ = read_flac_signal(
+            Path(enhanced_folder) / f"{scene_id}_{listener.id}_remix.flac"
+        )
+
+        # Apply hearing aid to reference signals
+        left_reference = apply_ha(
+            enhancer=enhancer,
+            compressor=None,
+            signal=reference_mixture[:, 0],
+            audiogram=listener.audiogram_left,
+            apply_compressor=False,
+        )
+        right_reference = apply_ha(
+            enhancer=enhancer,
+            compressor=None,
+            signal=reference_mixture[:, 1],
+            audiogram=listener.audiogram_right,
+            apply_compressor=False,
+        )
+
+        # Compute the scores
+        left_score = compute_haaqi(
+            processed_signal=resample(
+                enhanced_signal[:, 0],
+                config.remix_sample_rate,
+                config.HAAQI_sample_rate,
+            ),
+            reference_signal=resample(
+                left_reference, config.sample_rate, config.HAAQI_sample_rate
+            ),
+            processed_sample_rate=config.HAAQI_sample_rate,
+            reference_sample_rate=config.HAAQI_sample_rate,
+            audiogram=listener.audiogram_left,
+            equalisation=2,
+            level1=65 - 20 * np.log10(compute_rms(reference_mixture[:, 0])),
+        )
+
+        right_score = compute_haaqi(
+            processed_signal=resample(
+                enhanced_signal[:, 1],
+                config.remix_sample_rate,
+                config.HAAQI_sample_rate,
+            ),
+            reference_signal=resample(
+                right_reference, config.sample_rate, config.HAAQI_sample_rate
+            ),
+            processed_sample_rate=config.HAAQI_sample_rate,
+            reference_sample_rate=config.HAAQI_sample_rate,
+            audiogram=listener.audiogram_right,
+            equalisation=2,
+            level1=65 - 20 * np.log10(compute_rms(reference_mixture[:, 1])),
+        )
+
+        # Save scores
+        results_file.add_result(
+            {
+                "scene": scene_id,
+                "song": song_name,
+                "listener": listener.id,
+                "left_score": left_score,
+                "right_score": right_score,
+                "score": float(np.mean([left_score, right_score])),
+            }
+        )
+
+    logger.info("Done!")
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    run_calculate_aq()
diff --git a/recipes/cad_icassp_2024/baseline/merge_batches_results.py b/recipes/cad_icassp_2024/baseline/merge_batches_results.py
new file mode 100644
index 000000000..ff29762da
--- /dev/null
+++ b/recipes/cad_icassp_2024/baseline/merge_batches_results.py
@@ -0,0 +1,31 @@
+"""Join batches scores into a single file."""
+
+# pylint: disable=import-error
+import hydra
+import pandas as pd
+from omegaconf import DictConfig
+
+
+@hydra.main(config_path="", config_name="config")
+def join_batches(config: DictConfig) -> None:
+    """
+    Join batches scores into a single file.
+
+    Args:
+        config (DictConfig): Dictionary of configuration options.
+            The `.evaluate.batch_size` is extracted to determine how many
+            batches there are to combine.
+
+    """
+    batches_results = []
+    for batch in range(config.evaluate.batch_size):
+        batches_results.append(
+            pd.read_csv(f"scores_{batch + 1}-{config.evaluate.batch_size}.csv")
+        )
+    df_res = pd.concat(batches_results, ignore_index=True)
+    df_res.to_csv("scores.csv", index=False)
+
+
+# pylint: disable=no-value-for-parameter
+if __name__ == "__main__":
+    join_batches()
diff --git a/recipes/cad_icassp_2024/generate_dataset/README.md b/recipes/cad_icassp_2024/generate_dataset/README.md
new file mode 100644
index 000000000..2d4236d0f
--- /dev/null
+++ b/recipes/cad_icassp_2024/generate_dataset/README.md
@@ -0,0 +1,67 @@
+# Generate music dataset for the ICASSP 2024 Cadenza Challenge
+
+The ICASSP 2024 Cadenza Challenge music dataset is based on the MUSDB18-HQ dataset.
+
+Steps:
+
+1. Download `cadenza_cad1_task1_core_musdb18hq.tar.gz` and `cadenza_cad1_task1_core_metadata.tar.gz`
+packages from the [Cadenza Challenge website](https://cadenza-challenge.github.io/).
+2. Unpack packages under the same root directory.
+3. Run the script
+
+## Unpack the data
+
+To unpack the data run:
+
+```bash
+tar -xvzf <PACKAGE_NAME>
+```
+
+## Generate the dataset
+
+To generate the dataset, set the `path.root` parameter in the `generate_dataset/config.yaml`
+to where you unpacked the data. Then run:
+
+```bash
+python generates_at_mic_musdb18.py
+```
+
+or, run the script with the `path.root` parameter:
+
+```bash
+python generates_at_mic_musdb18.py path.root <PATH_TO_UNPACKED_DATA>
+```
+
+The script will generate the dataset in the `path.root` directory.
+
+The script should create the `at_mic_microphone` where all music samples
+picked up by the microphones (at the mic) are saved.
+
+In the next example, `A Classic Education - NightOwl-hp_0103` corresponds to the
+song `A Classic Education - NightOwl` with the `hp_0103` head position.
+
+```text
+cadenza_data
+├───audio
+|   ├───at_mic_music
+|   |   └───train (80.8 GB)
+|   |       ├───A Classic Education - NightOwl-hp_0103
+|   |       |   |  bass.wav
+|   |       |   |  drums.wav
+|   |       |   |  other.wav
+|   |       |   |  vocals.wav
+|   |       |   |  mixture.wav
+|   |       |
+|   |       ├───A Classic Education - NightOwl-hp_0138
+|   |       |   ....
+|   |
+|   ├───hrtf (336 kB)
+|   |
+|   └───music
+|       └───train (20.2 GB)
+|
+└───metadata  (328 kB)
+|  gains.json
+    |  at_mic_music.train.json
+    |  ...
+```
diff --git a/recipes/cad_icassp_2024/generate_dataset/config.yaml b/recipes/cad_icassp_2024/generate_dataset/config.yaml
new file mode 100644
index 000000000..947715efa
--- /dev/null
+++ b/recipes/cad_icassp_2024/generate_dataset/config.yaml
@@ -0,0 +1,24 @@
+path:
+  root: ???
+  metadata_dir: ${path.root}/metadata
+  music_dir: ${path.root}/audio/music # musdb18 dataset
+  hrtf_dir: ${path.root}/audio/hrtf
+  scene_file: ${path.metadata_dir}/scenes.train.json
+  music_file: ${path.metadata_dir}/musdb18.train.json
+  head_loudspeaker_positions_file: ${path.metadata_dir}/head_loudspeaker_positions.json
+  tracklist_file: ./musdb18_tracklist.csv
+  gains_file: ${path.metadata_dir}/gains.json
+  output_music_dir: ${path.root}/audio/at_mic_music # at microphone musdb18 dataset
+  output_music_file: ${path.metadata_dir}/at_mic_music.train.json
+
+sample_rate: 44100
+
+scene:
+  number_scenes_per_song: 4
+
+scene_listener:
+  number_listeners_per_scene: 2
+
+hydra:
+  run:
+    dir: .
diff --git a/recipes/cad_icassp_2024/generate_dataset/generate_at_mic_musdb18.py b/recipes/cad_icassp_2024/generate_dataset/generate_at_mic_musdb18.py
new file mode 100644
index 000000000..e98ced488
--- /dev/null
+++ b/recipes/cad_icassp_2024/generate_dataset/generate_at_mic_musdb18.py
@@ -0,0 +1,228 @@
+"""
+Script creates the dataset for the ICASSP 2024 Grand Challenge.
+
+It takes the music from the MUSDB18 dataset and applies the HRTF signals
+to simulate the music at the microphone position.
+The output is saved in the same format as the MUSDB18 dataset.
+
+The script takes as input:
+    - The metadata of the scenes.
+    - The metadata of the music.
+    - The metadata of the head positions.
+    - The HRTF signals.
+    - The music signals.
+
+The script outputs:
+    - The metadata of the music at the hearing aids microphone.
+    - The music signals at the hearing aids microphone.
+"""
+from __future__ import annotations
+
+# pylint: disable=import-error
+import json
+import logging
+import warnings
+from pathlib import Path
+
+import hydra
+import numpy as np
+import pyloudnorm as pyln
+from numpy import ndarray
+from omegaconf import DictConfig
+from scipy.signal import lfilter
+
+from clarity.utils.file_io import read_signal, write_signal
+
+logger = logging.getLogger(__name__)
+
+
+def apply_hrtf(signal: ndarray, hrtf_left: ndarray, hrtf_right) -> ndarray:
+    """Applies the Left and Right HRTF to a signal.
+
+    Args:
+        signal (ndarray): Signal.
+        hrtf_left (ndarray): Left HRTF.
+        hrtf_right (ndarray): Right HRTF.
+
+    Returns:
+        ndarray: Signal with applied HRTF.
+    """
+    output_left_ear = lfilter(hrtf_left[:, 0], 1, signal[:, 0])
+    output_right_ear = lfilter(hrtf_left[:, 1], 1, signal[:, 0])
+
+    output_left_ear += lfilter(hrtf_right[:, 0], 1, signal[:, 1])
+    output_right_ear += lfilter(hrtf_right[:, 1], 1, signal[:, 1])
+
+    return np.stack([output_left_ear, output_right_ear], axis=1)
+
+
+def load_hrtf_signals(hrtf_path: str, hp: dict) -> tuple[ndarray, ndarray]:
+    """Loads the HRTF signals for a given head position.
+
+    Args:
+        hrtf_path (str): Path to the HRTF signals.
+        hp (dict): Head position.
+
+    Returns:
+        tuple(ndarray, ndarray): Left and right HRTF signals.
+    """
+
+    hp_left_path = (
+        Path(hrtf_path) / f"{hp['mic']}-{hp['subject']}-n{abs(hp['left_angle'])}.wav"
+    )
+    hp_right_path = (
+        Path(hrtf_path) / f"{hp['mic']}-{hp['subject']}-p{abs(hp['right_angle'])}.wav"
+    )
+
+    hp_left_signal = read_signal(hp_left_path)
+    hp_right_signal = read_signal(hp_right_path)
+
+    return hp_left_signal, hp_right_signal
+
+
+def normalise_lufs_level(
+    signal: ndarray, reference_signal: ndarray, sample_rate: float
+) -> ndarray:
+    """Normalises the signal to the LUFS level of the reference signal.
+
+    Args:
+        signal (ndarray): Signal to normalise.
+        reference_signal (ndarray): Reference signal.
+        sample_rate (float): Sample rate of the signal.
+
+    Returns:
+        ndarray: Normalised signal.
+    """
+    loudness_meter = pyln.Meter(int(sample_rate))
+
+    signal_lufs = loudness_meter.integrated_loudness(signal)
+    reference_signal_lufs = loudness_meter.integrated_loudness(reference_signal)
+
+    gain = reference_signal_lufs - signal_lufs
+    return pyln.normalize.loudness(signal, signal_lufs, signal_lufs + gain)
+
+
+def find_precreated_samples(source_dir: str | Path) -> list[str]:
+    """Finds music tracks created in a previous run.
+    This avoids reprocessing them.
+
+    Args:
+        source_dir (str| Path): Source directory.
+
+    Returns:
+        list[str]: List of precreated samples.
+    """
+    if isinstance(source_dir, str):
+        source_dir = Path(source_dir)
+
+    if not source_dir.exists():
+        return []
+
+    previous_tracks = []
+    for song_path in source_dir.glob("train/*"):
+        if len(list(song_path.glob("*"))) >= 5:
+            previous_tracks.append(song_path.name)
+
+    return previous_tracks
+
+
+@hydra.main(config_path="", config_name="config")
+def run(cfg: DictConfig) -> None:
+    """Main function of the script."""
+
+    logger.info("Generating dataset for the ICASSP 2024 Grand Challenge.\n")
+    logger.info(f"Processing music for scenes: {cfg.path.scene_file}")
+    logger.info(f"Transforming music signals from: {cfg.path.music_dir}")
+    logger.info(f"and save them to {cfg.path.output_music_dir}")
+
+    # Load precraeted samples to avoid reprocessing them
+    precreated_samples = find_precreated_samples(cfg.path.output_music_dir)
+    if len(precreated_samples) > 0:
+        logger.warning(f"Found {len(precreated_samples)} precreated samples.\n")
+
+    # Load the scenes metadata
+    with open(cfg.path.scene_file, encoding="utf-8") as f:
+        scenes_metadata = json.load(f)
+
+    # Load the music metadata
+    with open(cfg.path.music_file, encoding="utf-8") as f:
+        music_metadata = json.load(f)
+        music_metadata = {m["Track Name"]: m for m in music_metadata}
+
+    # Load the head positions metadata
+    with open(cfg.path.head_loudspeaker_positions_file, encoding="utf-8") as f:
+        head_positions_metadata = json.load(f)
+
+    # From the scenes, get the samples names and parameters
+    toprocess_samples = {
+        f"{v['music']}-{v['head_loudspeaker_positions']}": {
+            "music": v["music"],
+            "head_loudspeaker_positions": v["head_loudspeaker_positions"],
+        }
+        for _, v in scenes_metadata.items()
+    }
+
+    # create output metadata content
+    out_music = {}
+    for idx, sample in enumerate(toprocess_samples.items(), 1):
+        sample_name, sample_detail = sample
+        music = music_metadata[sample_detail["music"]]
+        head_position = sample_detail["head_loudspeaker_positions"]
+
+        out_music[sample_name] = {
+            "Track Name": sample_name,
+            "Split": music["Split"],
+            "Path": f"{music['Split']}/{sample_name}",
+            "Original Track Name": music["Track Name"],
+            "Head Position": head_position,
+        }
+
+        if sample_name in precreated_samples:
+            logger.info(
+                f"[{idx}/{len(toprocess_samples)}] Skipping sample: {sample_name}"
+            )
+            continue
+
+        scene_path = Path(cfg.path.output_music_dir) / music["Split"] / sample_name
+        scene_path.mkdir(parents=True, exist_ok=True)
+
+        logger.info(f"[{idx}/{len(toprocess_samples)}] Creating sample: {sample_name}")
+
+        hrtf_left, hrtf_rigth = load_hrtf_signals(
+            cfg.path.hrtf_dir, head_positions_metadata[head_position]
+        )
+
+        for stem_name in ["mixture", "vocals", "drums", "bass", "other"]:
+            music_signal = read_signal(
+                Path(cfg.path.music_dir) / music["Path"] / f"{stem_name}.wav"
+            )
+
+            at_mic_signal = apply_hrtf(music_signal, hrtf_left, hrtf_rigth)
+
+            with warnings.catch_warnings():
+                warnings.filterwarnings(
+                    "ignore", message="Possible clipped samples in output"
+                )
+                at_mic_signal = normalise_lufs_level(
+                    at_mic_signal, music_signal, cfg.sample_rate
+                )
+
+            # Save the signal
+            save_path = scene_path / f"{stem_name}.wav"
+            write_signal(
+                save_path, at_mic_signal, cfg.sample_rate, floating_point=False
+            )
+
+        precreated_samples.append(sample_name)
+
+    # Save the metadata
+    with open(cfg.path.output_music_file, "w", encoding="utf-8") as f:
+        json.dump(out_music, f, indent=4)
+
+    logger.info(f"Saved metadata to: {cfg.path.output_music_file}")
+    logger.info("Done.")
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    run()
diff --git a/recipes/cad_icassp_2024/generate_dataset/generate_train_scenes.py b/recipes/cad_icassp_2024/generate_dataset/generate_train_scenes.py
new file mode 100644
index 000000000..2667e287c
--- /dev/null
+++ b/recipes/cad_icassp_2024/generate_dataset/generate_train_scenes.py
@@ -0,0 +1,116 @@
+"""Module to generate the scenes and scene-listeners metadata files for training"""
+from __future__ import annotations
+
+# pylint: disable=import-error
+import json
+from pathlib import Path
+
+import hydra
+import numpy as np
+from omegaconf import DictConfig
+
+from recipes.cad_icassp_2024.baseline.evaluate import set_song_seed
+
+
+def choose_samples(source: list, size: int) -> list:
+    """Choose a number of samples from a list
+    Args:
+        source: List of samples to choose from
+        size: Number of samples to choose
+    Returns:
+        List of chosen samples
+    """
+    if len(source) == 0:
+        return []
+    if len(source) <= size:
+        return source
+    return np.random.choice(source, size, replace=False)
+
+
+def generate_scenes(cfg: DictConfig) -> None:
+    """
+    Generate the scenes metadata file for training
+    Args:
+        cfg: Hydra config
+    """
+
+    with open(cfg.path.gains_file, encoding="utf-8") as f:
+        gains = list(json.load(f).keys())
+
+    with open(cfg.path.head_loudspeaker_positions_file, encoding="utf-8") as f:
+        head_loudspeaker_positions = list(json.load(f).keys())
+
+    scenes_metadata = {}
+    scene_id = 10001
+
+    music_path = Path(cfg.path.metadata_dir) / "musdb18.train.json"
+    with open(music_path, encoding="utf-8") as f:
+        tracks_list = [item["Track Name"] for item in json.load(f)]
+
+    for track_name in tracks_list:
+        set_song_seed(track_name)
+
+        gains_to_choose = gains.copy()
+        hlp_to_choose = head_loudspeaker_positions.copy()
+
+        for _ in range(cfg.scene.number_scenes_per_song):
+            g = choose_samples(gains_to_choose, 1)[0]
+            hlp = choose_samples(hlp_to_choose, 1)[0]
+
+            # remove the chosen gain and head loudspeaker position from the list
+            # so we choose a different one next time
+            gains_to_choose.remove(g)
+            hlp_to_choose.remove(hlp)
+
+            scenes_metadata[f"scene_{scene_id:04d}"] = {
+                "music": track_name,
+                "gain": g,
+                "head_loudspeaker_positions": hlp,
+            }
+            scene_id += 1
+
+    output_file = Path(cfg.path.metadata_dir) / "scenes.train.json"
+    output_file.parent.mkdir(parents=True, exist_ok=True)
+    with open(output_file, "w", encoding="utf-8") as f:
+        json.dump(scenes_metadata, f, indent=4)
+
+
+def generate_scene_listener(cfg: DictConfig) -> None:
+    """Generates the scene-listeners metadata file for training
+
+    Args:
+        cfg: Hydra config
+    """
+    scenes_path = Path(cfg.path.metadata_dir) / "scenes.train.json"
+    with open(scenes_path, encoding="utf-8") as f:
+        scenes = list(json.load(f).keys())
+
+    listeners_path = Path(cfg.path.metadata_dir) / "listeners.train.json"
+    with open(listeners_path, encoding="utf-8") as f:
+        listeners = list(json.load(f).keys())
+
+    scene_listeners = {}
+
+    set_song_seed("icassp2024")
+
+    for scene in scenes:
+        scene_listeners[scene] = list(
+            choose_samples(listeners, cfg.scene_listener.number_listeners_per_scene)
+        )
+
+    output_file = Path(cfg.path.metadata_dir) / "scene_listeners.train.json"
+    output_file.parent.mkdir(parents=True, exist_ok=True)
+    with open(output_file, "w", encoding="utf-8") as f:
+        json.dump(scene_listeners, f, indent=4)
+
+
+@hydra.main(config_path="", config_name="config")
+def run(cfg: DictConfig) -> None:
+    """Module generates the scenes and scene-listeners metadata files for training."""
+    generate_scenes(cfg)
+    generate_scene_listener(cfg)
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    run()


Test patch:
diff --git a/recipes/cad1/task1/baseline/test.py b/recipes/cad1/task1/baseline/test.py
new file mode 100644
index 000000000..47592ad17
--- /dev/null
+++ b/recipes/cad1/task1/baseline/test.py
@@ -0,0 +1,241 @@
+""" Run the baseline enhancement. """
+from __future__ import annotations
+
+# pylint: disable=import-error
+# pylint: disable=too-many-function-args
+import json
+import logging
+import shutil
+from pathlib import Path
+
+import hydra
+import numpy as np
+import pandas as pd
+import torch
+from omegaconf import DictConfig
+from scipy.io import wavfile
+from torchaudio.pipelines import HDEMUCS_HIGH_MUSDB
+
+from clarity.enhancer.compressor import Compressor
+from clarity.enhancer.nalr import NALR
+from clarity.utils.audiogram import Listener
+from recipes.cad1.task1.baseline.enhance import (
+    decompose_signal,
+    get_device,
+    process_stems_for_listener,
+    remix_signal,
+    save_flac_signal,
+)
+from recipes.cad1.task1.baseline.evaluate import make_song_listener_list
+
+# pylint: disable=too-many-locals
+
+logger = logging.getLogger(__name__)
+
+
+def pack_submission(
+    team_id: str,
+    root_dir: str | Path,
+    base_dir: str | Path = ".",
+) -> None:
+    """
+    Pack the submission files into an archive file.
+
+    Args:
+        team_id (str): Team ID.
+        root_dir (str | Path): Root directory of the archived file.
+        base_dir (str | Path): Base directory to archive. Defaults to ".".
+    """
+    # Pack the submission files
+    logger.info(f"Packing submission files for team {team_id}...")
+    shutil.make_archive(
+        f"submission_{team_id}",
+        "zip",
+        root_dir=root_dir,
+        base_dir=base_dir,
+    )
+
+
+@hydra.main(config_path="", config_name="config")
+def enhance(config: DictConfig) -> None:
+    """
+    Run the music enhancement.
+    The system decomposes the music into vocal, drums, bass, and other stems.
+    Then, the NAL-R prescription procedure is applied to each stem.
+    Args:
+        config (dict): Dictionary of configuration options for enhancing music.
+
+    Returns 8 stems for each song:
+        - left channel vocal, drums, bass, and other stems
+        - right channel vocal, drums, bass, and other stems
+    """
+
+    if config.separator.model not in ["demucs", "openunmix"]:
+        raise ValueError(f"Separator model {config.separator.model} not supported.")
+
+    enhanced_folder = Path("enhanced_signals") / "evaluation"
+    enhanced_folder.mkdir(parents=True, exist_ok=True)
+
+    if config.separator.model == "demucs":
+        separation_model = HDEMUCS_HIGH_MUSDB.get_model()
+        model_sample_rate = HDEMUCS_HIGH_MUSDB.sample_rate
+        sources_order = separation_model.sources
+        normalise = True
+    elif config.separator.model == "openunmix":
+        separation_model = torch.hub.load("sigsep/open-unmix-pytorch", "umxhq", niter=0)
+        model_sample_rate = separation_model.sample_rate
+        sources_order = ["vocals", "drums", "bass", "other"]
+        normalise = False
+    else:
+        raise ValueError(f"Separator model {config.separator.model} not supported.")
+
+    device, _ = get_device(config.separator.device)
+    separation_model.to(device)
+
+    # Processing Validation Set
+    # Load listener audiograms and songs
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
+
+    with open(config.path.music_file, encoding="utf-8") as file:
+        song_data = json.load(file)
+    songs_details = pd.DataFrame.from_dict(song_data)
+
+    with open(config.path.music_segments_test_file, encoding="utf-8") as file:
+        songs_segments = json.load(file)
+
+    song_listener_pairs = make_song_listener_list(
+        songs_details["Track Name"], listener_dict
+    )
+    # Select a batch to process
+    song_listener_pairs = song_listener_pairs[
+        config.evaluate.batch :: config.evaluate.batch_size
+    ]
+
+    # Create hearing aid objects
+    enhancer = NALR(**config.nalr)
+    compressor = Compressor(**config.compressor)
+
+    # Decompose each song into left and right vocal, drums, bass, and other stems
+    # and process each stem for the listener
+    prev_song_name = None
+    num_song_list_pair = len(song_listener_pairs)
+    for idx, song_listener in enumerate(song_listener_pairs, 1):
+        song_name, listener_name = song_listener
+        logger.info(
+            f"[{idx:03d}/{num_song_list_pair:03d}] "
+            f"Processing {song_name} for {listener_name}..."
+        )
+        # Get the listener's audiogram
+        listener = listener_dict[listener_name]
+
+        # Find the music split directory
+        split_directory = (
+            "test"
+            if songs_details.loc[
+                songs_details["Track Name"] == song_name, "Split"
+            ].iloc[0]
+            == "test"
+            else "train"
+        )
+
+        # Baseline Steps
+        # 1. Decompose the mixture signal into vocal, drums, bass, and other stems
+        #    We validate if 2 consecutive signals are the same to avoid
+        #    decomposing the same song multiple times
+        if prev_song_name != song_name:
+            # Decompose song only once
+            prev_song_name = song_name
+
+            sample_rate, mixture_signal = wavfile.read(
+                Path(config.path.music_dir)
+                / split_directory
+                / song_name
+                / "mixture.wav"
+            )
+            mixture_signal = (mixture_signal / 32768.0).astype(np.float32).T
+            assert sample_rate == config.sample_rate
+
+            # Decompose mixture signal into stems
+            stems = decompose_signal(
+                separation_model,
+                model_sample_rate,
+                mixture_signal,
+                sample_rate,
+                device,
+                sources_order,
+                listener,
+                normalise,
+            )
+
+        # 2. Apply NAL-R prescription to each stem
+        #     Baseline applies NALR prescription to each stem instead of using the
+        #     listener's audiograms in the decomposition. This step can be skipped
+        #     if the listener's audiograms are used in the decomposition
+        processed_stems = process_stems_for_listener(
+            stems,
+            enhancer,
+            compressor,
+            listener,
+            config.apply_compressor,
+        )
+
+        # 3. Save processed stems
+        for stem_str, stem_signal in processed_stems.items():
+            filename = (
+                enhanced_folder
+                / f"{listener.id}"
+                / f"{song_name}"
+                / f"{listener.id}_{song_name}_{stem_str}.flac"
+            )
+            filename.parent.mkdir(parents=True, exist_ok=True)
+            start = songs_segments[song_name]["objective_evaluation"]["start"]
+            end = songs_segments[song_name]["objective_evaluation"]["end"]
+            save_flac_signal(
+                signal=stem_signal[
+                    int(start * config.sample_rate) : int(end * config.sample_rate)
+                ],
+                filename=filename,
+                signal_sample_rate=config.sample_rate,
+                output_sample_rate=config.stem_sample_rate,
+                do_scale_signal=True,
+            )
+
+        # 3. Remix Signal
+        enhanced = remix_signal(processed_stems)
+
+        # 5. Save enhanced (remixed) signal
+        filename = (
+            enhanced_folder
+            / f"{listener.id}"
+            / f"{song_name}"
+            / f"{listener.id}_{song_name}_remix.flac"
+        )
+        start = songs_segments[song_name]["subjective_evaluation"]["start"]
+        end = songs_segments[song_name]["subjective_evaluation"]["end"]
+        save_flac_signal(
+            signal=enhanced[
+                int(start * config.sample_rate) : int(end * config.sample_rate)
+            ],
+            filename=filename,
+            signal_sample_rate=config.sample_rate,
+            output_sample_rate=config.remix_sample_rate,
+            do_clip_signal=True,
+            do_soft_clip=config.soft_clip,
+        )
+
+    pack_submission(
+        team_id=config.team_id,
+        root_dir=enhanced_folder.parent,
+        base_dir=enhanced_folder.name,
+    )
+
+    logger.info("Evaluation complete.!!")
+    logger.info(
+        f"Please, submit the file submission_{config.team_id}.zip to the challenge "
+        "using the link provided. Thank you.!!"
+    )
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    enhance()
diff --git a/recipes/cad1/task2/baseline/test.py b/recipes/cad1/task2/baseline/test.py
new file mode 100644
index 000000000..8e5e26638
--- /dev/null
+++ b/recipes/cad1/task2/baseline/test.py
@@ -0,0 +1,67 @@
+""" Run the dummy enhancement. """
+# pylint: disable=too-many-locals
+# pylint: disable=import-error
+from __future__ import annotations
+
+import logging
+import shutil
+from pathlib import Path
+
+import hydra
+from omegaconf import DictConfig
+
+from recipes.cad1.task2.baseline.enhance import enhance as enhance_set
+
+logger = logging.getLogger(__name__)
+
+
+def pack_submission(
+    team_id: str,
+    root_dir: str | Path,
+    base_dir: str | Path = ".",
+) -> None:
+    """
+    Pack the submission files into an archive file.
+
+    Args:
+        team_id (str): Team ID.
+        root_dir (str | Path): Root directory of the archived file.
+        base_dir (str | Path): Base directory to archive. Defaults to ".".
+    """
+    # Pack the submission files
+    logger.info(f"Packing submission files for team {team_id}...")
+    shutil.make_archive(
+        f"submission_{team_id}",
+        "zip",
+        root_dir=root_dir,
+        base_dir=base_dir,
+    )
+
+
+@hydra.main(config_path="", config_name="config")
+def enhance(config: DictConfig) -> None:
+    """
+    Run the music enhancement.
+    The baseline system is a dummy processor that returns the input signal.
+
+    Args:
+        config (dict): Dictionary of configuration options for enhancing music.
+    """
+    enhance_set(config)
+
+    pack_submission(
+        team_id=config.team_id,
+        root_dir=Path("enhanced_signals"),
+        base_dir=config.evaluate.split,
+    )
+
+    logger.info("Evaluation complete.!!")
+    logger.info(
+        f"Please, submit the file submission_{config.team_id}.zip to the challenge "
+        "using the link provided. Thank you.!!"
+    )
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    enhance()
diff --git a/tests/evaluator/haaqi/test_haaqi.py b/tests/evaluator/haaqi/test_haaqi.py
index c38ec5a60..bf29363e0 100644
--- a/tests/evaluator/haaqi/test_haaqi.py
+++ b/tests/evaluator/haaqi/test_haaqi.py
@@ -1,4 +1,5 @@
 """Tests for haaqi module"""
+# pylint: disable=import-error
 import numpy as np
 import pytest
 
@@ -57,8 +58,9 @@ def test_compute_haaqi(levels, freqs, expected_result):
     score = compute_haaqi(
         processed_signal=enh_signal,
         reference_signal=ref_signal,
+        processed_sample_rate=sample_rate,
+        reference_sample_rate=sample_rate,
         audiogram=audiogram,
-        sample_rate=sample_rate,
     )
 
     # Check that the score is a float between 0 and 1
diff --git a/tests/recipes/cad1/task1/baseline/test_enhance_task1.py b/tests/recipes/cad1/task1/baseline/test_enhance_task1.py
index b8cf8e8db..2b171815f 100644
--- a/tests/recipes/cad1/task1/baseline/test_enhance_task1.py
+++ b/tests/recipes/cad1/task1/baseline/test_enhance_task1.py
@@ -1,10 +1,10 @@
 """Tests for the enhance module"""
+# pylint: disable=import-error
 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
@@ -43,17 +43,19 @@ def test_map_to_dict():
     assert output == expected_output
 
 
+@pytest.mark.xfail(reason="Github issue downloading from torch hub")
 @pytest.mark.parametrize(
-    "separation_model",
+    "separation_model,normalise",
     [
-        pytest.param("demucs"),
-        pytest.param("openunmix", marks=pytest.mark.slow),
+        (pytest.param("demucs"), True),
+        (pytest.param("openunmix", marks=pytest.mark.slow), True),
     ],
 )
-def test_decompose_signal(separation_model):
+def test_decompose_signal(separation_model, normalise):
     """Takes a signal and decomposes it into VDBO sources using the HDEMUCS model"""
     np.random.seed(123456789)
     # Load Separation Model
+    separation_model = separation_model.values[0]
     if separation_model == "demucs":
         model = HDEMUCS_HIGH_MUSDB.get_model().double()
     elif separation_model == "openunmix":
@@ -67,27 +69,19 @@ def test_decompose_signal(separation_model):
     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
     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,
-        listener,
+        model=model,
+        model_sample_rate=sample_rate,
+        signal=signal,
+        signal_sample_rate=sample_rate,
+        device=device,
+        sources_list=["drums", "bass", "other", "vocals"],
+        listener=listener,
+        normalise=normalise,
     )
     expected_results = np.load(
         RESOURCES / f"test_enhance.test_decompose_signal_{separation_model}.npy",
diff --git a/tests/recipes/cad1/task1/baseline/test_evaluate.py b/tests/recipes/cad1/task1/baseline/test_evaluate.py
index 5b8a06120..8a3977d69 100644
--- a/tests/recipes/cad1/task1/baseline/test_evaluate.py
+++ b/tests/recipes/cad1/task1/baseline/test_evaluate.py
@@ -1,48 +1,21 @@
 """Tests for the evaluation module"""
 from pathlib import Path
 
+# pylint: disable=import-error
 import numpy as np
 import pytest
 from omegaconf import DictConfig
 from scipy.io import wavfile
 
 from clarity.utils.audiogram import Audiogram, Listener
+from clarity.utils.flac_encoder import FlacEncoder
 from recipes.cad1.task1.baseline.evaluate import (
-    ResultsFile,
     _evaluate_song_listener,
     make_song_listener_list,
     set_song_seed,
 )
 
 
-def test_results_file(tmp_path):
-    """Test the class ResultsFile"""
-    results_file = tmp_path / "results.csv"
-    result_file = ResultsFile(results_file.as_posix())
-    result_file.write_header()
-    result_file.add_result(
-        listener_id="My listener",
-        song="My favorite song",
-        score=0.9,
-        instruments_scores={
-            "left_bass": 0.8,
-            "right_bass": 0.8,
-            "left_drums": 0.9,
-            "right_drums": 0.9,
-            "left_other": 0.8,
-            "right_other": 0.8,
-            "left_vocals": 0.95,
-            "right_vocals": 0.95,
-        },
-    )
-    with open(results_file, encoding="utf-8") as file:
-        contents = file.read()
-        assert (
-            "My favorite song,My listener,0.9,0.8,0.8,0.9,0.9,0.8,0.8,0.95,0.95"
-            in contents
-        )
-
-
 @pytest.mark.parametrize(
     "song,expected_result",
     [("my favorite song", 83), ("another song", 3)],
@@ -87,10 +60,11 @@ def test_make_song_listener_list():
             "punk_is_not_dead",
             "my_music_listener",
             {
+                "stem_sample_rate": 44100,
+                "sample_rate": 44100,
                 "evaluate": {"set_random_seed": True},
                 "path": {"music_dir": None},
-                "sample_rate": 44100,
-                "nalr": {"sample_rate": 44100},
+                "nalr": {"fs": 44100},
             },
             "test",
             {
@@ -101,14 +75,14 @@ def test_make_song_listener_list():
                 }
             },
             {
-                "left_drums": 0.205517835,
-                "right_drums": 0.270553157,
-                "left_bass": 0.207187220,
-                "right_bass": 0.205454381,
-                "left_other": 0.237097711,
-                "right_other": 0.227505708,
-                "left_vocals": 0.227105999,
-                "right_vocals": 0.272616615,
+                "left_drums": 0.14229280292204488,
+                "right_drums": 0.15044867874762802,
+                "left_bass": 0.13337685099485902,
+                "right_bass": 0.14541734646032817,
+                "left_other": 0.16310385596493193,
+                "right_other": 0.1542791489799909,
+                "left_vocals": 0.12291878218281638,
+                "right_vocals": 0.13683790592287856,
             },
         )
     ],
@@ -142,22 +116,22 @@ def test_evaluate_song_listener(
     instruments = ["drums", "bass", "other", "vocals"]
 
     # Create reference and enhanced wav samples
+    flac_encoder = FlacEncoder()
     for lr_instrument in list(expected_results.keys()):
         # enhanced signals are mono
         enh_file = (
             enhanced_folder
             / f"{listener.id}"
             / f"{song}"
-            / f"{listener.id}_{song}_{lr_instrument}.wav"
+            / f"{listener.id}_{song}_{lr_instrument}.flac"
         )
         enh_file.parent.mkdir(exist_ok=True, parents=True)
+        with open(Path(enh_file).with_suffix(".txt"), "w", encoding="utf-8") as file:
+            file.write("1.0")
 
         # Using very short 100 ms signals to speed up the test
-        wavfile.write(
-            enh_file,
-            44100,
-            np.random.uniform(-1, 1, 4410).astype(np.float32) * 32768,
-        )
+        enh_signal = np.random.uniform(-1, 1, 4410).astype(np.float32) * 32768
+        flac_encoder.encode(enh_signal.astype(np.int16), config.sample_rate, enh_file)
 
     for instrument in instruments:
         # reference signals are stereo
@@ -182,7 +156,7 @@ def test_evaluate_song_listener(
     # Combined score
     assert isinstance(combined_score, float)
     assert combined_score == pytest.approx(
-        0.231629828, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
+        0.14358442152193474, rel=pytest.rel_tolerance, abs=pytest.abs_tolerance
     )
 
     # Per instrument score
diff --git a/tests/recipes/cad1/task2/baseline/test_baseline_utils.py b/tests/recipes/cad1/task2/baseline/test_baseline_utils.py
index 2ae029d68..7f0d527b6 100644
--- a/tests/recipes/cad1/task2/baseline/test_baseline_utils.py
+++ b/tests/recipes/cad1/task2/baseline/test_baseline_utils.py
@@ -1,6 +1,7 @@
 """Test for baseline_utils.py"""
 from pathlib import Path
 
+# pylint: disable=import-error
 import librosa
 import numpy as np
 import pytest
@@ -35,7 +36,7 @@ def test_load_listeners_and_scenes():
         {
             "path": {
                 "scenes_file": (RESOURCES / "scenes.json").as_posix(),
-                "listeners_train_file": (RESOURCES / "listeners.json").as_posix(),
+                "listeners_file": (RESOURCES / "listeners.json").as_posix(),
                 "scenes_listeners_file": (
                     RESOURCES / "scenes_listeners.json"
                 ).as_posix(),
diff --git a/tests/recipes/cad1/task2/baseline/test_enhance_task2.py b/tests/recipes/cad1/task2/baseline/test_enhance_task2.py
index 27e31d830..aaa340c46 100644
--- a/tests/recipes/cad1/task2/baseline/test_enhance_task2.py
+++ b/tests/recipes/cad1/task2/baseline/test_enhance_task2.py
@@ -1,4 +1,6 @@
 """Test the enhance module."""
+# pylint: disable=import-error
+
 from pathlib import Path
 
 import numpy as np
diff --git a/tests/recipes/cad_icassp_2024/__init__.py b/tests/recipes/cad_icassp_2024/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/recipes/cad_icassp_2024/baseline/__init__.py b/tests/recipes/cad_icassp_2024/baseline/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/recipes/cad_icassp_2024/baseline/test_enhance.py b/tests/recipes/cad_icassp_2024/baseline/test_enhance.py
new file mode 100644
index 000000000..36fac0a43
--- /dev/null
+++ b/tests/recipes/cad_icassp_2024/baseline/test_enhance.py
@@ -0,0 +1,134 @@
+"""Tests for the enhance module"""
+# pylint:: disable=import-error
+from pathlib import Path
+
+import numpy as np
+import pytest
+import torch
+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, Listener
+from clarity.utils.flac_encoder import read_flac_signal
+from recipes.cad_icassp_2024.baseline.enhance import (
+    decompose_signal,
+    process_remix_for_listener,
+    save_flac_signal,
+)
+
+BASE_DIR = Path.cwd()
+RESOURCES = BASE_DIR / "tests" / "resources" / "recipes" / "cad_icassp_2024"
+
+
+def test_save_flac_signal(tmp_path):
+    """Test save flac signal"""
+    np.random.seed(2024)
+    sample_rate = 44100
+    duration = 0.5
+    signal = np.random.rand(int(sample_rate * duration))
+
+    filename = Path(tmp_path) / "signal.flac"
+    save_flac_signal(signal, filename, sample_rate, sample_rate)
+
+    signal_out, sample_rate_out = read_flac_signal(filename)
+    assert np.sum(signal) == pytest.approx(11040.050741283)
+    assert np.sum(signal_out) == pytest.approx(11039.716)
+    assert sample_rate_out == sample_rate
+
+
+@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(2024)
+    # Load Separation Model
+    if separation_model == "demucs":
+        model = HDEMUCS_HIGH_MUSDB.get_model()
+        model_sample_rate = HDEMUCS_HIGH_MUSDB.sample_rate
+        sources_order = model.sources
+
+    elif separation_model == "openunmix":
+        model = torch.hub.load("sigsep/open-unmix-pytorch", "umxhq")
+        model_sample_rate = model.sample_rate
+        sources_order = ["vocals", "drums", "bass", "other"]
+
+    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))).astype(
+        np.float32
+    )
+
+    # 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(
+        model,
+        model_sample_rate,
+        signal,
+        sample_rate,
+        device,
+        sources_order,
+        listener,
+    )
+    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])
+
+
+@pytest.mark.parametrize(
+    "apply_compressor",
+    [
+        True,
+        False,
+    ],
+)
+def test_process_remix_for_listener(apply_compressor):
+    """Test the process remix for listener"""
+    np.random.seed(2024)
+    sample_rate = 44100
+    duration = 0.5
+    signal = np.random.uniform(size=(2, int(duration * sample_rate)))
+
+    audiogram = Audiogram(
+        levels=np.ones(9),
+        frequencies=np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000]),
+    )
+    listener = Listener(audiogram_left=audiogram, audiogram_right=audiogram)
+    enhancer = NALR(nfir=220, sample_rate=16000)
+    compressor = Compressor(
+        threshold=0.35, attenuation=0.1, attack=50, release=1000, rms_buffer_size=0.064
+    )
+
+    output = process_remix_for_listener(
+        signal, enhancer, compressor, listener, apply_compressor=apply_compressor
+    )
+
+    if apply_compressor:
+        expected_results = np.load(
+            RESOURCES / "test_enhance.test_process_remix_for_listener_w_compressor.npy",
+            allow_pickle=True,
+        )[()]
+    else:
+        expected_results = np.load(
+            RESOURCES
+            / "test_enhance.test_process_remix_for_listener_wo_compressor.npy",
+            allow_pickle=True,
+        )[()]
+
+    assert np.sum(output) == pytest.approx(np.sum(expected_results))
diff --git a/tests/recipes/cad_icassp_2024/baseline/test_evaluate.py b/tests/recipes/cad_icassp_2024/baseline/test_evaluate.py
new file mode 100644
index 000000000..a1e490149
--- /dev/null
+++ b/tests/recipes/cad_icassp_2024/baseline/test_evaluate.py
@@ -0,0 +1,172 @@
+"""Tests for the enhance module"""
+# pylint:: disable=import-error
+from pathlib import Path
+
+import numpy as np
+import pyloudnorm as pyln
+import pytest
+from scipy.io import wavfile
+
+from clarity.enhancer.compressor import Compressor
+from clarity.enhancer.nalr import NALR
+from clarity.utils.audiogram import Audiogram
+from recipes.cad_icassp_2024.baseline.evaluate import (
+    apply_gains,
+    apply_ha,
+    level_normalisation,
+    load_reference_stems,
+    make_scene_listener_list,
+    remix_stems,
+    set_song_seed,
+)
+
+
+@pytest.mark.parametrize(
+    "apply_compressor,expected_result",
+    [(True, 5130.70234905519), (False, 11039.797138690315)],
+)
+def test_apply_ha(apply_compressor, expected_result):
+    """Test apply_ha"""
+    np.random.seed(2024)
+
+    sample_rate = 44100
+    duration = 0.5
+
+    signal = np.random.rand(int(sample_rate * duration))
+
+    audiogram = Audiogram(
+        levels=np.ones(9),
+        frequencies=np.array([250, 500, 1000, 2000, 4000, 6000, 8000, 9000, 10000]),
+    )
+    enhancer = NALR(nfir=220, sample_rate=16000)
+    compressor = Compressor(
+        threshold=0.35, attenuation=0.1, attack=50, release=1000, rms_buffer_size=0.064
+    )
+
+    output = apply_ha(enhancer, compressor, signal, audiogram, apply_compressor)
+
+    assert np.sum(output) == pytest.approx(expected_result)
+
+
+@pytest.mark.parametrize(
+    "gains,expected_result",
+    [
+        ({"vocals": 0, "bass": 0, "drums": 0, "other": 0}, 88207.89862265925),
+        ({"vocals": 10, "bass": 0, "drums": 0, "other": 0}, 135801.06478181464),
+        ({"vocals": 0, "bass": 10, "drums": 0, "other": 0}, 135716.2197962051),
+        ({"vocals": 0, "bass": 0, "drums": 10, "other": 0}, 136040.5632722622),
+        ({"vocals": 0, "bass": 0, "drums": 0, "other": 10}, 136003.7152825283),
+        ({"vocals": 6, "bass": 0, "drums": 0, "other": 0}, 110114.28261802963),
+        ({"vocals": 3, "bass": 0, "drums": 0, "other": 0}, 97288.123734263),
+        ({"vocals": -3, "bass": 0, "drums": 0, "other": 0}, 81779.59153364039),
+        ({"vocals": -6, "bass": 0, "drums": 0, "other": 0}, 77228.6986292428),
+        ({"vocals": -10, "bass": 0, "drums": 0, "other": 0}, 73157.61801048138),
+    ],
+)
+def test_apply_gains(tmp_path, gains, expected_result):
+    """Test apply_gains
+    It test the 4 different gains for the 4 different stems
+    """
+    np.random.seed(2024)
+
+    sample_rate = 44100
+    duration = 0.5
+
+    for stem in ["vocals", "drums", "bass", "other", "mixture"]:
+        filename = Path(tmp_path) / f"{stem}.wav"
+        signal = np.random.uniform(size=(int(sample_rate * duration), 2))
+        wavfile.write(filename, sample_rate, signal)
+
+    stems, _ = load_reference_stems(tmp_path)
+    output = apply_gains(stems, sample_rate, gains)
+
+    result = 0
+    for _, stem_signal in output.items():
+        result += np.sum(stem_signal)
+
+    assert result == pytest.approx(expected_result)
+
+
+def test_level_normalisation():
+    """Test level_normalisation"""
+    np.random.seed(2024)
+
+    sample_rate = 44100
+    duration = 0.5
+
+    reference_signal = np.random.rand(int(sample_rate * duration))
+    signal = np.random.rand(int(sample_rate * duration)) * 2
+
+    meter = pyln.Meter(int(sample_rate))
+
+    normed_signal = level_normalisation(signal, reference_signal, sample_rate)
+
+    assert np.sum(normed_signal) == pytest.approx(10899.6694813693)
+
+    normed_signal_lufs = meter.integrated_loudness(normed_signal)
+    reference_signal_lufs = meter.integrated_loudness(reference_signal)
+    assert normed_signal_lufs == pytest.approx(reference_signal_lufs)
+
+
+def test_remix_stems(tmp_path):
+    """Test remix_stems"""
+    np.random.seed(2024)
+
+    sample_rate = 44100
+    duration = 0.5
+
+    for stem in ["vocals", "drums", "bass", "other", "mixture"]:
+        filename = Path(tmp_path) / f"{stem}.wav"
+        signal = np.random.rand(int(sample_rate * duration))
+        wavfile.write(filename, sample_rate, signal)
+
+    stems, mixture = load_reference_stems(tmp_path)
+    remix = remix_stems(stems, mixture, sample_rate)
+    assert np.sum(remix) == pytest.approx(21950.335598725396)
+
+
+def test_make_scene_listener_list():
+    """Test make_scene_listener_list"""
+    song_list = {
+        "my favorite song": ["list1", "list2"],
+        "another song": ["list3", "list4"],
+    }
+    expected_output = [
+        ("my favorite song", "list1"),
+        ("my favorite song", "list2"),
+        ("another song", "list3"),
+        ("another song", "list4"),
+    ]
+    assert make_scene_listener_list(song_list) == pytest.approx(expected_output)
+
+
+@pytest.mark.parametrize(
+    "song,expected_result",
+    [("my favorite song", 83), ("another song", 3)],
+)
+def test_set_song_seed(song, expected_result):
+    """Thest the function set_song_seed using 2 different inputs"""
+    # Set seed for the same song
+    set_song_seed(song)
+    assert np.random.randint(100) == pytest.approx(expected_result)
+
+
+def test_load_reference_stems(tmp_path):
+    """Test load_reference_stems"""
+    np.random.seed(2024)
+
+    sample_rate = 44100
+    duration = 0.5
+
+    for stem in ["vocals", "drums", "bass", "other", "mixture"]:
+        filename = Path(tmp_path) / f"{stem}.wav"
+        signal = np.random.rand(int(sample_rate * duration))
+        wavfile.write(filename, sample_rate, signal)
+
+    stems, mixture = load_reference_stems(tmp_path)
+    assert len(stems) == 4
+    assert np.sum(stems["vocals"]) == pytest.approx(11040.05074128)
+    assert np.sum(stems["drums"]) == pytest.approx(10970.61284548)
+    assert np.sum(stems["bass"]) == pytest.approx(11062.77053343)
+    assert np.sum(stems["other"]) == pytest.approx(11058.65518849)
+    assert np.sum(mixture) == pytest.approx(11046.02394025)
diff --git a/tests/recipes/cad_icassp_2024/generate_dataset/test_generate_at_mic_musdb18.py b/tests/recipes/cad_icassp_2024/generate_dataset/test_generate_at_mic_musdb18.py
new file mode 100644
index 000000000..215b115cd
--- /dev/null
+++ b/tests/recipes/cad_icassp_2024/generate_dataset/test_generate_at_mic_musdb18.py
@@ -0,0 +1,168 @@
+"""Test the generate dataset script for the ICASSP 2024 CAD recipe."""
+# pylint:: disable=import-error
+from pathlib import Path
+
+import numpy as np
+import pytest
+from scipy.io import wavfile
+from scipy.signal import lfilter
+
+from recipes.cad_icassp_2024.generate_dataset.generate_at_mic_musdb18 import (
+    apply_hrtf,
+    find_precreated_samples,
+    load_hrtf_signals,
+    normalise_lufs_level,
+)
+
+# Create a mock object for the pyln.Meter class
+
+
+@pytest.fixture(name="temp_dir_with_samples")
+def fixture_temp_dir_with_samples(tmp_path):
+    source_dir = Path(tmp_path)
+    sample_dirs = {
+        "sample_dir1": [
+            "sample1.wav",
+            "sample2.wav",
+            "sample3.wav",
+            "sample4.wav",
+            "sample5.wav",
+        ],
+        "sample_dir2": ["sample1.wav"],
+    }
+
+    for sample_dir, sample_files in sample_dirs.items():
+        sample_dir_path = Path(source_dir) / "train" / sample_dir
+        sample_dir_path.mkdir(exist_ok=True, parents=True)
+        for sample_file in sample_files:
+            with open(sample_dir_path / sample_file, "w", encoding="utf-8") as f:
+                f.write("Sample content")
+    return source_dir
+
+
+def test_apply_hrtf_valid_signal():
+    # Test the function with a valid signal and HRTF
+    signal = np.array([[1, 2], [3, 4], [5, 6]], dtype=np.float64)
+    hrtf_left = np.array([[0.5, 0.3], [0.2, 0.1]], dtype=np.float64)
+    hrtf_right = np.array([[0.2, 0.1], [0.5, 0.3]], dtype=np.float64)
+
+    result = apply_hrtf(signal, hrtf_left, hrtf_right)
+
+    # Verify the result for each channel
+    expected_left_ear = lfilter(hrtf_left[:, 0], 1, signal[:, 0]) + lfilter(
+        hrtf_right[:, 0], 1, signal[:, 1]
+    )
+    expected_right_ear = lfilter(hrtf_left[:, 1], 1, signal[:, 0]) + lfilter(
+        hrtf_right[:, 1], 1, signal[:, 1]
+    )
+
+    expected_result = np.stack([expected_left_ear, expected_right_ear], axis=1)
+
+    np.testing.assert_allclose(result, expected_result, rtol=1e-3, atol=1e-6)
+
+
+def test_apply_hrtf_zero_signal():
+    # Test the function with a zero signal and HRTF
+    signal = np.zeros((3, 2), dtype=np.float64)
+    hrtf_left = np.array([[0.5, 0.3], [0.2, 0.1]], dtype=np.float64)
+    hrtf_right = np.array([[0.2, 0.1], [0.5, 0.3]], dtype=np.float64)
+
+    result = apply_hrtf(signal, hrtf_left, hrtf_right)
+
+    # The result should also be all zeros
+    expected_result = np.zeros((3, 2), dtype=np.float64)
+
+    np.testing.assert_allclose(result, expected_result, rtol=1e-3, atol=1e-6)
+
+
+def test_load_hrtf_signals(tmp_path):
+    # Create temporary files for testing
+    np.random.seed(2024)
+    hrtf_path = Path(tmp_path) / "hrtf_directory"
+    hrtf_path.mkdir(exist_ok=True, parents=True)
+
+    hp = {
+        "mic": "mic_name",
+        "subject": "subject_name",
+        "left_angle": -30,
+        "right_angle": 45,
+    }
+
+    left_file = hrtf_path / f"{hp['mic']}-{hp['subject']}-n{abs(hp['left_angle'])}.wav"
+    right_file = (
+        hrtf_path / f"{hp['mic']}-{hp['subject']}-p{abs(hp['right_angle'])}.wav"
+    )
+
+    left_signal = np.random.rand(44100)
+    right_signal = np.random.rand(44100)
+
+    wavfile.write(left_file, 44100, left_signal)
+    wavfile.write(right_file, 44100, right_signal)
+
+    # Call the function
+    left_result, right_result = load_hrtf_signals(str(hrtf_path), hp)
+
+    # Verify that the function returns the expected signals
+    assert np.sum(left_result) == pytest.approx(np.sum(left_signal))
+    assert np.sum(right_result) == pytest.approx(np.sum(right_signal))
+
+
+def test_normalise_lufs_level_same_loudness():
+    # Test when the signal and reference signal have the same loudness
+    np.random.seed(2024)
+    signal = np.random.rand(44100)
+    sample_rate = 44100.0
+
+    result = normalise_lufs_level(signal, signal, sample_rate)
+
+    # Since the loudness is the same, the result should be the same as the input signal
+    assert np.sum(result) == pytest.approx(np.sum(signal))
+
+
+def test_normalise_lufs_level_reference_quieter():
+    # Test when the reference signal is quieter than the signal
+    np.random.seed(2024)
+    signal = np.random.rand(44100)
+    reference_signal = np.random.rand(44100) / 0.2
+    sample_rate = 44100.0
+
+    result = normalise_lufs_level(signal, reference_signal, sample_rate)
+
+    # The result should be a normalized version of the signal so
+    # that it matches the reference loudness
+    expected_result = 109580.6307104611
+    assert np.sum(result) == pytest.approx(expected_result)
+
+
+def test_normalise_lufs_level_reference_louder():
+    # Test when the reference signal is louder than the signal
+    np.random.seed(2024)
+    signal = np.random.rand(44100)
+    reference_signal = np.random.rand(44100) * 0.2
+    sample_rate = 44100.0
+
+    result = normalise_lufs_level(signal, reference_signal, sample_rate)
+
+    # The result should be a normalized version of the signal so
+    # that it matches the reference loudness
+    expected_result = 4383.225228418446
+    assert np.sum(result) == pytest.approx(expected_result)
+
+
+def test_find_precreated_samples(temp_dir_with_samples):
+    # Get the list of precreated samples from the temporary directory
+    source_dir = temp_dir_with_samples
+    precreated_samples = find_precreated_samples(source_dir)
+
+    # Check if the expected sample files are in the result
+    assert "sample_dir1" in precreated_samples
+    assert "sample_dir2" not in precreated_samples
+
+
+def test_find_precreated_samples_empty_directory(tmp_path):
+    # Test when the source directory is empty
+    source_dir = Path(tmp_path)
+    precreated_samples = find_precreated_samples(source_dir)
+
+    # The result should be an empty list
+    assert precreated_samples == []
diff --git a/tests/recipes/cad_icassp_2024/generate_dataset/test_generate_train_scenes.py b/tests/recipes/cad_icassp_2024/generate_dataset/test_generate_train_scenes.py
new file mode 100644
index 000000000..287fe7709
--- /dev/null
+++ b/tests/recipes/cad_icassp_2024/generate_dataset/test_generate_train_scenes.py
@@ -0,0 +1,155 @@
+"""Test the generate scenes script for the ICASSP 2024 CAD recipe."""
+# pylint:: disable=import-error
+import json
+
+from omegaconf import DictConfig
+
+from recipes.cad_icassp_2024.generate_dataset.generate_train_scenes import (
+    choose_samples,
+    generate_scene_listener,
+    generate_scenes,
+)
+
+
+def test_choose_samples():
+    # Test with a source list containing integers
+    source = [1, 2, 3, 4, 5]
+    number = 3
+    samples = choose_samples(source, number)
+
+    # Check if the number of samples returned is equal to the specified number
+    assert len(samples) == number
+
+    # Check if all samples are in the source list
+    assert all(sample in source for sample in samples)
+
+    # Test with an empty source list
+    source = []
+    number = 3
+    samples = choose_samples(source, number)
+
+    # Check if an empty list is returned when the source list is empty
+    assert len(samples) == 0
+
+    # Test with a source list containing a single element
+    source = [42]
+    number = 3
+    samples = choose_samples(source, number)
+
+    # Check if the returned list contains the single element
+    assert len(samples) == 1
+    assert samples[0] == 42
+
+
+def test_generate_scenes(tmp_path):
+    # Define a mock configuration dictionary
+    tmp = tmp_path.as_posix()
+    cfg = DictConfig(
+        {
+            "path": {
+                "gains_file": f"{tmp}/fake_gains_file.json",
+                "head_loudspeaker_positions_file": f"{tmp}/fake_positions_file.json",
+                "metadata_dir": tmp,
+            },
+            "scene": {
+                "number_scenes_per_song": 2,  # Adjust as needed
+            },
+        }
+    )
+
+    # Create fake gains and head loudspeaker positions files
+    gains_data = {"gain1": 1, "gain2": 2, "gain3": 3}
+    positions_data = {"position1": [1, 2, 3], "position2": [4, 5, 6]}
+
+    with open(tmp_path / cfg.path.gains_file, "w", encoding="utf-8") as gains_file:
+        json.dump(gains_data, gains_file)
+
+    with open(
+        tmp_path / cfg.path.head_loudspeaker_positions_file, "w", encoding="utf-8"
+    ) as positions_file:
+        json.dump(positions_data, positions_file)
+
+    # Create a fake music metadata file
+    music_data = [{"Track Name": "track1"}, {"Track Name": "track2"}]
+
+    music_path = tmp_path / "musdb18.train.json"
+    music_path.parent.mkdir(exist_ok=True, parents=True)
+    with open(music_path, "w", encoding="utf-8") as music_file:
+        json.dump(music_data, music_file)
+
+    # Run the function
+    generate_scenes(cfg)
+
+    # Verify the results
+    scenes_file = tmp_path / "scenes.train.json"
+    assert scenes_file.exists()
+
+    with open(scenes_file, encoding="utf-8") as scenes_file:
+        scenes_data = json.load(scenes_file)
+
+    # Check if the number of scenes is correct
+    assert len(scenes_data) == 4
+
+    # Check if the scenes are correctly generated
+    assert scenes_data["scene_10001"]["music"] == "track1"
+    assert scenes_data["scene_10001"]["gain"] == "gain2"
+    assert scenes_data["scene_10001"]["head_loudspeaker_positions"] == "position2"
+    assert scenes_data["scene_10002"]["music"] == "track1"
+    assert scenes_data["scene_10002"]["gain"] == "gain1"
+    assert scenes_data["scene_10002"]["head_loudspeaker_positions"] == "position1"
+    assert scenes_data["scene_10003"]["music"] == "track2"
+    assert scenes_data["scene_10003"]["gain"] == "gain3"
+    assert scenes_data["scene_10003"]["head_loudspeaker_positions"] == "position2"
+    assert scenes_data["scene_10004"]["music"] == "track2"
+    assert scenes_data["scene_10004"]["gain"] == "gain1"
+    assert scenes_data["scene_10004"]["head_loudspeaker_positions"] == "position1"
+
+
+def test_generate_scene_listener(tmp_path):
+    # Define a mock configuration dictionary
+    cfg = DictConfig(
+        {
+            "path": {
+                "metadata_dir": tmp_path.as_posix(),
+            },
+            "scene_listener": {
+                "number_listeners_per_scene": 2,  # Adjust as needed
+            },
+        }
+    )
+
+    # Create a fake scenes metadata file
+    scenes_data = {
+        "scene1": {},
+        "scene2": {},
+        "scene3": {},
+    }
+    scenes_file = tmp_path / "scenes.train.json"
+    with open(scenes_file, "w", encoding="utf-8") as f:
+        json.dump(scenes_data, f)
+
+    # Create a fake listeners metadata file
+    listeners_data = {
+        "listener1": {},
+        "listener2": {},
+        "listener3": {},
+    }
+    listeners_file = tmp_path / "listeners.train.json"
+    with open(listeners_file, "w", encoding="utf-8") as f:
+        json.dump(listeners_data, f)
+
+    # Run the function
+    generate_scene_listener(cfg)
+
+    # Verify the results
+    scene_listeners_file = tmp_path / "scene_listeners.train.json"
+    assert scene_listeners_file.exists()
+
+    with open(scene_listeners_file, encoding="utf-8") as f:
+        scene_listeners_data = json.load(f)
+
+    # Check if the number of scene-listeners is correct
+    assert len(scene_listeners_data) == 3
+    assert len(scene_listeners_data["scene1"]) == 2
+    assert len(scene_listeners_data["scene2"]) == 2
+    assert len(scene_listeners_data["scene3"]) == 2
diff --git a/tests/resources/recipes/cad_icassp_2024/test_enhance.test_decompose_signal_demucs.npy b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_decompose_signal_demucs.npy
new file mode 100644
index 000000000..6c0577ac8
Binary files /dev/null and b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_decompose_signal_demucs.npy differ
diff --git a/tests/resources/recipes/cad_icassp_2024/test_enhance.test_decompose_signal_openunmix.npy b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_decompose_signal_openunmix.npy
new file mode 100644
index 000000000..0b90a2e9b
Binary files /dev/null and b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_decompose_signal_openunmix.npy differ
diff --git a/tests/resources/recipes/cad_icassp_2024/test_enhance.test_process_remix_for_listener_w_compressor.npy b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_process_remix_for_listener_w_compressor.npy
new file mode 100644
index 000000000..ab96c5de9
Binary files /dev/null and b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_process_remix_for_listener_w_compressor.npy differ
diff --git a/tests/resources/recipes/cad_icassp_2024/test_enhance.test_process_remix_for_listener_wo_compressor.npy b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_process_remix_for_listener_wo_compressor.npy
new file mode 100644
index 000000000..ab96c5de9
Binary files /dev/null and b/tests/resources/recipes/cad_icassp_2024/test_enhance.test_process_remix_for_listener_wo_compressor.npy differ
diff --git a/tests/resources/utils/test_source_separation_support.test_separate_sources.npy b/tests/resources/utils/test_source_separation_support.test_separate_sources.npy
new file mode 100644
index 000000000..650ea7987
Binary files /dev/null and b/tests/resources/utils/test_source_separation_support.test_separate_sources.npy differ
diff --git a/tests/resources/utils/test_source_separation_support.test_separate_sources_stereo.npy b/tests/resources/utils/test_source_separation_support.test_separate_sources_stereo.npy
new file mode 100644
index 000000000..b7c502703
Binary files /dev/null and b/tests/resources/utils/test_source_separation_support.test_separate_sources_stereo.npy differ
diff --git a/tests/utils/test_flac_encoder.py b/tests/utils/test_flac_encoder.py
new file mode 100644
index 000000000..85aa1e8f0
--- /dev/null
+++ b/tests/utils/test_flac_encoder.py
@@ -0,0 +1,76 @@
+"""Tests for the FlacEncoder class."""
+# pylint: disable=import-error
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+
+import numpy as np
+import pytest
+
+from clarity.utils.flac_encoder import FlacEncoder, read_flac_signal
+
+
+def test_encode_decode():
+    """Test that the FlacEncoder can encode and decode a signal."""
+    # create a random signal
+    np.random.seed(0)
+
+    sample_rate = 8000
+    signal = np.random.uniform(-1, 1, int(0.5 * sample_rate))
+    signal_int16 = signal * 32768.0
+    signal_int16 = np.clip(signal_int16, -32768, 32767).astype(np.int16)
+
+    # write the encoded bytes to a temporary file
+    with NamedTemporaryFile(suffix=".flac", delete=False) as tmpfile:
+        encoder = FlacEncoder()
+        # encode
+        _ = encoder.encode(
+            signal_int16, sample_rate=sample_rate, output_file=tmpfile.name
+        )
+        # decode
+        decoded_signal, decoded_sr = encoder.decode(Path(tmpfile.name))
+
+    # check that the decoded signal matches the original signal
+    assert np.sum(signal_int16) == pytest.approx(
+        np.sum(decoded_signal),
+        rel=pytest.rel_tolerance,
+        abs=pytest.abs_tolerance,
+    )
+
+    # check that the sample rate of the decoded signal is correct
+    assert decoded_sr == sample_rate
+
+
+def test_read_flac_signal(tmp_path):
+    # Create a test FLAC file
+    np.random.seed(2023)
+
+    filename = tmp_path / "test.flac"
+
+    sample_rate = 16000
+    signal = np.random.rand(1600)
+    scale_factor = np.max(np.abs(signal))
+
+    signal_scaled = signal / scale_factor
+    signal_scaled = signal_scaled * 32768.0
+    signal_scaled = np.clip(signal_scaled, -32768.0, 32767.0)
+    signal_scaled = signal_scaled.astype(np.dtype("int16"))
+
+    flac_encoder = FlacEncoder()
+    flac_encoder.encode(signal_scaled, sample_rate, filename)
+
+    # Create a test scale factor file
+    scale_filename = tmp_path / "test.txt"
+    with open(scale_filename, "w", encoding="utf-8") as fp:
+        fp.write(str(scale_factor))
+
+    # Call the function and check the output
+    signal_out, sample_rate_out = read_flac_signal(filename)
+
+    # As a result of the quantization, the signal is not exactly the same
+    # after encoding and decoding, so I'm changing the tolerance
+    # for this test
+    # np.sum(signal_out) = 2190.4271092907347
+    # np.sum(signal) = 2190.494140495932
+
+    assert np.sum(signal_out) == pytest.approx(np.sum(signal), rel=1e-4, abs=1e-4)
+    assert sample_rate_out == sample_rate
diff --git a/tests/utils/test_results_support.py b/tests/utils/test_results_support.py
new file mode 100644
index 000000000..507de0206
--- /dev/null
+++ b/tests/utils/test_results_support.py
@@ -0,0 +1,57 @@
+"""test for results support module"""
+# pylint: disable=import-error
+from pathlib import Path
+
+import pytest
+
+from clarity.utils.results_support import ResultsFile
+
+# Define some sample data for testing
+sample_header = ["Name", "Score"]
+sample_data = [{"Name": "Alice", "Score": 95}, {"Name": "Bob", "Score": 88}]
+
+
+@pytest.fixture(name="results_file")
+def fixture_results_file(tmpdir):
+    # Create a temporary directory and a temporary CSV file for testing
+    file_name = Path(tmpdir, "test_results.csv")
+    return ResultsFile(file_name, sample_header)
+
+
+def test_create_file_str(tmpdir):
+    # Create a temporary directory and a temporary CSV file for testing
+    file_name = f"{tmpdir}/test_results.csv"
+    result_file = ResultsFile(file_name, sample_header)
+    assert result_file.file_name.as_posix() == file_name
+
+
+def test_add_result(results_file):
+    # Test adding a result to the CSV file
+    results_file.add_result({"Name": "Charlie", "Score": 75})
+
+    # Read the CSV file and check if the added data is present
+    with open(results_file.file_name, encoding="utf-8") as csv_file:
+        lines = csv_file.readlines()
+        assert len(lines) == 2  # There should be 2 lines (header + 1 data row)
+        assert "Charlie,75\n" in lines  # Check if the added data is present
+
+
+def test_header_written(results_file):
+    # Test if the header row is written when the ResultsFile is created
+    with open(results_file.file_name, encoding="utf-8") as csv_file:
+        lines = csv_file.readlines()
+        assert len(lines) == 1  # There should be 1 line (only header)
+        assert lines[0].strip() == "Name,Score"  # Check the header content
+
+
+def test_missing_column(results_file):
+    # Test adding a result with a missing column
+    with pytest.raises(KeyError):
+        results_file.add_result({"Name": "Eve"})
+
+
+def test_nonexistent_file(tmp_path):
+    # Test creating a ResultsFile with a non-existent file
+    file_name = Path(tmp_path) / "nonexistent.csv"
+    with pytest.raises(FileNotFoundError):
+        ResultsFile(file_name, sample_header, append_results=True)
diff --git a/tests/utils/test_signal_processing.py b/tests/utils/test_signal_processing.py
index 62a91946b..f0bb22470 100644
--- a/tests/utils/test_signal_processing.py
+++ b/tests/utils/test_signal_processing.py
@@ -1,15 +1,44 @@
 """Test for utils.signal_processing module"""
+# pylint: disable=import-error
 import numpy as np
 import pytest
 
 from clarity.utils.signal_processing import (
+    clip_signal,
     compute_rms,
     denormalize_signals,
     normalize_signal,
     resample,
+    to_16bit,
 )
 
 
+def test_clip_signal_hard_clip():
+    # Test hard clipping (default behavior)
+    input_signal = np.array([1.5, -0.5, 2.0, -2.5, 0.75])
+    clipped_signal, n_clipped = clip_signal(input_signal)
+    assert np.all(clipped_signal == np.array([1.0, -0.5, 1.0, -1.0, 0.75]))
+    assert n_clipped == 3
+
+
+def test_clip_signal_soft_clip():
+    # Test soft clipping
+    input_signal = np.array([1.5, -0.5, 2.0, -2.5, 0.75])
+    clipped_signal, n_clipped = clip_signal(input_signal, soft_clip=True)
+    assert np.sum(clipped_signal) == pytest.approx(
+        np.sum([0.90514825, -0.46211716, 0.96402758, -0.9866143, 0.63514895])
+    )
+    assert n_clipped == 0
+
+
+def test_clip_signal_all_within_range():
+    # Test when all values are already within the range
+    input_signal = np.array([-0.8, 0.2, -0.6, 0.9])
+    clipped_signal, n_clipped = clip_signal(input_signal)
+    assert np.all(clipped_signal == input_signal)
+    assert n_clipped == 0
+
+
 @pytest.mark.parametrize(
     "signal,normalised_result,row_means",
     (  # Simple input
@@ -138,6 +167,41 @@ def test_compute_rms():
     )
 
 
+@pytest.mark.parametrize(
+    "signal,soft_clip,expected_output",
+    [
+        (
+            np.array([0.5, 2.0, -1.5, 0.8]),
+            True,
+            (np.array([0.46211716, 0.96402758, -0.90514825, 0.66403677]), 0),
+        ),
+        (np.array([0.5, 2.0, -1.5, 0.8]), False, (np.array([0.5, 1.0, -1.0, 0.8]), 2)),
+    ],
+)
+def test_clip_signal(signal, soft_clip, expected_output):
+    """Test the clip_signal function"""
+    # Test with soft clip
+    output = clip_signal(signal, soft_clip=soft_clip)
+    assert np.allclose(output[0], expected_output[0])
+    assert output[1] == expected_output[1]
+
+
+@pytest.mark.parametrize(
+    "signal,expected_output",
+    [
+        (np.array([0.5, 0.8, 0.2, 1.0]), np.array([16384, 26214, 6553, 32767])),
+        (np.array([-0.5, -0.8, -0.2, -1.0]), np.array([-16384, -26214, -6553, -32768])),
+        (np.array([0.5, -0.8, 0.2, -1.0]), np.array([16384, -26214, 6553, -32768])),
+    ],
+)
+def test_to_16bit(signal, expected_output):
+    """Test the to_16bit function"""
+    # Test with positive signal
+    output = to_16bit(signal)
+    print(output)
+    assert np.allclose(output, expected_output)
+
+
 @pytest.mark.parametrize(
     "input_sample_rate, input_shape, output_sample_rate, output_shape",
     [
diff --git a/tests/utils/test_source_separation_support.py b/tests/utils/test_source_separation_support.py
new file mode 100644
index 000000000..60591a4b1
--- /dev/null
+++ b/tests/utils/test_source_separation_support.py
@@ -0,0 +1,167 @@
+""" Test module for the source separation support"""
+# pylint: disable=import-error
+from pathlib import Path
+
+import numpy as np
+import pytest
+import torch
+
+from clarity.utils.source_separation_support import get_device, separate_sources
+
+BASE_DIR = Path.cwd()
+RESOURCES = BASE_DIR / "tests" / "resources" / "utils"
+
+
+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"]
+    # Create a dummy model
+    model = DummyModel(sources)
+    expected_results = np.load(
+        RESOURCES / "test_source_separation_support.test_separate_sources.npy",
+        allow_pickle=True,
+    )
+    device = torch.device("cpu")
+
+    mix = np.random.randn(batch_size, num_channels, length * sample_rate)
+    # Call separate_sources
+    output = separate_sources(model, mix, sample_rate, device=device)
+
+    # 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_separate_sources_no_batch():
+    """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
+
+    num_channels = 1
+    length = 1
+    sample_rate = 16000
+    sources = ["vocals", "drums", "bass", "other"]
+    # Create a dummy model
+    model = DummyModel(sources)
+    expected_results = np.load(
+        RESOURCES / "test_source_separation_support.test_separate_sources.npy",
+        allow_pickle=True,
+    )
+    device = torch.device("cpu")
+
+    mix = np.random.randn(num_channels, length * sample_rate)
+    # Call separate_sources
+    output = separate_sources(model, mix, sample_rate, device=device)
+
+    # 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_separate_sources_extra_dim():
+    """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
+
+    num_channels = 2
+    length = 1
+    sample_rate = 16000
+    sources = ["vocals", "drums", "bass", "other"]
+    # Create a dummy model
+    model = DummyModel(sources)
+    expected_results = np.load(
+        RESOURCES / "test_source_separation_support.test_separate_sources_stereo.npy",
+        allow_pickle=True,
+    )
+    device = torch.device("cpu")
+
+    mix = np.random.randn(1, num_channels, length * sample_rate)
+    # Call separate_sources
+    output = separate_sources(model, mix, sample_rate, device=device)
+
+    # 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_get_device_cpu():
+    """Test the correct device selection given the inputs"""
+    # Test default case (no argument passed)
+    device, device_type = get_device("cpu")
+    assert device == torch.device("cpu")
+    assert device_type == "cpu"
+
+
+def tests_get_device_wrong():
+    """Test the wrong device selection given the inputs"""
+    # Test default case (no argument passed)
+    with pytest.raises(ValueError):
+        device, device_type = get_device("wrong")
+        assert device == torch.device("cpu")
+        assert device_type == "cpu"