Repository: claritychallenge/clarity (language: python)

Issue:
update branch



Discussion / hints:

Fix patch:
diff --git a/clarity/utils/car_noise_simulator/__init__.py b/clarity/utils/car_noise_simulator/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/clarity/utils/car_noise_simulator/carnoise_parameters_generator.py b/clarity/utils/car_noise_simulator/carnoise_parameters_generator.py
new file mode 100644
index 000000000..ee7303a38
--- /dev/null
+++ b/clarity/utils/car_noise_simulator/carnoise_parameters_generator.py
@@ -0,0 +1,236 @@
+"""
+Class to generate random parameters for the Car noise signal generation
+
+These are 2 separated class to keep the logic separated
+"""
+from typing import Dict, Optional, Tuple
+
+import numpy as np
+
+
+class CarNoiseParametersGenerator:
+    """
+    A class to generate noise parameters for a car.
+    The constructor takes a boolean flag to indicate whether some
+    parameters should be randomized or not.
+
+    The method `gen_parameters` takes a speed in kilometers per hour
+    and returns a dictionary of noise parameters.
+
+    The global variables `GEAR_LOOKUP` and `RPM_LOOKUP`
+    are used to determine the gear and RPM for a given speed.
+
+    Example:
+        >>> car_noise_parameters = CarNoiseParameters(random_flag=True)
+        >>> parameters = car_noise_parameters.gen_parameters(speed_kph=100)
+        >>> parameters
+        {
+        "speed": 70,
+        "gear": 4,
+        "rpm": 1890.0,
+        "primary_filter": {
+            "order": 1,
+            "btype": "lowpass",
+            "cutoff_hz": 12.685320000000003,
+        },
+        "secondary_filter": {
+            "order": 2,
+            "btype": "lowpass",
+            "cutoff_hz": 276.22400000000005,
+        },
+        "bump": {"order": 1, "btype": "bandpass", "cutoff_hz": [34, 66.64000000000001]},
+        "dip_low": {"order": 1, "btype": "lowpass", "cutoff_hz": 90},
+        "dip_high": {"order": 1, "btype": "highpass", "cutoff_hz": 220.12494775682322},
+    }
+    """
+
+    GEAR_LOOKUP = {110: [6], 100: [5, 6], 85: [5], 75: [4, 5], 60: [4], 50: [3]}
+    RPM_LOOKUP = {6: 0.28, 5: 0.34, 4: 0.45, 3: 0.60}
+    REFERENCE_CONSTANT_DB = 30
+
+    def __init__(
+        self,
+        primary_filter_cutoff: float = 0.14,
+        primary_filter_constant_hz: float = 2.86,
+        secondary_filter_cutoff: float = 0.8,
+        secondary_filter_constant_hz: float = 200,
+        random_flag: bool = True,
+        random_seed: Optional[int] = None,
+    ) -> None:
+        """
+        Constructor takes a boolean flag to indicate whether
+        some parameters should be randomized or not.
+
+        It preset several speed independent parameters for the noise generation.
+
+        Args:
+            primary_filter_cutoff (float, optional): The speed dependent cutoff in Hz
+                for Primary Filter
+            primary_filter_constant_hz (float, optional): The constant cutoff in Hz
+                for Primary Filter
+            secondary_filter_cutoff (float, optional): The speed dependent cutoff in Hz
+                for Secondary Filter
+            secondary_filter_constant_hz (float, optional): The constant cutoff in Hz
+                for Secondary Filter
+            random_flag (bool, optional): Flag to indicate whether some parameters should
+                be randomized or not
+            random_seed (int, optional): Random seed to use for randomization
+
+        """
+
+        self.random_flag = random_flag
+
+        if self.random_flag and random_seed is not None:
+            np.random.seed(random_seed)
+
+        # .. randomization range for frequency multiplier
+        self.randomisationrange_freqmultiplier = (
+            np.arange(0.9, 1.1, 0.001) if self.random_flag else [1]
+        )
+
+        # primray filter .. fix at 6 dB per octave and a
+        # slight speed-dependent Hz + randomization
+        self.primary_filter = {
+            "speeddependence_cutoff_hzperkph": primary_filter_cutoff,
+            "constant_hz": primary_filter_constant_hz,
+        }
+
+        # .. secondary filter .. fix at 12 dB per octave and a
+        # slight speed-dependent Hz + randomization
+        self.secondary_filter = {
+            "speeddependence_cutoff_hzperkph": secondary_filter_cutoff,
+            "constant_hz": secondary_filter_constant_hz,
+        }
+
+        # bump
+        self.bump_lowerlimitforrandomization_db = 0 if self.random_flag else 6
+        self.bump_upperlimitforrandomization_db = 6
+
+    def set_new_randomisationrange_freqmultiplier(self):
+        """Set a new freqeuncy multiplier use in the
+        primary and secondary filters
+        """
+        self.randomisationrange_freqmultiplier = (
+            np.arange(0.9, 1.1, 0.001) if self.random_flag else [1]
+        )
+
+    def gen_parameters(self, speed_kph: float) -> Dict:
+        """
+        Generate a dictionary of noise parameters for a given speed.
+
+        Parameters
+        ----------
+        speed_kph : float
+            The speed of the car in kilometers per hour between 50 and 120 km/h.
+
+        Returns
+        -------
+        parameters : dict
+            A dictionary of noise parameters.
+        """
+
+        if not 50 <= speed_kph <= 120:
+            raise ValueError("Speed should be between 50 and 120 kph.")
+
+        gear = self._get_gear(speed_kph)
+        rpm = self._get_rpm(gear, speed_kph)
+
+        referencelevel_db = self.REFERENCE_CONSTANT_DB
+        if self.random_flag:
+            referencelevel_db += np.random.choice(np.arange(0, 3.1, 0.1))
+
+        engine_num_harmonics = (
+            25 if not self.random_flag else np.random.choice(np.arange(10, 41))
+        )
+
+        primary_filter = self._generate_primary_filter(speed_kph)
+        secondary_filter = self._generate_secondary_filter(speed_kph)
+        bump_filter = self._generate_bump_filter()
+        dip_filter_low, dip_filter_high = self._generate_dip_filter()
+
+        return {
+            "speed": float(speed_kph),
+            "gear": int(gear),
+            "reference_level_db": float(referencelevel_db),
+            "engine_num_harmonics": int(engine_num_harmonics),
+            "rpm": float(rpm),
+            "primary_filter": primary_filter,
+            "secondary_filter": secondary_filter,
+            "bump": bump_filter,
+            "dip_low": dip_filter_low,
+            "dip_high": dip_filter_high,
+        }
+
+    def _get_gear(self, speed_kph: float) -> int:
+        for speed, possible_gears in self.GEAR_LOOKUP.items():
+            if speed_kph >= speed:
+                return int(
+                    possible_gears[-1]
+                    if not self.random_flag
+                    else np.random.choice(possible_gears)
+                )
+        return 0
+
+    def _get_rpm(self, gear: int, speed_kph: float) -> float:
+        rpm = self.RPM_LOOKUP[gear] * speed_kph * 60
+        return rpm
+
+    def _generate_primary_filter(self, speed_kph: float) -> Dict:
+        return {
+            "order": 1,
+            "btype": "lowpass",
+            "cutoff_hz": (
+                self.primary_filter["speeddependence_cutoff_hzperkph"] * speed_kph
+                + self.primary_filter["constant_hz"]
+            )
+            * np.random.choice(self.randomisationrange_freqmultiplier),
+        }
+
+    def _generate_secondary_filter(self, speed_kph: float) -> Dict:
+        return {
+            "order": 2,
+            "btype": "lowpass",
+            "cutoff_hz": (
+                self.secondary_filter["speeddependence_cutoff_hzperkph"] * speed_kph
+                + self.secondary_filter["constant_hz"]
+            )
+            * np.random.choice(self.randomisationrange_freqmultiplier),
+        }
+
+    def _generate_bump_filter(self) -> Dict:
+        bumpfilter_order = 1 if not self.random_flag else np.random.choice([1, 2])
+        bumpfilter_f1_hz = (
+            30 if not self.random_flag else np.random.randint(20, 101, size=1)[0]
+        )
+        bumpfilter_f2_hz = (
+            60
+            if not self.random_flag
+            else bumpfilter_f1_hz * np.random.choice(np.arange(1.25, 2.01, 0.01))
+        )
+        filter_dict = {
+            "order": int(bumpfilter_order),
+            "btype": "bandpass",
+            "cutoff_hz": [int(bumpfilter_f1_hz), int(bumpfilter_f2_hz)],
+        }
+        return filter_dict
+
+    def _generate_dip_filter(self) -> Tuple[Dict, Dict]:
+        dipfilter_order = 2 if not self.random_flag else np.random.choice([1, 2])
+        dipfilter_f1_hz = (
+            200 if not self.random_flag else np.random.choice(range(30, 201, 10))
+        )
+        dipfilter_f2_hz = (
+            300 if not self.random_flag else dipfilter_f1_hz * np.random.uniform(2, 3)
+        )
+        filter_dict_low = {
+            "order": int(dipfilter_order),
+            "btype": "lowpass",
+            "cutoff_hz": int(dipfilter_f1_hz),
+        }
+        filter_dict_high = {
+            "order": int(dipfilter_order),
+            "btype": "highpass",
+            "cutoff_hz": int(dipfilter_f2_hz),
+        }
+
+        return filter_dict_low, filter_dict_high
diff --git a/clarity/utils/car_noise_simulator/carnoise_signal_generator.py b/clarity/utils/car_noise_simulator/carnoise_signal_generator.py
new file mode 100644
index 000000000..44907264b
--- /dev/null
+++ b/clarity/utils/car_noise_simulator/carnoise_signal_generator.py
@@ -0,0 +1,393 @@
+"""
+Class to generate the car noise signal.
+"""
+from typing import Dict, Tuple, Union
+
+import numpy as np
+from scipy.signal import butter, lfilter
+
+
+def _butter_filter(
+    order: int, cutoff_hz: Union[list, np.ndarray], btype: str, sample_rate: int
+) -> Tuple[np.ndarray, np.ndarray]:
+    """
+    Function that creates a butter filter
+    Args:
+        order (int): Order of the filter
+        cutoff_hz (list or np.ndarray): Cutoff frequency of the filter
+        btype (str): Type of the filter
+        sample_rate (int): Sampling frequency
+    Returns:
+        numer (np.ndarray): Numerator of the filter
+        denom (np.ndarray): Denominator of the filter
+    """
+    if isinstance(cutoff_hz, list):
+        cutoff_hz = np.array(cutoff_hz)
+    numer, denom = butter(
+        order, cutoff_hz / (sample_rate / 2.0), btype=btype, output="ba"
+    )
+    return numer, denom
+
+
+class CarNoiseSignalGenerator:
+    """
+    A class to generate car noise signal.
+
+    The constructor takes the sample_rate and duration of
+    the generated signals.
+
+    The method `generate_car_noise` takes parameters for the
+    noise and generates the signal. These parameters
+    can be generated by the CarNoiseParameters class.
+
+
+    Example:
+        >>> car_noise_parameters = CarNoiseParameters(random_flag=True)
+        >>> parameters = car_noise_parameters.gen_parameters(speed_kph=100)
+
+        >>> car_noise = CarNoiseGenerator(sample_rate=44100, duration_secs=5, random_flag=True)
+        >>> car_noise_signal = car_noise.generate_car_noise(parameters, 3, 0.5)
+    """
+
+    REFERENCE_CONSTANT_DB = 30
+    FINAl_MULTIPLIER = 1 / 2000
+
+    def __init__(
+        self,
+        sample_rate: int,
+        duration_secs: float,
+        random_flag: bool = True,
+    ):
+        """Constructor takes the sample_rate and duration of the generated signals.
+
+        Args:
+            sample_rate (int): Sample rate of the generated signal
+            duration_secs (float): Duration of the generated signal
+            random_flag (bool, optional): Flag to indicate whether add some randomness
+            to the output signals. Defaults to True.
+        """
+        self.sample_rate = sample_rate
+        self.duration_secs = duration_secs
+        self.random_flag = random_flag
+
+        self.sampleduration_secs = 1 / self.sample_rate
+        self.duration_samples = round(self.duration_secs) * self.sample_rate
+        self.timevector_ms = np.arange(self.duration_samples) * self.sampleduration_secs
+
+    def generate_car_noise(
+        self,
+        noise_parameters: dict,
+        number_noise_sources: int,
+        commonness_factor: float,
+    ) -> np.ndarray:
+        """
+        Method that generates the car noise signal.
+        It organizes the parameters and calls the methods to generate the independent parts.
+
+        Args:
+            noise_parameters (dict): Dictionary with the parameters for the noise
+                as generated by the CarNoiseParameters class.
+            number_noise_sources (int): Number of noise sources. First source is the engine noise.
+                Following sources are other noise sources.
+            commonness_factor (float): Commonness factor
+        Returns:
+            np.ndarray: Car noise signal
+        """
+
+        # .. reference level = no speed dependence  plus a small randomization
+        referencelevel_db = noise_parameters["reference_level_db"]
+
+        filter_numer_denom: Dict = {}
+        for filter_type in [
+            "primary_filter",
+            "secondary_filter",
+            "bump",
+            "dip_low",
+            "dip_high",
+        ]:
+            if filter_type in noise_parameters:
+                filter_numer_denom[filter_type] = {}
+                numer, denom = _butter_filter(
+                    **noise_parameters[filter_type], sample_rate=self.sample_rate
+                )
+                filter_numer_denom[filter_type]["numer"] = numer
+                filter_numer_denom[filter_type]["denom"] = denom
+            else:
+                raise ValueError(f"Parameter for filter {filter_type} are mandatory.")
+
+        # initialise waveform matrix
+        # global_noise = Main array storing the different sources
+        # extra_noise_for_coherence = Array to add some correlation between sources
+        car_noise = np.zeros((number_noise_sources + 1, self.duration_samples))
+        extra_noise_for_coherence = np.zeros((1, self.duration_samples))
+
+        # Generate Engine Noise
+        # stored in index 0 in source_noise
+        engine_noise = self.generate_engine_noise(
+            speed=noise_parameters["speed"],
+            rpm=noise_parameters["rpm"],
+            engine_num_harmonics=noise_parameters["engine_num_harmonics"],
+            reference_level_db=referencelevel_db,
+            primary_filter=filter_numer_denom["primary_filter"],
+            secondary_filter=filter_numer_denom["secondary_filter"],
+        )
+        car_noise[0, :] = engine_noise
+
+        # Generate Noise for N sources
+        for n in range(1, number_noise_sources + 2):
+            source_n_noise = self.generate_source_noise(
+                reference_level_db=referencelevel_db,
+                primary_filter=filter_numer_denom["primary_filter"],
+                secondary_filter=filter_numer_denom["secondary_filter"],
+                bump_filter=filter_numer_denom["bump"],
+                dip_low_filter=filter_numer_denom["dip_low"],
+                dip_high_filter=filter_numer_denom["dip_high"],
+            )
+
+            if n <= number_noise_sources:
+                car_noise[n, :] = source_n_noise
+            else:
+                extra_noise_for_coherence[0, :] = source_n_noise
+
+        # Add some correlation between sources
+        for n in range(1, number_noise_sources + 1):
+            car_noise[n, :] = self.apply_commonness(
+                car_noise[n, :], extra_noise_for_coherence[0, :], commonness_factor
+            )
+
+        return car_noise * self.FINAl_MULTIPLIER
+
+    def generate_source_noise(
+        self,
+        reference_level_db: float,
+        primary_filter: Dict[str, np.ndarray],
+        secondary_filter: Dict[str, np.ndarray],
+        bump_filter: Dict[str, np.ndarray],
+        dip_low_filter: Dict[str, np.ndarray],
+        dip_high_filter: Dict[str, np.ndarray],
+    ) -> np.ndarray:
+        """
+        Method that generates the noise of a single source.
+
+        Args:
+            reference_level_db (float): Reference level in dB
+            primary_filter (Dict[str, np.ndarray]): Primary filter
+            secondary_filter (Dict[str, np.ndarray]): Secondary filter
+            bump_filter (Dict[str, np.ndarray]): Bump filter
+            dip_low_filter (Dict[str, np.ndarray]): Low dip filter
+            dip_high_filter (Dict[str, np.ndarray]): High dip filter
+
+        Returns:
+            np.ndarray: Noise of a single source
+
+        """
+        lowpasslevel_db = (
+            reference_level_db + 35
+        )  # puts it slightly quieter than the tones
+        lowpassnoise_gaussianstd = 10 ** (lowpasslevel_db / 20)
+
+        source_noise = (
+            np.random.normal(size=self.duration_samples) * lowpassnoise_gaussianstd
+        )
+
+        source_noise = lfilter(
+            primary_filter["numer"], primary_filter["denom"], source_noise
+        )
+
+        # Add bump to the noise
+        # Get Bump noise parameters
+        lowpassnoise_gaussianstd, bump_gaussianstd = self.get_bump_params(
+            reference_level_db
+        )
+        bump_waveform = np.random.normal(size=self.duration_samples) * bump_gaussianstd
+        bump_waveform = lfilter(
+            bump_filter["numer"], bump_filter["denom"], bump_waveform
+        )
+
+        source_noise = source_noise + bump_waveform
+
+        # Add dip to the noise
+        dip_lowerpart = lfilter(
+            dip_low_filter["numer"], dip_low_filter["denom"], source_noise
+        )
+        dip_upperpart = lfilter(
+            dip_high_filter["numer"], dip_high_filter["denom"], source_noise
+        )
+        source_noise = dip_lowerpart + dip_upperpart
+
+        # final secondary filter
+        source_noise = lfilter(
+            secondary_filter["numer"], secondary_filter["denom"], source_noise
+        )
+
+        return source_noise
+
+    def generate_engine_noise(
+        self,
+        speed: float,
+        rpm: float,
+        reference_level_db: float,
+        engine_num_harmonics: int,
+        primary_filter: Dict[str, np.ndarray],
+        secondary_filter: Dict[str, np.ndarray],
+    ) -> np.ndarray:
+        """
+        Method that generates the noise of the engine.
+        Args:
+            speed (float): Speed of the car in km/h
+            rpm (float): RPM of the engine
+            reference_level_db (float): Reference level in dB
+            engine_num_harmonics (int): Number of harmonics of the engine
+            primary_filter (Dict[str, np.ndarray]): Primary filter
+            secondary_filter (Dict[str, np.ndarray]): Secondary filter
+
+        Returns:
+            np.ndarray: Noise of the engine
+
+        """
+        # Creating butter filters for engine noise
+
+        (
+            harmonic_complex_freqs_hz,
+            harmonic_complex_power_db,
+        ) = self.get_engine_params(speed, rpm, reference_level_db, engine_num_harmonics)
+
+        engine_noise = np.zeros(self.duration_samples)
+
+        for c in range(engine_num_harmonics):
+            component = {}
+            # set amplitude of tone
+            component["amplitude"] = 10 ** (harmonic_complex_power_db[c] / 20)
+            # set random phase
+            component["phase_rads"] = np.random.rand() * 2 * np.pi
+            # set angular frequency
+            component["angularfreq_rads"] = 2 * np.pi * harmonic_complex_freqs_hz[c]
+            # make :-)
+            component_waveform = component["amplitude"] * np.sin(
+                component["angularfreq_rads"] * self.timevector_ms
+                + component["phase_rads"]
+            )
+            # store
+            engine_noise += component_waveform
+
+        # Applies Primary and Secondary filters
+        engine_noise = lfilter(
+            primary_filter["numer"],
+            primary_filter["denom"],
+            engine_noise,
+        )
+        engine_noise = lfilter(
+            secondary_filter["numer"],
+            secondary_filter["denom"],
+            engine_noise,
+        )
+
+        return engine_noise
+
+    def get_bump_params(self, reference_level_db: float) -> Tuple[float, float]:
+        """
+        Method that gets the parameters of the bump noise
+        Args:
+            reference_level_db (float): Reference level in dB
+
+        Returns:
+            lowpass_noise_gaussian_std (float): Standard deviation of the low pass noise
+            bump_gaussian_std (float): Standard deviation of the bump noise
+        """
+        lowpasslevel_db = (
+            reference_level_db + 35
+        )  # puts it slightly quieter than the tones
+        lowpass_noise_gaussian_std = 10 ** (lowpasslevel_db / 20)
+
+        bump_level_db = lowpasslevel_db + np.random.choice(
+            np.arange(6 if not self.random_flag else 0, 6 + 1, 1)
+        )
+        bump_gaussian_std = 10 ** (bump_level_db / 20)
+
+        return lowpass_noise_gaussian_std, bump_gaussian_std
+
+    def get_engine_params(
+        self,
+        speed: float,
+        rpm: float,
+        reference_level_db: float,
+        engine_num_harmonics: int,
+    ) -> Tuple[np.ndarray, np.ndarray]:
+        """
+        Method that gets the parameters of the engine noise
+
+        Args:
+            speed (float): Speed of the car in km/h
+            rpm (float): RPM of the engine
+            reference_level_db (float): Reference level in dB
+            engine_num_harmonics (int): Number of harmonics of the engine
+
+        Returns:
+            harmoniccomplex_freqs_hz (np.ndarray): Frequency of the harmonic complex
+            harmoniccomplex_ntones (int): Number of tones of the harmonic complex
+            harmoniccomplex_power_db (np.ndarray): Power of the harmonic complex
+        """
+
+        harmoniccomplex_f0 = rpm / 60
+
+        harmoniccomplex_freqs_hz = harmoniccomplex_f0 * np.arange(
+            1, engine_num_harmonics + 1
+        )
+
+        tone_speeddependence_level_dbperkph = 2 * 0.067
+        toneconstant_db = -3
+
+        harmoniccomplex_power_db = np.zeros(engine_num_harmonics)
+
+        # Generating power levels for each component
+        for n in range(engine_num_harmonics):
+            harmoniccomplex_power_db[n] = (
+                reference_level_db
+                + toneconstant_db
+                + tone_speeddependence_level_dbperkph * speed
+                + np.random.choice(
+                    np.arange(
+                        0 if not self.random_flag else -2,
+                        0 if not self.random_flag else 2.1,
+                        0.1,
+                    )
+                )
+            )
+            if self.random_flag == 0:
+                if n + 1 in [5, 7, 13, 14, 15, 19, 21, 23]:
+                    harmoniccomplex_power_db[n] = -99
+            else:
+                if np.random.uniform(0, 1) < 0.333:
+                    harmoniccomplex_power_db[n] = -99
+
+        return (
+            harmoniccomplex_freqs_hz,
+            harmoniccomplex_power_db,
+        )
+
+    @staticmethod
+    def apply_commonness(
+        target_signal: np.ndarray,
+        coherence_signal: np.ndarray,
+        commonness_factor: float,
+    ) -> np.ndarray:
+        """Function to apply correlation between the target signal
+        using the coherence signal.
+
+        Args:
+            target_signal (np.ndarray): Target signal
+            coherence_signal (np.ndarray): Coherence signal
+            commonness_factor (float): Commonness factor
+
+        Returns:
+            target_signal (np.ndarray): Target signal with the coherence signal
+        """
+        target_signal = (
+            1 - commonness_factor
+        ) * target_signal + commonness_factor * coherence_signal
+
+        # correct amplitude for the sum of two gaussians
+        correctionfactor = 1 / np.sqrt(
+            (1 - commonness_factor) ** 2 + commonness_factor**2
+        )
+        return target_signal * correctionfactor
diff --git a/recipes/cad1/README.md b/recipes/cad1/README.md
index 423f93bae..1e98cfb27 100644
--- a/recipes/cad1/README.md
+++ b/recipes/cad1/README.md
@@ -21,7 +21,7 @@ The performance of each system on the validation set is reported below.
 
 ### Task 1 - Listening music via headphones
 
-TODO - add Table with results
+TODO - add Table with results.
 
 ### Task 2 - Listening music in a car wearing hearing aids
 
diff --git a/recipes/cad1/task2/baseline/README.md b/recipes/cad1/task2/baseline/README.md
new file mode 100644
index 000000000..696ffb285
--- /dev/null
+++ b/recipes/cad1/task2/baseline/README.md
@@ -0,0 +1,140 @@
+# The First Cadenza Challenge (CAD1) - Task 2: Listening music in a car
+
+Cadenza challenge code for the First Cadenza Challenge (CAD1) Task2.
+
+For more information please visit the [challenge website](https://cadenzachallenge.org/docs/cadenza1/cc1_intro).
+
+## 1. Data structure
+
+### 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,
+totalling 7000 songs with a distribution of 80% / 10% / 10% for `train`, `valid` and `test`.
+
+From FMA small:
+
+* Hip-Hop
+* Instrumental
+* International
+* Pop
+* Rock
+
+From MTG-Jamendo:
+
+* Classical
+* Orchestral
+
+The HRTFs data is based on the eBrIRD - ELOSPHERES binaural room impulse response database.
+
+To download the data, please visit [here](https://forms.gle/9L5ncYKe2YhD5c828).
+
+The data will download into a package file called `cadenza_cad1_task2_core.v1.0.tar.gz`.
+
+Unpack this package using
+
+```bash
+tar -xvzf cadenza_cad1_task2_core.v1.0.tar.gz
+```
+
+Once unpacked the directory structure will be as follows
+
+**cadenza_cad1_task2_core.v1.0** contains the training and validation data:
+
+```bash
+clarity_CPC2_data
+└── cadenza_data
+   └── cad1  # The hearing aid output signals
+        └── taks2
+            ├── audio
+            |   ├── eBrird  # HRTFs directory
+            |   └── music
+            |       ├── training
+            |       └── validation
+            ├── metadata  # Metadata
+            └── manifest  # Lists the package contents
+```
+
+### 1.2 Demo data
+
+To help you to start with the challenge, we provide a small subset of the data.
+The `demo_data` folder contains a single song and two listeners from the validation set.
+
+To use the demo data, simply download the package `cadenza_task2_data_demo.tar.tar.xz`
+from [here](https://drive.google.com/drive/folders/1On5Bv7Sd6zLZWfA76jdkM-FmGS61Mbi-?usp=share_link)
+and unpack it under `recipes/cad1/task2/`, i.e., one level above the baseline directory.
+Note that the `root.path` variable in `config.yaml` is already set to the demo data by default.
+
+To unpack the demo data, run:
+
+```bash
+tar -xvf cadenza_data_demo.tar.xz
+```
+
+## 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 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
+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.
+This attenuation prevents some clipping in the hearing aid output signal.
+
+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.
+
+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=/full/path/to/my/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` 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.
+
+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 HAAQI scores for each ear and the average of both will be generated in
+the `path.exp_folder`.
+
+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.
+
+**Baseline performance on the validation set will be updated soon.**
+
+## 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)
+* [3] 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)
+* [4] Bogdanov, D., Won, M., Tovstogan, P., Porter, A., & Serra, X. (2019). The MTG-Jamendo dataset for automatic music tagging. ICML.
diff --git a/recipes/cad1/task2/baseline/__init__.py b/recipes/cad1/task2/baseline/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad1/task2/baseline/audio_manager.py b/recipes/cad1/task2/baseline/audio_manager.py
new file mode 100644
index 000000000..8deccd69d
--- /dev/null
+++ b/recipes/cad1/task2/baseline/audio_manager.py
@@ -0,0 +1,118 @@
+"""A utility class for managing audio files."""
+
+import logging
+import warnings
+from pathlib import Path
+from typing import Dict, Tuple, Union
+
+import numpy as np
+import pyloudnorm as pyln
+from scipy.io import wavfile
+
+logger = logging.getLogger(__name__)
+
+
+class AudioManager:
+    """A utility class for managing audio files."""
+
+    def __init__(
+        self,
+        sample_rate: int = 44100,
+        output_audio_path: Union[str, Path] = "",
+        soft_clip: bool = False,
+    ):
+        """Initialize the AudioManager instance."""
+        self.audios_to_save: Dict[str, np.adarray] = {}
+        self.sample_rate = sample_rate
+        self.soft_clip = soft_clip
+        self.output_audio_path = Path(output_audio_path)
+        self.output_audio_path.mkdir(exist_ok=True, parents=True)
+        self.level_meter = pyln.Meter(self.sample_rate)
+
+    def add_audios_to_save(self, file_name: str, waveform: np.ndarray) -> None:
+        """Add a waveform to the list of audios to save.
+
+        Args:
+            file_name (str): The name of the track.
+            waveform (np.ndarray): The track to save.
+        """
+        self.audios_to_save[file_name] = waveform.copy()
+
+    def save_audios(self) -> None:
+        """Save the audios to the given path.
+
+        Args:
+            output_audio_path (str): The path to save the audios to.
+        """
+        for file_name, waveform in self.audios_to_save.items():
+            self._save_audio(file_name, waveform)
+
+    def _save_audio(self, file_name: str, waveform: np.ndarray) -> None:
+        """Save the audio to the given path.
+        It always save in Int16 format.
+
+        Args:
+            file_name (str): The name of the track.
+            waveform (np.ndarray): The track to save.
+            output_audio_path (str): The path to save the audio to.
+            sample_rate (int): The sample rate of the audio.
+        """
+        waveform = waveform.T if waveform.shape[0] == 2 else waveform
+
+        n_clipped, waveform = self.clip_audio(waveform)
+        if n_clipped > 0:
+            logger.warning(
+                f"Writing {self.output_audio_path / file_name}: {n_clipped} samples clipped"
+            )
+
+        waveform = (32768.0 * waveform).astype(np.int16)
+
+        wavfile.write(
+            self.output_audio_path / f"{file_name}.wav",
+            self.sample_rate,
+            waveform,
+        )
+
+    def clip_audio(
+        self, signal: np.ndarray, min_val: float = -1, max_val: float = 1
+    ) -> Tuple[int, np.ndarray]:
+        """Clip a WAV file to the given range.
+
+        Args:
+            signal (np.ndarray): The WAV file to clip.
+            min_val (float): The minimum value to clip to. Defaults to -1.
+            max_val (float): The maximum value to clip to. Defaults to 1.
+
+        Returns:
+            Tuple[int, np.ndarray]: The number of samples clipped and the clipped signal.
+        """
+        if self.soft_clip:
+            signal = np.tanh(signal)
+        n_clipped = np.sum(np.abs(signal) > 1.0)
+        return int(n_clipped), np.clip(signal, min_val, max_val)
+
+    def get_lufs_level(self, signal: np.ndarray) -> float:
+        """Get the LUFS level of the signal.
+
+        Args:
+            signal (np.ndarray): The signal to get the LUFS level of.
+
+        Returns:
+            float: The LUFS level of the signal.
+        """
+        return self.level_meter.integrated_loudness(signal)
+
+    def scale_to_lufs(self, signal: np.ndarray, target_lufs: float) -> np.ndarray:
+        """Scale the signal to the given LUFS level.
+
+        Args:
+            signal (np.ndarray): The signal to scale.
+            target_lufs (float): The target LUFS level.
+
+        Returns:
+            np.ndarray: The scaled signal.
+        """
+        current_lufs = self.get_lufs_level(signal)
+        with warnings.catch_warnings(record=True):
+            scaled_signal = pyln.normalize.loudness(signal, current_lufs, target_lufs).T
+        return scaled_signal
diff --git a/recipes/cad1/task2/baseline/baseline_utils.py b/recipes/cad1/task2/baseline/baseline_utils.py
new file mode 100644
index 000000000..7365ff708
--- /dev/null
+++ b/recipes/cad1/task2/baseline/baseline_utils.py
@@ -0,0 +1,122 @@
+"""Utility functions for the baseline model."""
+# pylint: disable=import-error
+
+import json
+import logging
+import warnings
+from pathlib import Path
+from typing import Optional, Tuple, Union
+
+import librosa
+import numpy as np
+import pandas as pd
+from omegaconf import DictConfig
+
+logger = logging.getLogger(__name__)
+
+
+def read_mp3(
+    file_path: Union[str, Path], sample_rate: Optional[int] = None
+) -> Tuple[np.ndarray, Optional[int]]:
+    """Read a MP3 file and return its signal.
+
+    Args:
+        file_path (str, Path): The path to the mp3 file.
+        sample_rate (int): The sampling frequency of the mp3 file.
+
+    Returns:
+        signal (np.ndarray): The signal of the mp3 file.
+        sample_rate (int): The sampling frequency of the mp3 file.
+    """
+
+    try:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            signal, sample_rate = librosa.load(
+                str(file_path),
+                sr=sample_rate,
+                mono=False,
+                res_type="kaiser_best",
+                dtype=np.float32,
+            )
+    except Exception as error:
+        raise ValueError from error
+
+    if signal.ndim == 1:
+        # If mono, duplicate to stereo
+        signal = np.stack([signal, signal], axis=0)
+
+    # Peak Normalization for cases when signal has
+    # absolute values greater than 1
+    if np.max(np.abs(signal)) > 1:
+        signal = signal / np.max(np.abs(signal))
+
+    return signal, sample_rate
+
+
+def load_hrtf(config: DictConfig) -> dict:
+    """Load the HRTF file.
+
+    Args:
+        config (DictConfig): A dictionary-like object containing various configuration
+            parameters for the evaluation. This includes the path to the HRTF files.
+
+    Returns:
+        hrtf_data (dict): A dictionary containing the HRTF data for the dataset.
+
+    """
+    with open(config.path.hrtf_file, "r", encoding="utf-8") as fp:
+        hrtf_data = json.load(fp)
+    return hrtf_data[config.evaluate.split]
+
+
+def load_listeners_and_scenes(config: DictConfig) -> Tuple[dict, dict, dict]:
+    """Load listener and scene data
+
+    Args:
+        config (DictConfig): A dictionary-like object containing various configuration
+            parameters for the evaluation. This includes the path to the scenes file,
+            the path to the listeners train file, and the path to the listeners valid file.
+
+    Returns:
+        Tuple[dict, dict, dict]: A tuple containing the scene data, the listener data and
+            the pair scenes-listeners.
+
+    """
+    # Load listener data
+    with open(config.path.scenes_file, "r", encoding="utf-8") as fp:
+        df_scenes = pd.read_json(fp, orient="index")
+
+    # Load audiograms and scence data for the corresponding split
+    if config.evaluate.split == "train":
+        with open(config.path.listeners_train_file, "r", encoding="utf-8") as fp:
+            listener_audiograms = json.load(fp)
+        scenes = df_scenes[df_scenes["split"] == "train"].to_dict("index")
+
+    elif config.evaluate.split == "valid":
+        with open(config.path.listeners_valid_file, "r", encoding="utf-8") as fp:
+            listener_audiograms = json.load(fp)
+        scenes = df_scenes[df_scenes["split"] == "valid"].to_dict("index")
+
+    with open(config.path.scenes_listeners_file, "r", encoding="utf-8") as fp:
+        scenes_listeners = json.load(fp)
+        scenes_listeners = {
+            k: v for k, v in scenes_listeners.items() if k in scenes.keys()
+        }
+
+    return scenes, listener_audiograms, scenes_listeners
+
+
+def make_scene_listener_list(scenes_listeners, small_test=False):
+    """Make the list of scene-listener pairing to process"""
+    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
diff --git a/recipes/cad1/task2/baseline/car_scene_acoustics.py b/recipes/cad1/task2/baseline/car_scene_acoustics.py
new file mode 100644
index 000000000..e9690a5b0
--- /dev/null
+++ b/recipes/cad1/task2/baseline/car_scene_acoustics.py
@@ -0,0 +1,398 @@
+"""A class for the car acoustic environment."""
+# pylint: disable=import-error
+# pylint: disable=too-many-instance-attributes
+
+import logging
+import warnings
+from pathlib import Path
+from typing import Optional
+
+import numpy as np
+import pyloudnorm as pyln
+from omegaconf import DictConfig
+from scipy.io import wavfile
+from scipy.signal import lfilter
+
+from clarity.enhancer.compressor import Compressor
+from clarity.enhancer.nalr import NALR
+from clarity.utils.car_noise_simulator.carnoise_signal_generator import (
+    CarNoiseSignalGenerator,
+)
+from recipes.cad1.task2.baseline.audio_manager import AudioManager
+
+logger = logging.getLogger(__name__)
+
+
+class CarSceneAcoustics:
+    """
+    A class for the car acoustic environment.
+
+    Constants:
+        ANECHOIC_HRTF_FOR_NOISE (dict): A dictionary containing the names of the anechoic BRIRs
+            for the following directions:
+                0 degrees: front
+                    - 000_left: The left channel of the BRIR for 0 degrees.
+                    - 000_right: The right channel of the BRIR for 0 degrees.
+                -90 degrees: left
+                    - m90_left: The left channel of the BRIR for -90 degrees.
+                    - m90_right: The right channel of the BRIR for -90 degrees.
+                90 degrees: right
+                    - p90_left: The left channel of the BRIR for 90 degrees.
+                    - p90_right: The right channel of the BRIR for 90 degrees.
+    """
+
+    ANECHOIC_HRTF_FOR_NOISE = {
+        "000_left": "HR36_E02_CH1_Left.wav",
+        "000_right": "HR36_E02_CH1_Right.wav",
+        "m90_left": "HR0_E02_CH1_Left.wav",
+        "m90_right": "HR0_E02_CH1_Right.wav",
+        "p90_left": "HR72_E02_CH1_Left.wav",
+        "p90_right": "HR72_E02_CH1_Right.wav",
+    }
+
+    def __init__(
+        self,
+        track_duration: int,
+        sample_rate: int,
+        hrtf_dir: str,
+        config_nalr: dict,
+        config_compressor: dict,
+        extend_noise: float = 0.2,
+    ):
+        """
+        Initializes the CarSceneAcoustics object.
+
+        Args:
+        track_duration (int): The duration of the audio track in seconds..
+        sample_rate (int): The sample rate of the audio in Hz.
+        hrtf_dir (str): The path to the directory containing the BRIR files.
+        config_nalr (dict): The configuration for the NALR enhancer.
+        config_compressor (dict): The configuration for the compressor.
+        extend_noise (float): The factor by which to extend the duration of the car
+            noise generated by the CarNoiseGenerator. Defaults to 0.2.
+            This is to prevent the car noise from being shorter than the audio track.
+        """
+
+        self.track_duration = track_duration * (1 + extend_noise)
+        self.sample_rate = sample_rate
+        self.hrtf_dir = hrtf_dir
+
+        self.preload_anechoic_hrtf(self.hrtf_dir)
+        self.enhancer = NALR(**config_nalr)
+        self.compressor = Compressor(**config_compressor)
+
+        self.carnoise = CarNoiseSignalGenerator(
+            duration_secs=self.track_duration,
+            sample_rate=self.sample_rate,
+        )
+        self.loudness_meter = pyln.Meter(self.sample_rate)
+
+    def preload_anechoic_hrtf(self, hrtf_dir: str) -> None:
+        """
+        Loads the Anechoic BRIRs from the eBrird database for the given directions.
+        Using the following directions:
+            0 degrees: front
+            -90 degrees: left
+            90 degrees: right
+
+        Args:
+            brird_dir (str): The path to the directory containing the BRIR files.
+        """
+        self.hrir_for_noise = {}
+        anechoic_hrtf_dir = Path(hrtf_dir) / "Anechoic" / "audio"
+
+        for key, item in self.ANECHOIC_HRTF_FOR_NOISE.items():
+            self.hrir_for_noise[key] = wavfile.read(anechoic_hrtf_dir / item)[1]
+
+    def apply_hearing_aid(
+        self, signal: np.ndarray, audiogram: np.ndarray, center_frequencies: np.ndarray
+    ) -> np.ndarray:
+        """
+        Applies the hearing aid:
+        It consists in NALR prescription and Compressor
+
+        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.
+
+        Returns:
+            np.ndarray: The enhanced audio signal.
+        """
+        nalr_fir, _ = self.enhancer.build(audiogram, center_frequencies)
+        signal = self.enhancer.apply(nalr_fir, signal)
+        signal, _, _ = self.compressor.process(signal)
+        return signal
+
+    def add_anechoic_hrtf_to_noise(self, noise_signal: np.ndarray) -> np.ndarray:
+        """
+        Adds the Anechoic HRTF to the noise signal.
+        Args:
+            noise_signal: A numpy array representing the different components
+                of the car noise signal.
+
+        Returns:
+            np.ndarray: The noise signal with the Anechoic HRTF applied.
+
+        """
+        # Apply Anechoic HRTF to the noise signal
+        # Engine first
+        out_left = lfilter(self.hrir_for_noise["000_left"], 1, noise_signal[0, :])
+        our_right = lfilter(self.hrir_for_noise["000_right"], 1, noise_signal[0, :])
+
+        # noise processing hardwired for 2 noises
+        out_left += lfilter(self.hrir_for_noise["m90_left"], 1, noise_signal[1, :])
+        our_right += lfilter(self.hrir_for_noise["m90_right"], 1, noise_signal[1, :])
+
+        # swap HRIR so this noise is on the other side
+        out_left += lfilter(self.hrir_for_noise["p90_left"], 1, noise_signal[2, :])
+        our_right += lfilter(self.hrir_for_noise["p90_right"], 1, noise_signal[2, :])
+
+        return np.stack([out_left, our_right], axis=0)
+
+    def get_car_noise(
+        self,
+        car_noise_params: dict,
+    ) -> np.ndarray:
+        """
+        Generates car noise.
+
+        Args:
+            car_noise_params (dict): Car Noise Parameters as generated by
+                Class CarNoiseParameterGenerator
+
+        Returns:
+            numpy.ndarray: A numpy array representing the different components
+                of the car noise signal
+
+        """
+        return self.carnoise.generate_car_noise(
+            noise_parameters=car_noise_params,
+            number_noise_sources=2,
+            commonness_factor=0,
+        )
+
+    def add_hrtf_to_stereo_signal(
+        self, signal: np.ndarray, hrir: dict, hrtf_type: str
+    ) -> np.ndarray:
+        """Add a head rotation transfer function using binaural room impulse
+            response (BRIR) from eBrird.
+
+        Args:
+            signal (np.ndarray): a numpy array of shape (2, n_samples) containing the
+                stereo audio signal.
+            hrir: a dictionary containing the HRIR (head-related impulse response) filenames.
+            hrtf_type: the type of HRTF to use. Can be either "Anechoic" or "Car".
+
+        Returns:
+            A numpy array of shape (2, n_samples) containing the stereo audio signal with the
+                BRIR added.
+
+        """
+        car_hrtf_path = Path(self.hrtf_dir) / hrtf_type / "audio"
+
+        # HRTF from left speaker
+        hr_ls03_ch1_left = wavfile.read(
+            car_hrtf_path / f"{hrir['left_speaker']['left_side']}.wav"
+        )[1]
+        hr_ls03_ch1_right = wavfile.read(
+            car_hrtf_path / f"{hrir['left_speaker']['right_side']}.wav"
+        )[1]
+
+        # HRTF from right speaker
+        hr_ls04_ch1_left = wavfile.read(
+            car_hrtf_path / f"{hrir['right_speaker']['left_side']}.wav"
+        )[1]
+        hr_ls04_ch1_right = wavfile.read(
+            car_hrtf_path / f"{hrir['right_speaker']['right_side']}.wav"
+        )[1]
+
+        # add the BRIRs to the signal
+        # Left Speaker (LS03)
+        out_left = lfilter(hr_ls03_ch1_left, 1, signal[0, :])
+        out_right = lfilter(hr_ls03_ch1_right, 1, signal[0, :])
+        # Right Speaker (LS04)
+        out_left += lfilter(hr_ls04_ch1_left, 1, signal[1, :])
+        out_right += lfilter(hr_ls04_ch1_right, 1, signal[1, :])
+
+        return np.stack([out_left, out_right], axis=0)
+
+    def scale_signal_to_snr(
+        self,
+        signal: np.ndarray,
+        reference_signal: np.ndarray = None,
+        snr: Optional[float] = 0,
+    ) -> np.ndarray:
+        """
+        Scales the target signal to the desired SNR.
+        We transpose channel because pylodnorm operates
+        on arrays with shape [n_samples, n_channels].
+
+        Args:
+            target_signal (np.ndarray): The target signal to scale.
+            reference_signal (np.ndarray): The reference signal.
+                If None, the reference signal is set to 0 LUFS.
+            snr (float): The desired SNR gain in dB.
+                If None, the target signal is scaled to the reference signal.
+
+        Returns:
+            np.ndarray: The scaled target signal.
+        """
+        # Ensure channels are in the correct dimension
+        if reference_signal.shape[0] < reference_signal.shape[1]:
+            reference_signal = reference_signal.T
+        if signal.shape[0] < signal.shape[1]:
+            signal = signal.T
+
+        ref_signal_lufs = (
+            0.0
+            if reference_signal is None
+            else self.loudness_meter.integrated_loudness(reference_signal)
+        )
+
+        signal_lufs = self.loudness_meter.integrated_loudness(signal)
+        target_lufs = ref_signal_lufs - snr
+
+        with warnings.catch_warnings(record=True):
+            normalised_signal = pyln.normalize.loudness(
+                signal, signal_lufs, target_lufs
+            )
+
+        # return to original shape
+        return normalised_signal.T
+
+    def equalise_level(
+        self, signal: np.ndarray, reference_signal: np.ndarray, max_level: float = 20
+    ) -> np.ndarray:
+        """
+        Equalises the level of the target signal to the reference signal.
+
+        Args:
+            signal (np.ndarray): The target signal to equalise.
+            reference_signal (np.ndarray): The reference signal.
+            max_level (float): The maximum level of the target signal.
+                This to prevent clipping.
+
+        Returns:
+            np.ndarray: The equalised target signal.
+        """
+        signal_lufs = self.loudness_meter.integrated_loudness(signal.T)
+        target_lufs = self.loudness_meter.integrated_loudness(reference_signal.T)
+        with warnings.catch_warnings(record=True):
+            return pyln.normalize.loudness(
+                signal, signal_lufs, min(target_lufs, max_level)
+            )
+
+    @staticmethod
+    def add_two_signals(signal1: np.ndarray, signal2: np.ndarray) -> np.ndarray:
+        """
+        Adds two signals together.
+
+        Args:
+            signal1 (np.ndarray): The first signal.
+            signal2 (np.ndarray): The second signal.
+
+        Returns:
+            np.ndarray: The sum of the two signals.
+        """
+        min_length = min(signal1.shape[1], signal2.shape[1])
+        return signal1[:, :min_length] + signal2[:, :min_length]
+
+    def apply_car_acoustics_to_signal(
+        self,
+        enh_signal: np.ndarray,
+        scene: dict,
+        listener: dict,
+        hrtf: dict,
+        audio_manager: AudioManager,
+        config: DictConfig,
+    ) -> np.ndarray:
+        """
+        Applies the car acoustics to the enhanced signal.
+
+        Args:
+            enh_signal (np.ndarray): The enhanced signal to apply the car acoustics to.
+            scene (dict): The scene dictionary with the acoustics parameters.
+            listener (dict): The listener dictionary with the audiograms.
+            hrtf (dict): A dictionary containing the head-related transfer functions (HRTFs)
+                for the listener being evaluated. This includes the left and right HRTFs for
+                the car and the anechoic room.
+            audio_manager (AudioManager): The audio manager object.
+            config (DictConfig): The config object.
+
+        Returns:
+            np.ndarray: The enhanced signal with the car acoustics applied.
+            np.ndarray: The reference signal normalised to enhanced level.
+        """
+
+        # 1. Generates car noise and adds anechoic HRTFs to the car noise
+        # car_noise_anechoic = car_noise + anechoic HRTF
+
+        car_noise = self.get_car_noise(scene["car_noise_parameters"])
+        car_noise_anechoic = self.add_anechoic_hrtf_to_noise(car_noise)
+
+        if config.evaluate.save_intermediate_wavs:
+            audio_manager.add_audios_to_save("car_noise_anechoic", car_noise_anechoic)
+
+        # 2. Add HRTFs to enhanced signal
+        # processed_signal = enh_signal + car HRTF
+
+        processed_signal = self.add_hrtf_to_stereo_signal(
+            enh_signal, hrtf["car"], "Car"
+        )
+
+        if config.evaluate.save_intermediate_wavs:
+            audio_manager.add_audios_to_save("enh_signal_hrtf", processed_signal)
+
+        # 3. Scale noise to target SNR
+        # car_noise_anechoic = car_noise_anechoic * scale_factor
+
+        car_noise_anechoic = self.scale_signal_to_snr(
+            signal=car_noise_anechoic,
+            reference_signal=processed_signal,
+            snr=float(scene["snr"]),
+        )
+
+        if config.evaluate.save_intermediate_wavs:
+            audio_manager.add_audios_to_save(
+                "car_noise_anechoic_scaled", car_noise_anechoic
+            )
+
+        # 4. Add the scaled anechoic car noise to the enhanced signal
+        # processed_signal = (enh_signal * car HRTF) + (car_noise * Anechoic HRTF) * scale_factor
+        processed_signal = self.add_two_signals(processed_signal, car_noise_anechoic)
+
+        if config.evaluate.save_intermediate_wavs:
+            audio_manager.add_audios_to_save(
+                "enh_signal_hrtf_plus_car_noise_anechoic", processed_signal
+            )
+
+        # 5. Apply Hearing Aid to Left and Right channels and join them
+        processed_signal_left = self.apply_hearing_aid(
+            processed_signal[0, :],
+            np.array(listener["audiogram_levels_l"]),
+            np.array(listener["audiogram_cfs"]),
+        )
+
+        processed_signal_right = self.apply_hearing_aid(
+            processed_signal[1, :],
+            np.array(listener["audiogram_levels_r"]),
+            np.array(listener["audiogram_cfs"]),
+        )
+
+        processed_signal = np.stack(
+            [processed_signal_left, processed_signal_right], axis=0
+        )
+
+        # processed_signal = np.clip(processed_signal, -1.0, 1.0)
+        n_clipped, processed_signal = audio_manager.clip_audio(processed_signal)
+
+        if n_clipped > 0:
+            logger.warning(
+                f"Scene {scene['scene']}: {n_clipped} samples clipped in evaluation signal."
+            )
+
+        audio_manager.add_audios_to_save("ha_processed_signal", processed_signal)
+
+        return processed_signal
diff --git a/recipes/cad1/task2/baseline/config.yaml b/recipes/cad1/task2/baseline/config.yaml
new file mode 100644
index 000000000..36a5ceb2f
--- /dev/null
+++ b/recipes/cad1/task2/baseline/config.yaml
@@ -0,0 +1,44 @@
+path:
+  root: ../../cadenza_task2_data_demo/cad1/task2
+  audio_dir: ${path.root}/audio
+  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
+  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
+
+nalr:
+  nfir: 220
+  fs: ${sample_rate}
+
+compressor:
+  threshold: 0.7
+  attenuation: 0.1
+  attack: 5
+  release: 20
+  rms_buffer_size: 0.064
+
+soft_clip: False
+
+enhance:
+  average_level: -14   # Average level according Spotify's levels
+  min_level: -19
+
+evaluate:
+  set_random_seed: True
+  small_test: False
+  save_intermediate_wavs: False
+  split: valid # train, valid
+  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/cad1/task2/baseline/enhance.py b/recipes/cad1/task2/baseline/enhance.py
new file mode 100644
index 000000000..48c6effa1
--- /dev/null
+++ b/recipes/cad1/task2/baseline/enhance.py
@@ -0,0 +1,148 @@
+""" Run the dummy enhancement. """
+# pylint: disable=too-many-locals
+# pylint: disable=import-error
+
+import logging
+import warnings
+from pathlib import Path
+from typing import Tuple
+
+import hydra
+import numpy as np
+import pyloudnorm as pyln
+from omegaconf import DictConfig
+from scipy.io import wavfile
+from tqdm import tqdm
+
+from recipes.cad1.task2.baseline.baseline_utils import (
+    make_scene_listener_list,
+    read_mp3,
+)
+from recipes.cad1.task2.baseline.evaluate import load_listeners_and_scenes
+
+logger = logging.getLogger(__name__)
+
+
+def compute_average_hearing_loss(listener: dict) -> float:
+    """
+    Compute the average hearing loss of a listener.
+
+    Args:
+        listener (dict): The audiogram of the listener.
+
+    Returns:
+        average_hearing_loss (float): The average hearing loss of the listener.
+
+    """
+    cfs = [500, 1000, 2000, 4000]
+    left_loss = [
+        listener["audiogram_levels_l"][i]
+        for i in range(len(listener["audiogram_cfs"]))
+        if listener["audiogram_cfs"][i] in cfs
+    ]
+    right_loss = [
+        listener["audiogram_levels_r"][i]
+        for i in range(len(listener["audiogram_cfs"]))
+        if listener["audiogram_cfs"][i] in cfs
+    ]
+    return max(float(np.mean(left_loss)), float(np.mean(right_loss)))
+
+
+def enhance_song(
+    waveform: np.ndarray,
+    listener_audiograms: dict,
+    config: DictConfig,
+) -> Tuple[np.ndarray, np.ndarray]:
+    """
+    Enhance a single song for a listener.
+
+    Baseline enhancement returns the signal with a loudness
+    of -14 LUFS if the average hearing loss is below 50 dB HL,
+    and -11 LUFS otherwise.
+
+    Args:
+        waveform (np.ndarray): The waveform of the song.
+        listener_audiograms (dict): The audiograms of the listener.
+        config (dict): Dictionary of configuration options for enhancing music.
+
+    Returns:
+        out_left (np.ndarray): The enhanced left channel.
+        out_right (np.ndarray): The enhanced right channel.
+
+    """
+    meter = pyln.Meter(config.sample_rate)
+    original_loudness = meter.integrated_loudness(waveform.T)
+
+    average_hearing_loss = compute_average_hearing_loss(listener_audiograms)
+
+    extra_reduction = (
+        (average_hearing_loss - 50) / 5 if (average_hearing_loss - 50) / 5 > 0 else 0
+    )
+    target_level = (
+        config.enhance.min_level - extra_reduction
+        if average_hearing_loss >= 50
+        else config.enhance.average_level
+    )
+
+    with warnings.catch_warnings(record=True):
+        if original_loudness > target_level:
+            waveform = pyln.normalize.loudness(
+                waveform.T, original_loudness, target_level
+            ).T
+
+    out_left = waveform[0, :]
+    out_right = waveform[1, :]
+
+    return out_left, out_right
+
+
+@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.
+    """
+    enhanced_folder = Path("enhanced_signals") / config.evaluate.split
+    enhanced_folder.mkdir(parents=True, exist_ok=True)
+
+    # Load scenes and listeners depending on config.evaluate.split
+    scenes, listener_audiograms, scenes_listeners = load_listeners_and_scenes(config)
+    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
+    ]
+
+    for scene_id, listener_id in tqdm(scene_listener_pairs):
+        current_scene = scenes[scene_id]
+        listener = listener_audiograms[listener_id]
+
+        song_path = Path(config.path.music_dir) / f"{current_scene['song_path']}"
+
+        # Read song
+        song_waveform, _ = read_mp3(song_path, config.sample_rate)
+        out_l, out_r = enhance_song(
+            waveform=song_waveform, listener_audiograms=listener, config=config
+        )
+
+        enhanced = np.stack([out_l, out_r], axis=1)
+        filename = f"{listener['name']}_{current_scene['song']}.wav"
+
+        # Clip and save
+        if config.soft_clip:
+            enhanced = np.tanh(enhanced)
+        n_clipped = np.sum(np.abs(enhanced) > 1.0)
+        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 / filename, config.sample_rate, signal_16)
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    enhance()
diff --git a/recipes/cad1/task2/baseline/evaluate.py b/recipes/cad1/task2/baseline/evaluate.py
new file mode 100644
index 000000000..97d805db9
--- /dev/null
+++ b/recipes/cad1/task2/baseline/evaluate.py
@@ -0,0 +1,294 @@
+"""Evaluate the enhanced signals using the HAAQI metric."""
+# pylint: disable=too-many-locals
+# pylint: disable=import-error
+
+import csv
+import hashlib
+import logging
+from pathlib import Path
+from typing import Tuple
+
+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 recipes.cad1.task2.baseline.audio_manager import AudioManager
+from recipes.cad1.task2.baseline.baseline_utils import (
+    load_hrtf,
+    load_listeners_and_scenes,
+    make_scene_listener_list,
+    read_mp3,
+)
+from recipes.cad1.task2.baseline.car_scene_acoustics import CarSceneAcoustics
+
+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(
+                [
+                    "song",
+                    "listener",
+                    "score",
+                    "haaqi_left",
+                    "haaqi_right",
+                ]
+            )
+
+    def add_result(
+        self,
+        scene: str,
+        song: 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.
+            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,
+                    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'
+    `.hexdigest` of the scene itself.
+    """
+    scene_encoded = hashlib.md5(scene.encode("utf-8")).hexdigest()
+    scene_md5 = int(scene_encoded, 16) % (10**8)
+    np.random.seed(scene_md5)
+
+
+# pylint: disable=too-many-arguments
+def evaluate_scene(
+    ref_signal: np.ndarray,
+    enh_signal: np.ndarray,
+    sample_rate: int,
+    scene_id: str,
+    current_scene: dict,
+    listener_audiogram: dict,
+    car_scene_acoustic: CarSceneAcoustics,
+    hrtf: dict,
+    config: DictConfig,
+) -> Tuple[float, float]:
+    """Evaluate a single scene and return HAAQI scores for left and right ears
+
+    Args:
+        ref_signal (np.ndarray): A numpy array of shape (2, n_samples)
+            containing the reference signal.
+        enh_signal (np.ndarray): A numpy array of shape (2, n_samples)
+            containing the enhanced signal.
+        sample_rate (int): The sampling frequency of the reference and enhanced signals.
+        scene_id (str): A string identifier for the scene being evaluated.
+        current_scene (dict): A dictionary containing information about the scene being evaluated,
+            including the song ID, the listener ID, the car noise type, and the split.
+        listener_audiogram (dict): A dictionary containing the listener's audiogram data,
+            including the center frequencies (cfs) and the hearing levels for both ears
+            (audiogram_levels_l and audiogram_levels_r).
+        car_scene_acoustic (CarSceneAcoustics): An instance of the CarSceneAcoustics class,
+            which is used to generate car noise and add binaural room impulse responses (BRIRs)
+            to the enhanced signal.
+        hrtf (dict): A dictionary containing the head-related transfer functions (HRTFs)
+            for the listener being evaluated. This includes the left and right HRTFs for
+            the car and the anechoic room.
+        config (DictConfig): A dictionary-like object containing various configuration
+            parameters for the evaluation. This includes the path to the enhanced signal folder,
+            the path to the music directory, and a flag indicating whether to set a random seed.
+
+    Returns:
+        Tuple[float, float]: A tuple containing the HAAQI scores for the left and right ears.
+
+    """
+    audio_manager = AudioManager(
+        output_audio_path=Path("evaluation_signals")
+        / f"{listener_audiogram['name']} / {current_scene['song']}",
+        sample_rate=sample_rate,
+        soft_clip=config.soft_clip,
+    )
+
+    if config.evaluate.set_random_seed:
+        set_scene_seed(scene_id)
+
+    # Applies the Car Acoustics to the enhanced signal, i.e., the speakers output
+
+    processed_signal = car_scene_acoustic.apply_car_acoustics_to_signal(
+        enh_signal,
+        current_scene,
+        listener_audiogram,
+        hrtf,
+        audio_manager,
+        config,
+    )
+
+    # Normalise reference signal level to ha_processed_signal level
+    # ref_signal = ref_signal * scale_factor
+    # Following Spotify standard, Max level is -11 LUFS to avoid clipping
+    # https://artists.spotify.com/en/help/article/loudness-normalization
+    ref_signal = car_scene_acoustic.add_hrtf_to_stereo_signal(
+        enh_signal, hrtf["anechoic"], "Anechoic"
+    )
+    if config.evaluate.save_intermediate_wavs:
+        audio_manager.add_audios_to_save("ref_signal_anechoic", ref_signal)
+
+    ref_signal = car_scene_acoustic.equalise_level(
+        signal=ref_signal, reference_signal=processed_signal, max_level=-14
+    )
+    audio_manager.add_audios_to_save("ref_signal_for_eval", ref_signal)
+
+    audio_manager.save_audios()
+
+    # Compute HAAQI scores
+    aq_score_l = compute_haaqi(
+        processed_signal[0, :],
+        ref_signal[0, :],
+        np.array(listener_audiogram["audiogram_levels_l"]),
+        np.array(listener_audiogram["audiogram_cfs"]),
+        sample_rate,
+    )
+    aq_score_r = compute_haaqi(
+        processed_signal[1, :],
+        ref_signal[1, :],
+        np.array(listener_audiogram["audiogram_levels_r"]),
+        np.array(listener_audiogram["audiogram_cfs"]),
+        sample_rate,
+    )
+    return aq_score_l, aq_score_r
+
+
+@hydra.main(config_path="", config_name="config")
+def run_calculate_audio_quality(config: DictConfig) -> None:
+    """Evaluate the enhanced signals using the HAAQI metric."""
+
+    # Load scenes and listeners depending on config.evaluate.split
+    scenes, listener_audiograms, scenes_listeners = load_listeners_and_scenes(config)
+    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
+    ]
+
+    hrtfs = load_hrtf(config)
+
+    enhanced_folder = Path("enhanced_signals")
+    logger.info(f"Evaluating from {enhanced_folder} directory")
+
+    results_file = ResultsFile(
+        f"scores_{config.evaluate.batch}-{config.evaluate.batch_size}.csv"
+    )
+    results_file.write_header()
+
+    # Initialize acoustic scene model
+    car_scene_acoustic = CarSceneAcoustics(
+        track_duration=30,
+        sample_rate=config.sample_rate,
+        hrtf_dir=config.path.hrtf_dir,
+        config_nalr=config.nalr,
+        config_compressor=config.compressor,
+        extend_noise=0.2,
+    )
+
+    # Iterate over scenes
+    for scene_id, listener_id in tqdm(scene_listener_pairs):
+        current_scene = scenes[scene_id]
+
+        # Retrieve audiograms
+        listener = listener_audiograms[listener_id]
+
+        # Retrieve HRTF according to the listener's head orientation
+        hrtf_scene = hrtfs[str(current_scene["hr"])]
+
+        # Load reference signal
+        reference_song_path = (
+            Path(config.path.music_dir) / f"{current_scene['song_path']}"
+        )
+        # Read MP3 reference signal using librosa
+        reference_signal, _ = read_mp3(
+            reference_song_path.as_posix(), sample_rate=config.sample_rate
+        )
+
+        # Load enhanced signal
+        enhanced_folder = Path("enhanced_signals") / config.evaluate.split
+        enhanced_song_id = f"{listener['name']}_{current_scene['song']}"
+        enhanced_song_path = enhanced_folder / f"{enhanced_song_id}.wav"
+
+        # 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
+
+        # Evaluate scene
+        aq_score_l, aq_score_r = evaluate_scene(
+            reference_signal,
+            enhanced_signal.T,
+            config.sample_rate,
+            scene_id,
+            current_scene,
+            listener,
+            car_scene_acoustic,
+            hrtf_scene,
+            config,
+        )
+
+        # Compute combined score and save
+        score = np.mean([aq_score_r, aq_score_l])
+        results_file.add_result(
+            scene_id,
+            current_scene["song"],
+            listener_id,
+            score=float(score),
+            haaqi_left=aq_score_l,
+            haaqi_right=aq_score_r,
+        )
+
+
+# pylint: disable=no-value-for-parameter
+if __name__ == "__main__":
+    run_calculate_audio_quality()
diff --git a/recipes/cad1/task2/baseline/merge_batches_results.py b/recipes/cad1/task2/baseline/merge_batches_results.py
new file mode 100644
index 000000000..d5803f7ef
--- /dev/null
+++ b/recipes/cad1/task2/baseline/merge_batches_results.py
@@ -0,0 +1,25 @@
+"""Join batches scores into a single file."""
+
+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.
+
+    """
+    batches_results = []
+    for batch in range(config.evaluate.batch_size):
+        batches_results.append(
+            pd.read_csv(f"scores_{batch}-{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/cad1/task2/data_preparation/__init__.py b/recipes/cad1/task2/data_preparation/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad1/task2/data_preparation/build_scene_metadata.py b/recipes/cad1/task2/data_preparation/build_scene_metadata.py
new file mode 100644
index 000000000..a9ee80375
--- /dev/null
+++ b/recipes/cad1/task2/data_preparation/build_scene_metadata.py
@@ -0,0 +1,183 @@
+"""Module to Generate the metadata for the scenes in the CAD-1 Task-2 challenge."""
+# pylint: disable=import-error
+
+import json
+import logging
+from typing import Any, Dict
+
+import hydra
+import numpy as np
+from omegaconf import DictConfig
+
+from clarity.utils.car_noise_simulator.carnoise_parameters_generator import (
+    CarNoiseParametersGenerator,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def set_seed(seed: int) -> None:
+    """Set the seed for the random number generator."""
+    np.random.seed(seed)
+
+
+def get_random_head_rotation(input_dict: dict) -> Any:
+    """
+    Selects a random head rotation degree.
+
+    Args:
+        input_dict (dict): A dictionary where keys are bird IDs and values are bird data.
+
+    Returns:
+        A random item from the input dictionary.
+    """
+    return float(np.random.choice(list(input_dict.keys()), size=1, replace=False)[0])
+
+
+def get_random_car_params(min_speed: int = 50, max_speed: int = 120) -> Dict:
+    """
+    Returns a dictionary with the parameters for a car noise.
+    These parameters are generated randomly, based on the car speed.
+    The parameters are to be used by the CarNoiseSignalGeneration class
+
+    Parameters:
+        - min_speed (int): The minimum speed that can be returned (default 50).
+        - max_speed (int): The maximum speed that can be returned (default 120).
+
+    Returns:
+        A dictionary containing the parameters needed by the CarNoiseSignalGeneration class.
+    """
+    speed = np.random.randint(min_speed, max_speed)
+    car_params = CarNoiseParametersGenerator().gen_parameters(speed)
+    return car_params
+
+
+def read_json(path_file) -> Dict:
+    """Function the read a json file and return the data as a dictionary
+    or only the keys if ```return_keys``` is True.
+
+    Args:
+        path_file (str): Path to the json file.
+    """
+
+    with open(path_file, "r", encoding="utf-8") as file:
+        json_data = json.load(file)
+    return json_data
+
+
+def get_random_snr(min_snr, max_snr, round_to=4) -> float:
+    """Function to get a random SNR value.
+
+    Args:
+        min_snr (float): The minimum SNR value.
+        max_snr (float): The maximum SNR value.
+        round_to (int): The number of decimals to round the SNR value to.
+
+    Returns:
+        float: A random SNR value.
+    """
+
+    return float(np.round(np.random.uniform(min_snr, max_snr, 1), round_to))
+
+
+@hydra.main(config_path=".", config_name="config")
+def run(cfg: DictConfig) -> None:
+    """Main function to generate the metadata for the scenes in the CAD-1 Task-2 challenge.
+
+    This function relies on a random seed to generate the metadata.
+    The seed is set to 2023 by default and it should always be present.
+    This ensures all participant have access to the same initial scenes.
+    However, participants can augment the number of scenes by adding more seeds
+    in the config file. Note the first seed must always be 2023 and cannot be change.
+
+    Args:
+        cfg (DictConfig): The configuration object.
+    """
+
+    logger.info("Generating metadata")
+
+    assert (
+        cfg.seed[0] == 2023
+    ), f"First training seed must be 2023, {cfg.valid_seed} was provided"
+
+    assert (
+        cfg.valid_seed == 2023
+    ), f"Validation seed should be 2023, {cfg.valid_seed} was provided"
+
+    # read metadata in json format
+    train_songs = read_json(cfg.path.train_music_file)
+    valid_songs = read_json(cfg.path.valid_music_file)
+    train_listeners = read_json(cfg.path.listeners_train_file)
+    valid_listeners = read_json(cfg.path.listeners_valid_file)
+    brir = read_json(cfg.path.brir_file)
+
+    # Start generating scenes for training
+    train_scenes: Dict = {}
+    valid_scenes: Dict = {}
+    scene_listener: Dict[str, list] = {}
+
+    logger.info("... training metadata")
+    scene_id = 100000
+    for seed in cfg.seed:
+        logger.info(f"...... seed {seed}")
+        set_seed(seed)
+        songs_shuffled = np.random.permutation(list(train_songs.keys()))
+        for song in songs_shuffled:
+            train_scenes[f"S{scene_id:06d}"] = {
+                "scene": f"S{scene_id:06d}",
+                "song": song,
+                "song_path": train_songs[song]["path"],
+                "hr": get_random_head_rotation(brir["train"]),
+                "car_noise_parameters": get_random_car_params(
+                    min_speed=50, max_speed=120
+                ),
+                "snr": get_random_snr(0, 15.0),
+                "split": "train",
+            }
+            scene_id += 1
+
+    # Start generating scenes for validation
+    logger.info("... validation metadata with seed 2023")
+
+    seed = cfg.valid_seed
+    set_seed(seed)
+    songs_shuffled = np.random.permutation(list(valid_songs.keys()))
+    scene_id = 500000
+
+    for song in songs_shuffled:
+        valid_scenes[f"S{scene_id:06d}"] = {
+            "scene": f"S{scene_id:06d}",
+            "song": song,
+            "song_path": valid_songs[song]["path"],
+            "hr": get_random_head_rotation(brir["valid"]),
+            "car_noise_parameters": get_random_car_params(min_speed=50, max_speed=120),
+            "snr": get_random_snr(0, 15.0),
+            "split": "valid",
+        }
+        scene_id += 1
+
+    for scene_id in train_scenes:
+        listener = np.random.choice(list(train_listeners.keys()), 1, replace=False)[0]
+        if f"{scene_id}" not in scene_listener:
+            scene_listener[f"{scene_id}"] = []
+        scene_listener[f"{scene_id}"].append(listener)
+
+    for scene_id in valid_scenes:
+        listener = np.random.choice(list(valid_listeners.keys()), 1, replace=False)[0]
+        if f"{scene_id}" not in scene_listener:
+            scene_listener[f"{scene_id}"] = []
+        scene_listener[f"{scene_id}"].append(listener)
+
+    logger.info(f"Saving scenes metadata in {cfg.path.metadata_dir}")
+    with open(cfg.path.scenes_file, "w", encoding="utf-8") as file:
+        json.dump({**train_scenes, **valid_scenes}, file, indent=4)
+
+    with open(cfg.path.scenes_listeners_file, "w", encoding="utf-8") as file:
+        json.dump(scene_listener, file, indent=4)
+
+    logger.info("Done")
+
+
+# pylint: disable=no-value-for-parameter
+if __name__ == "__main__":
+    run()
diff --git a/recipes/cad1/task2/data_preparation/config.yaml b/recipes/cad1/task2/data_preparation/config.yaml
new file mode 100644
index 000000000..b32302149
--- /dev/null
+++ b/recipes/cad1/task2/data_preparation/config.yaml
@@ -0,0 +1,21 @@
+path:
+  root: ./
+  metadata_dir: ${path.root}/metadata
+  train_music_file: ${path.metadata_dir}/music.train.json
+  valid_music_file: ${path.metadata_dir}/music.valid.json
+  listeners_train_file: ${path.metadata_dir}/listeners.train.json
+  listeners_valid_file: ${path.metadata_dir}/listeners.valid.json
+  brir_file: ${path.metadata_dir}/eBrird_BRIR.json
+  scenes_file: ${path.metadata_dir}/scenes.json
+  scenes_listeners_file: ${path.metadata_dir}/scenes_listeners.json
+
+# Every seed value generates 8000 new samples.
+# Seed 2023 must never be removed.
+# If you want to add more seeds, add them to the list.
+# eg., seed: [2023, 2024, 2025]
+seed: [2023]
+valid_seed: 2023
+
+hydra:
+  run:
+    dir: .


Test patch:
diff --git a/tests/recipes/cad1/task1/test_enhance.py b/tests/recipes/cad1/task1/test_enhance.py
index 80aa13ca9..ef4af5aaf 100644
--- a/tests/recipes/cad1/task1/test_enhance.py
+++ b/tests/recipes/cad1/task1/test_enhance.py
@@ -19,7 +19,7 @@
 )
 
 BASE_DIR = Path.cwd()
-RESOURCES = BASE_DIR / "tests" / "resources"
+RESOURCES = BASE_DIR / "tests" / "resources" / "recipes" / "cad1" / "task1"
 
 
 def test_map_to_dict():
@@ -64,8 +64,8 @@ def test_decompose_signal():
         RESOURCES / "test_enhance.test_decompose_signal.npy",
         allow_pickle=True,
     )[()]
-    for key in output.keys():
-        np.testing.assert_array_almost_equal(output[key], expected_results[key])
+    for key, item in output.items():
+        np.testing.assert_array_almost_equal(item, expected_results[key])
 
 
 def test_apply_baseline_ha():
@@ -118,8 +118,8 @@ def test_process_stems_for_listener():
         RESOURCES / "test_enhance.test_process_stems_for_listener.npy",
         allow_pickle=True,
     )[()]
-    for key in output.keys():
-        np.testing.assert_array_almost_equal(output[key], expected_results[key])
+    for key, item in output.items():
+        np.testing.assert_array_almost_equal(item, expected_results[key])
 
 
 def test_separate_sources():
diff --git a/tests/recipes/cad1/task2/__init__.py b/tests/recipes/cad1/task2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/recipes/cad1/task2/test_audio_manager.py b/tests/recipes/cad1/task2/test_audio_manager.py
new file mode 100644
index 000000000..e78b4aa0f
--- /dev/null
+++ b/tests/recipes/cad1/task2/test_audio_manager.py
@@ -0,0 +1,33 @@
+"""Test for AudioManager module"""
+# pylint: disable=import-error
+
+from pathlib import Path
+
+import numpy as np
+from scipy.io import wavfile
+
+from recipes.cad1.task2.baseline.audio_manager import AudioManager
+
+
+def test_save_audios(tmp_path):
+    """Test save_audios method."""
+    np.random.seed(42)
+    # Initialize an audio manager with temporary directory as output audio path
+    audio_manager = AudioManager(output_audio_path=tmp_path.as_posix())
+
+    # Create sample audio data
+    audio_data = np.random.randn(2, 44100)
+
+    # Add audio data to audio manager
+    audio_manager.add_audios_to_save("test_audio", audio_data)
+
+    # Save audio
+    audio_manager.save_audios()
+
+    # Check if audio file was saved
+    audio_file = Path(tmp_path) / "test_audio.wav"
+    assert audio_file.is_file()
+
+    # Check if audio data was saved correctly
+    sample_rate, _ = wavfile.read(audio_file)
+    assert sample_rate == audio_manager.sample_rate
diff --git a/tests/recipes/cad1/task2/test_baseline_utils.py b/tests/recipes/cad1/task2/test_baseline_utils.py
new file mode 100644
index 000000000..2ac5220ee
--- /dev/null
+++ b/tests/recipes/cad1/task2/test_baseline_utils.py
@@ -0,0 +1,43 @@
+"""Test for baseline_utils.py"""
+# pylint: disable=import-error
+
+from pathlib import Path
+
+import librosa
+import numpy as np
+from omegaconf import OmegaConf
+
+from recipes.cad1.task2.baseline.baseline_utils import (
+    load_listeners_and_scenes,
+    read_mp3,
+)
+
+BASE_DIR = Path.cwd()
+RESOURCES = BASE_DIR / "tests" / "resources" / "recipes" / "cad1" / "task2"
+
+
+def test_read_mp3():
+    """Test read_mp3()"""
+    signal, sample_rate = read_mp3(librosa.example("brahms"))
+    assert isinstance(signal, np.ndarray)
+    assert isinstance(sample_rate, int)
+
+
+def test_load_listeners_and_scenes():
+    """Test load_listeners_and_scenes()"""
+    config = OmegaConf.create(
+        {
+            "path": {
+                "scenes_file": (RESOURCES / "scenes.json").as_posix(),
+                "listeners_train_file": (RESOURCES / "listeners.json").as_posix(),
+                "scenes_listeners_file": (
+                    RESOURCES / "scenes_listeners.json"
+                ).as_posix(),
+            },
+            "evaluate": {"split": "train"},
+        }
+    )
+    scenes, listener_audiograms, scene_listeners = load_listeners_and_scenes(config)
+    assert isinstance(scenes, dict)
+    assert isinstance(listener_audiograms, dict)
+    assert isinstance(scene_listeners, dict)
diff --git a/tests/recipes/cad1/task2/test_build_scene_metadata.py b/tests/recipes/cad1/task2/test_build_scene_metadata.py
new file mode 100644
index 000000000..578e74bba
--- /dev/null
+++ b/tests/recipes/cad1/task2/test_build_scene_metadata.py
@@ -0,0 +1,70 @@
+"""Tests for the build_scene_metadata.py script"""
+# pylint: disable=import-error
+
+from pathlib import Path
+
+import numpy as np
+import pytest
+
+from recipes.cad1.task2.data_preparation.build_scene_metadata import (
+    get_random_car_params,
+    get_random_head_rotation,
+    read_json,
+    set_seed,
+)
+
+BASE_DIR = Path.cwd()
+RESOURCES = BASE_DIR / "tests" / "resources" / "recipes" / "cad1" / "task2"
+
+
+def test_set_seed():
+    """Test that the seed is set correctly"""
+    seed = 123
+    set_seed(seed)
+    np.testing.assert_array_almost_equal(np.random.rand(), 0.6964691855978616)
+
+
+def test_get_random_head_rotation():
+    """Test that the function returns a random item from the dictionary"""
+    input_dict = {"1": "value1", "2": "value2", "3": "value3"}
+    np.random.seed(42)
+    random_item = get_random_head_rotation(input_dict)
+    assert random_item == 1
+
+
+def test_get_random_car_params():
+    """Test that the function returns a dictionary with the expected keys"""
+    np.random.seed(42)
+    car_params = get_random_car_params()
+    assert car_params == {
+        "bump": {"btype": "bandpass", "cutoff_hz": [94, 187], "order": 1},
+        "dip_high": {"btype": "highpass", "cutoff_hz": 128, "order": 2},
+        "dip_low": {"btype": "lowpass", "cutoff_hz": 60, "order": 2},
+        "engine_num_harmonics": 20,
+        "gear": 5,
+        "primary_filter": {"btype": "lowpass", "cutoff_hz": 16.507, "order": 1},
+        "reference_level_db": 31.4,
+        "rpm": 2060.4,
+        "secondary_filter": {
+            "btype": "lowpass",
+            "cutoff_hz": 305.51040000000006,
+            "order": 2,
+        },
+        "speed": 101.0,
+    }
+
+
+@pytest.mark.parametrize(
+    "json_file, expected",
+    [
+        (
+            RESOURCES / "test_build_scene_metadata.json_sample.json",
+            {"key1": "value1", "key2": "value2"},
+        ),
+    ],
+)
+def test_read_json(json_file, expected):
+    """Test that the function reads the json file correctly"""
+    output = read_json(json_file.as_posix())
+
+    assert output == expected
diff --git a/tests/recipes/cad1/task2/test_enhance.py b/tests/recipes/cad1/task2/test_enhance.py
new file mode 100644
index 000000000..17ef5f492
--- /dev/null
+++ b/tests/recipes/cad1/task2/test_enhance.py
@@ -0,0 +1,56 @@
+"""Test the enhance module."""
+# pylint: disable=import-error
+
+from pathlib import Path
+
+import numpy as np
+import pyloudnorm as pyln
+from omegaconf import DictConfig
+
+from recipes.cad1.task2.baseline.enhance import enhance_song
+
+BASE_DIR = Path.cwd()
+RESOURCES = BASE_DIR / "tests" / "resources" / "recipes" / "cad1" / "task2"
+
+
+def test_enhance_song():
+    """Test the enhance_song function."""
+    np.random.seed(42)
+
+    # Set the sample rate and gain
+    duration = 0.5
+
+    config = DictConfig(
+        {
+            "sample_rate": 16000,
+            "enhance": {"min_level": -11, "max_level": -19, "average_level": -14},
+        }
+    )
+    listener = {
+        "audiogram_levels_l": [20, 30, 35, 45, 50, 60, 65, 60],
+        "audiogram_levels_r": [20, 30, 35, 45, 50, 60, 65, 60],
+        "audiogram_cfs": [250, 500, 1000, 2000, 3000, 4000, 6000, 8000],
+    }
+
+    # Create a sample waveform
+    waveform = np.random.rand(2, int(config.sample_rate * duration))
+
+    # Call the function
+    out_left, out_right = enhance_song(waveform, listener, config)
+
+    expected_left = np.load(
+        RESOURCES / "test_enhance.enhance_song_left.npy", allow_pickle=True
+    )
+    expected_right = np.load(
+        RESOURCES / "test_enhance.enhance_song_right.npy", allow_pickle=True
+    )
+
+    # Check that the output is not equal to the input
+    np.testing.assert_array_almost_equal(out_left, expected_left)
+    np.testing.assert_array_almost_equal(out_right, expected_right)
+
+    # Check that the output has the correct loudness
+    meter = pyln.Meter(config.sample_rate)
+
+    out_loudness = meter.integrated_loudness(np.array([out_left, out_right]).T)
+    assert np.isclose(out_loudness, -14, atol=0.1)
diff --git a/tests/resources/test_enhance.test_apply_baseline_ha.npy b/tests/resources/recipes/cad1/task1/test_enhance.test_apply_baseline_ha.npy
similarity index 100%
rename from tests/resources/test_enhance.test_apply_baseline_ha.npy
rename to tests/resources/recipes/cad1/task1/test_enhance.test_apply_baseline_ha.npy
diff --git a/tests/resources/test_enhance.test_decompose_signal.npy b/tests/resources/recipes/cad1/task1/test_enhance.test_decompose_signal.npy
similarity index 100%
rename from tests/resources/test_enhance.test_decompose_signal.npy
rename to tests/resources/recipes/cad1/task1/test_enhance.test_decompose_signal.npy
diff --git a/tests/resources/test_enhance.test_process_stems_for_listener.npy b/tests/resources/recipes/cad1/task1/test_enhance.test_process_stems_for_listener.npy
similarity index 100%
rename from tests/resources/test_enhance.test_process_stems_for_listener.npy
rename to tests/resources/recipes/cad1/task1/test_enhance.test_process_stems_for_listener.npy
diff --git a/tests/resources/test_enhance.test_separate_sources.npy b/tests/resources/recipes/cad1/task1/test_enhance.test_separate_sources.npy
similarity index 100%
rename from tests/resources/test_enhance.test_separate_sources.npy
rename to tests/resources/recipes/cad1/task1/test_enhance.test_separate_sources.npy
diff --git a/tests/resources/recipes/cad1/task2/listeners.json b/tests/resources/recipes/cad1/task2/listeners.json
new file mode 100644
index 000000000..53fbf2c88
--- /dev/null
+++ b/tests/resources/recipes/cad1/task2/listeners.json
@@ -0,0 +1,68 @@
+{
+  "L5071": {
+    "name": "L5071",
+    "audiogram_cfs": [
+      250,
+      500,
+      1000,
+      2000,
+      3000,
+      4000,
+      6000,
+      8000
+    ],
+    "audiogram_levels_l": [
+      15,
+      30,
+      40,
+      55,
+      55,
+      55,
+      70,
+      65
+    ],
+    "audiogram_levels_r": [
+      15,
+      20,
+      35,
+      45,
+      40,
+      35,
+      50,
+      65
+    ]
+  },
+  "L5042": {
+    "name": "L5042",
+    "audiogram_cfs": [
+      250,
+      500,
+      1000,
+      2000,
+      3000,
+      4000,
+      6000,
+      8000
+    ],
+    "audiogram_levels_l": [
+      25,
+      25,
+      40,
+      50,
+      50,
+      65,
+      80,
+      80
+    ],
+    "audiogram_levels_r": [
+      20,
+      20,
+      30,
+      45,
+      60,
+      70,
+      80,
+      80
+    ]
+  }
+}
\ No newline at end of file
diff --git a/tests/resources/recipes/cad1/task2/scenes.json b/tests/resources/recipes/cad1/task2/scenes.json
new file mode 100644
index 000000000..000d51722
--- /dev/null
+++ b/tests/resources/recipes/cad1/task2/scenes.json
@@ -0,0 +1,8 @@
+{
+    "SCENE01": {
+        "split": "valid"
+    },
+    "SCENE02": {
+        "split": "valid"
+    }
+}
diff --git a/tests/resources/recipes/cad1/task2/scenes_listeners.json b/tests/resources/recipes/cad1/task2/scenes_listeners.json
new file mode 100644
index 000000000..d4c003069
--- /dev/null
+++ b/tests/resources/recipes/cad1/task2/scenes_listeners.json
@@ -0,0 +1,4 @@
+{
+    "SCENE1": ["L5071"],
+    "SCENE2": ["L5042"]
+}
\ No newline at end of file
diff --git a/tests/resources/recipes/cad1/task2/test_build_scene_metadata.json_sample.json b/tests/resources/recipes/cad1/task2/test_build_scene_metadata.json_sample.json
new file mode 100644
index 000000000..73d1c4824
--- /dev/null
+++ b/tests/resources/recipes/cad1/task2/test_build_scene_metadata.json_sample.json
@@ -0,0 +1,1 @@
+{"key1": "value1", "key2": "value2"}
\ No newline at end of file
diff --git a/tests/resources/recipes/cad1/task2/test_enhance.enhance_song_left.npy b/tests/resources/recipes/cad1/task2/test_enhance.enhance_song_left.npy
new file mode 100644
index 000000000..1e48a6cc4
Binary files /dev/null and b/tests/resources/recipes/cad1/task2/test_enhance.enhance_song_left.npy differ
diff --git a/tests/resources/recipes/cad1/task2/test_enhance.enhance_song_right.npy b/tests/resources/recipes/cad1/task2/test_enhance.enhance_song_right.npy
new file mode 100644
index 000000000..31c5e4d47
Binary files /dev/null and b/tests/resources/recipes/cad1/task2/test_enhance.enhance_song_right.npy differ
diff --git a/tests/resources/utils/test_carnoise.signal_generator.npy b/tests/resources/utils/test_carnoise.signal_generator.npy
new file mode 100644
index 000000000..35308b48f
Binary files /dev/null and b/tests/resources/utils/test_carnoise.signal_generator.npy differ
diff --git a/tests/utils/car_noise_simulator/test_carnoise_parameter_generator.py b/tests/utils/car_noise_simulator/test_carnoise_parameter_generator.py
new file mode 100644
index 000000000..b9f020c9c
--- /dev/null
+++ b/tests/utils/car_noise_simulator/test_carnoise_parameter_generator.py
@@ -0,0 +1,103 @@
+"""Tests for the Car noise parameters generator"""
+# pylint: disable=import-error
+
+import numpy as np
+import pytest
+
+from clarity.utils.car_noise_simulator.carnoise_parameters_generator import (
+    CarNoiseParametersGenerator,
+)
+
+
+@pytest.mark.parametrize(
+    "random_flag,speed,expected_result",
+    [
+        (
+            False,
+            100,
+            {
+                "bump": {"btype": "bandpass", "cutoff_hz": [30, 60], "order": 1},
+                "dip_high": {"btype": "highpass", "cutoff_hz": 300, "order": 2},
+                "dip_low": {"btype": "lowpass", "cutoff_hz": 200, "order": 2},
+                "engine_num_harmonics": 25,
+                "gear": 6,
+                "primary_filter": {
+                    "btype": "lowpass",
+                    "cutoff_hz": 16.860000000000003,
+                    "order": 1,
+                },
+                "reference_level_db": 30,
+                "rpm": 1680.0000000000002,
+                "secondary_filter": {
+                    "btype": "lowpass",
+                    "cutoff_hz": 280.0,
+                    "order": 2,
+                },
+                "speed": 100.0,
+            },
+        ),
+        (
+            True,
+            100,
+            {
+                "bump": {"btype": "bandpass", "cutoff_hz": [80, 116], "order": 2},
+                "dip_high": {"btype": "highpass", "cutoff_hz": 319, "order": 1},
+                "dip_low": {"btype": "lowpass", "cutoff_hz": 130, "order": 1},
+                "engine_num_harmonics": 38,
+                "gear": 5,
+                "primary_filter": {
+                    "btype": "lowpass",
+                    "cutoff_hz": 15.410040000000004,
+                    "order": 1,
+                },
+                "reference_level_db": 31.9,
+                "rpm": 2040.0,
+                "secondary_filter": {
+                    "btype": "lowpass",
+                    "cutoff_hz": 281.68000000000006,
+                    "order": 2,
+                },
+                "speed": 100.0,
+            },
+        ),
+        (
+            True,
+            70,
+            {
+                "bump": {"btype": "bandpass", "cutoff_hz": [91, 168], "order": 1},
+                "dip_high": {"btype": "highpass", "cutoff_hz": 220, "order": 1},
+                "dip_low": {"btype": "lowpass", "cutoff_hz": 90, "order": 1},
+                "engine_num_harmonics": 29,
+                "gear": 4,
+                "primary_filter": {
+                    "btype": "lowpass",
+                    "cutoff_hz": 12.558720000000001,
+                    "order": 1,
+                },
+                "reference_level_db": 30.6,
+                "rpm": 1890.0,
+                "secondary_filter": {
+                    "btype": "lowpass",
+                    "cutoff_hz": 233.984,
+                    "order": 2,
+                },
+                "speed": 70.0,
+            },
+        ),
+    ],
+)
+def test_gen_parameters(random_flag, speed, expected_result):
+    """Test the generation of parameters with and without randomisation."""
+    np.random.seed(42)
+    car_noise_parameters = CarNoiseParametersGenerator(random_flag=random_flag)
+    parameters = car_noise_parameters.gen_parameters(speed_kph=speed)
+
+    assert parameters == expected_result
+
+
+@pytest.mark.parametrize("speed", [40, 130])
+def test_gen_parameters_invalid_speed(speed):
+    """Test speed values out of the boundaries."""
+    car_noise_parameters = CarNoiseParametersGenerator(random_flag=True)
+    with pytest.raises(ValueError):
+        car_noise_parameters.gen_parameters(speed)
diff --git a/tests/utils/car_noise_simulator/test_carnoise_signal_generator.py b/tests/utils/car_noise_simulator/test_carnoise_signal_generator.py
new file mode 100644
index 000000000..17a79a303
--- /dev/null
+++ b/tests/utils/car_noise_simulator/test_carnoise_signal_generator.py
@@ -0,0 +1,49 @@
+"""Tests for the Car noise signal generator"""
+# pylint: disable=import-error
+
+from pathlib import Path
+
+import numpy as np
+
+from clarity.utils.car_noise_simulator.carnoise_signal_generator import (
+    CarNoiseSignalGenerator,
+)
+
+BASE_DIR = Path.cwd()
+RESOURCES = BASE_DIR / "tests" / "resources" / "utils"
+
+
+def test_car_noise_generation():
+    """Test that the car noise generator returns the expected signal"""
+    np.random.seed(42)
+    carnoise_params = {
+        "bump": {"btype": "bandpass", "cutoff_hz": [30, 60], "order": 1},
+        "dip_high": {"btype": "highpass", "cutoff_hz": 300, "order": 2},
+        "dip_low": {"btype": "lowpass", "cutoff_hz": 200, "order": 2},
+        "engine_num_harmonics": 25,
+        "gear": 6,
+        "primary_filter": {
+            "btype": "lowpass",
+            "cutoff_hz": 16.860000000000003,
+            "order": 1,
+        },
+        "reference_level_db": 30,
+        "rpm": 1680.0000000000002,
+        "secondary_filter": {
+            "btype": "lowpass",
+            "cutoff_hz": 280.0,
+            "order": 2,
+        },
+        "speed": 100.0,
+    }
+
+    car_noise = CarNoiseSignalGenerator(
+        sample_rate=16000, duration_secs=1, random_flag=True
+    )
+    car_noise_signal = car_noise.generate_car_noise(carnoise_params, 3, 0.5)
+
+    assert car_noise_signal.shape == (4, 16000)
+    expected = np.load(
+        RESOURCES / "test_carnoise.signal_generator.npy", allow_pickle=True
+    )
+    np.testing.assert_array_almost_equal(car_noise_signal, expected)