Repository: claritychallenge/clarity (language: python)

Issue:
Preparartion for Cadenza 2 Challenge
There are several things that need to be implemented for CAD2 challenge
- [x] #374
- [x] #375
  - [x] #373
    - [x] #372
       - [x] Dataloader
       - [x] Architecture
       - [x] Training script 
    - [x] #377
  - [x] Task 2 - Rebalancing Classical Music
    - [x] Separation model for replication Task 2
       - [x] Dataloader
       - [x] Architecture
       - [x] Training script 
    - [x] #390
update branch

Preparartion for Cadenza 2 Challenge
There are several things that need to be implemented for CAD2 challenge
- [x] #374
- [x] #375
  - [x] #373
    - [x] #372
       - [x] Dataloader
       - [x] Architecture
       - [x] Training script 
    - [x] #377
  - [x] Task 2 - Rebalancing Classical Music
    - [x] Separation model for replication Task 2
       - [x] Dataloader
       - [x] Architecture
       - [x] Training script 
    - [x] #390
Baseline CAD2 - Task 1

Preparartion for Cadenza 2 Challenge
There are several things that need to be implemented for CAD2 challenge
- [x] #374
- [x] #375
  - [x] #373
    - [x] #372
       - [x] Dataloader
       - [x] Architecture
       - [x] Training script 
    - [x] #377
  - [x] Task 2 - Rebalancing Classical Music
    - [x] Separation model for replication Task 2
       - [x] Dataloader
       - [x] Architecture
       - [x] Training script 
    - [x] #390


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 88c206be6..7b59f18f0 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -70,6 +70,9 @@ repos:
       - id: mypy
         args:
           - --explicit-package-bases
+        additional_dependencies:
+          - 'types-PyYAML'
+
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
     # Ruff version.
diff --git a/clarity/enhancer/multiband_compressor/__init__.py b/clarity/enhancer/multiband_compressor/__init__.py
index e69de29bb..fa6c6a3b7 100644
--- a/clarity/enhancer/multiband_compressor/__init__.py
+++ b/clarity/enhancer/multiband_compressor/__init__.py
@@ -0,0 +1,7 @@
+from clarity.enhancer.multiband_compressor.compressor_qmul import Compressor
+from clarity.enhancer.multiband_compressor.crossover import Crossover
+from clarity.enhancer.multiband_compressor.multiband_compressor import (
+    MultibandCompressor,
+)
+
+__all__ = ["MultibandCompressor", "Compressor", "Crossover"]
diff --git a/clarity/enhancer/multiband_compressor/compressor_qmul.py b/clarity/enhancer/multiband_compressor/compressor_qmul.py
index acbdc8b9e..845ea8a16 100644
--- a/clarity/enhancer/multiband_compressor/compressor_qmul.py
+++ b/clarity/enhancer/multiband_compressor/compressor_qmul.py
@@ -2,8 +2,6 @@
 
 from __future__ import annotations
 
-import warnings
-
 import numpy as np
 
 
@@ -68,39 +66,28 @@ def __init__(
         knee_width: float = 0.0,
         sample_rate: float = 44100.0,
     ) -> None:
-        if threshold > 0 or threshold < -60:
-            warnings.warn(
-                "Threshold outside the recommended range [0.0, -60.0] dB."
-                f" {threshold} dB was provided.",
-                stacklevel=1,
-            )
-        if ratio < 1 or ratio > 20:
-            warnings.warn(
-                "Ratio outside the recommended range [1.0, 20.0]."
-                f" {ratio} was provided.",
-                stacklevel=1,
-            )
-
-        if attack < 0.1 or attack > 80.0:
-            warnings.warn(
-                "Attack outside the recommended range [0.1, 80.0] ms."
-                f" {attack} ms was provided.",
-                stacklevel=1,
-            )
-
-        if release < 0.1 or release > 1000.0:
-            warnings.warn(
-                "Release outside the recommended range [0.1, 1000.0] ms."
-                f" {release} ms was provided.",
-                stacklevel=1,
-            )
-
-        if makeup_gain < 0 or makeup_gain > 24:
-            warnings.warn(
-                "Make-up gain outside the recommended range [0.0, 24.0] dB."
-                f" {makeup_gain} dB was provided.",
-                stacklevel=1,
-            )
+        """Constructor for the Compressor class.
+
+        Args:
+            threshold (float): The threshold level in dB.
+            ratio (float): The compression ratio.
+            attack (float): The attack time in ms.
+            release (float): The release time in ms.
+            makeup_gain (float): The make-up gain in dB.
+            knee_width (float): The knee width in dB.
+            sample_rate (float): The sample rate in Hz.
+
+        Notes:
+            Original implementation recommends ranges for each parameter.
+            We are not enforcing these ranges in this implementation.
+            The ranges are:
+            - threshold in the range [0.0, -60.0] dB,
+            - ratio in the range [1.0, 20.0],
+            - attack in the range [0.1, 80.0] ms,
+            - release in the range [0.1, 1000.0] ms,
+            - makeup_gain in the range [0.0, 24.0] dB.
+            - knee_width in the range [0.0, 10.0] dB.
+        """
 
         self.threshold = float(threshold)
         self.ratio = float(ratio)
@@ -110,6 +97,8 @@ def __init__(
         self.sample_rate = float(sample_rate)
         self.knee_width = float(knee_width)
 
+        self.eps = 1e-12
+
         self.alpha_attack = np.exp(-1.0 / (0.001 * self.sample_rate * self.attack))
         self.alpha_release = np.exp(-1.0 / (0.001 * self.sample_rate * self.release))
 
@@ -125,6 +114,7 @@ def __call__(self, input_signal: np.ndarray) -> np.ndarray:
         """
 
         # Compute the instantaneous desired levels
+        input_signal[input_signal == 0] = self.eps
         x_g = 20 * np.log10(np.abs(input_signal))
         x_g[x_g < -120] = -120
         y_g = self.threshold + (x_g - self.threshold) / self.ratio
diff --git a/clarity/utils/flac_encoder.py b/clarity/utils/flac_encoder.py
index 56eaef92e..e738ed361 100644
--- a/clarity/utils/flac_encoder.py
+++ b/clarity/utils/flac_encoder.py
@@ -15,6 +15,8 @@
 import pyflac as pf
 import soundfile as sf
 
+from clarity.utils.signal_processing import clip_signal, resample, to_16bit
+
 logger = logging.getLogger(__name__)
 
 
@@ -189,11 +191,11 @@ def encode(
         """
         if signal.dtype != np.int16:
             logger.error(
-                f"FLAC encoder only supports 16-bit integer signals, "
+                "FLAC encoder only supports 16-bit integer signals, "
                 f"but got {signal.dtype}"
             )
             raise ValueError(
-                f"FLAC encoder only supports 16-bit integer signals, "
+                "FLAC encoder only supports 16-bit integer signals, "
                 f"but got {signal.dtype}"
             )
 
@@ -261,3 +263,65 @@ def read_flac_signal(filename: Path) -> tuple[np.ndarray, float]:
             # Scale signal
             signal *= max_value
     return signal, sample_rate
+
+
+def save_flac_signal(
+    signal: np.ndarray,
+    filename: Path,
+    signal_sample_rate: int,
+    output_sample_rate: int | None = None,
+    do_clip_signal: bool = False,
+    do_soft_clip: bool = False,
+    do_scale_signal: bool = False,
+) -> None:
+    """
+    Function to save output signals.
+
+    - The output signal will be resample to ``output_sample_rate``.
+        If ``output_sample_rate`` is None, the output signal will have
+        the same sample rate as the input signal.
+    - The output signal will be clipped to [-1, 1] if ``do_clip_signal`` is True
+        and use soft clipped if ``do_soft_clip`` is True. Note that if
+        ``do_clip_signal`` is False, ``do_soft_clip`` will be ignored.
+        Note that if ``do_clip_signal`` is True, ``do_scale_signal`` will be ignored.
+    - The output signal will be scaled to [-1, 1] if ``do_scale_signal`` is True.
+        If signal is scale, the scale factor will be saved in a TXT file.
+        Note that if ``do_clip_signal`` is True, ``do_scale_signal`` will be ignored.
+    - The output signal will be saved as a FLAC file.
+
+    Args:
+        signal (np.ndarray) : Signal to save
+        filename (Path) : Path to save signal
+        signal_sample_rate (int) : Sample rate of the input signal
+        output_sample_rate (int) : Sample rate of the output signal
+        do_clip_signal (bool) : Whether to clip signal
+        do_soft_clip (bool) : Whether to apply soft clipping
+        do_scale_signal (bool) : Whether to scale signal
+    """
+    # Resample signal to expected output sample rate
+    if output_sample_rate is None:
+        output_sample_rate = signal_sample_rate
+
+    if signal_sample_rate != output_sample_rate:
+        signal = resample(signal, signal_sample_rate, output_sample_rate)
+
+    if do_scale_signal:
+        # Scale stem signal
+        max_value = np.max(np.abs(signal))
+        signal = signal / max_value
+
+        # Save scale factor
+        with open(filename.with_suffix(".txt"), "w", encoding="utf-8") as file:
+            file.write(f"{max_value}")
+
+    elif do_clip_signal:
+        # Clip the signal
+        signal, n_clipped = clip_signal(signal, do_soft_clip)
+        if n_clipped > 0:
+            logger.warning(f"Writing {filename}: {n_clipped} samples clipped")
+
+    # Convert signal to 16-bit integer
+    signal = to_16bit(signal)
+
+    # Create flac encoder object to compress and save the signal
+    FlacEncoder().encode(signal, output_sample_rate, filename)
diff --git a/pyproject.toml b/pyproject.toml
index 5f3640405..1d24c0dcb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -39,6 +39,7 @@ dependencies = [
     "pystoi",
     "pytorch-lightning",
     "resampy",
+    "safetensors>=0.4.3",
     "scikit-learn>=1.0.2",
     "scipy>=1.7.3, <1.13.0",
     "SoundFile>=0.10.3.post1",
diff --git a/recipes/cad2/README.md b/recipes/cad2/README.md
index e69de29bb..6eed863c9 100644
--- a/recipes/cad2/README.md
+++ b/recipes/cad2/README.md
@@ -0,0 +1,7 @@
+# The Second Cadenza Challenge
+
+Cadenza challenge code for the Second Cadenza Challenge (CAD2).
+For more information please visit the [challenge website](https://cadenzachallenge.org/docs/cadenza2/intro).
+
+In the directories `task 1` and `task 2`, you will find the code for the baseline
+for each system and the instruction on how to obtain the data.
diff --git a/recipes/cad2/task1/ConvTasNet/README.md b/recipes/cad2/task1/ConvTasNet/README.md
new file mode 100644
index 000000000..986d26d0a
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/README.md
@@ -0,0 +1,41 @@
+# CAD2-TASK1 singing/accompaniment separation model
+
+This recipe contains the necessary content to replicate the separation models used in CAD2-Task1.
+
+- The system is based on Asteroid Source Separation system.
+- ConvTasNet implementation is based on stereo adaptation by Alexandre Defossez <https://github.com/facebookresearch/demucs/blob/v1/demucs/tasnet.py>
+- Evaluation logic is based on <https://github.com/asteroid-team/asteroid/blob/master/egs/musdb18/X-UMX/eval.py>
+- Dataloader is based on <https://github.com/asteroid-team/asteroid/blob/master/asteroid/data/musdb18_dataset.py>
+
+You can replicate the Causal and Non-Causal model by running:
+
+- **To replicate the Non-Causal model**
+
+```bash
+python train.py \
+    --exp_dir /path/to/save/exps \
+    --batch_size 4 \
+    --aggregate 2 \
+    --lr 0.0005 \
+    --root /path/to/MUSDB18 \
+    --sample_rate 44100 \
+    --segment 5.0 \
+    --samples_per_track 64
+```
+
+- **To replicate the Causal model**
+
+```bash
+python train.py \
+    --exp_dir /path/to/save/exps \
+    --batch_size 4 \
+    --aggregate 1 \
+    --lr 0.0005 \
+    --root /path/to/MUSDB18 \
+    --sample_rate 44100 \
+    --segment 4.0 \
+    --samples_per_track 64 \
+    --causal True \
+    --n_src 2 \
+    --norm_type cLN
+```
diff --git a/recipes/cad2/task1/ConvTasNet/__init__.py b/recipes/cad2/task1/ConvTasNet/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad2/task1/ConvTasNet/eval.py b/recipes/cad2/task1/ConvTasNet/eval.py
new file mode 100644
index 000000000..920082da4
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/eval.py
@@ -0,0 +1,119 @@
+import argparse
+import os
+import sys
+from pathlib import Path
+
+import musdb
+import museval
+import soundfile as sf
+import torch
+import yaml
+from local import ConvTasNetStereo
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    "--out_dir",
+    type=str,
+    required=True,
+    help="Directory in exp_dir where the eval results will be stored",
+)
+parser.add_argument(
+    "--use_gpu", type=int, default=0, help="Whether to use the GPU for model execution"
+)
+parser.add_argument("--exp_dir", default="exp/tmp", help="Experiment root")
+parser.add_argument(
+    "--n_save_ex",
+    type=int,
+    default=10,
+    help="Number of audio examples to save, -1 means all",
+)
+
+compute_metrics = ["si_sdr", "sdr", "sir", "sar"]
+
+
+def main(conf):
+    model_path = os.path.join(conf["exp_dir"], "best_model.pth")
+
+    model = ConvTasNetStereo(
+        **conf["train_conf"]["convtasnet"],
+        samplerate=conf["train_conf"]["data"]["sample_rate"],
+    )
+
+    saved = torch.load(model_path, map_location="cpu")
+    model.load_state_dict(saved["state_dict"])
+
+    # Handle device placement
+    if conf["use_gpu"]:
+        model.cuda()
+
+    model_device = next(model.parameters()).device
+
+    # Evaluation is mode using 'remix' mixture
+    test_set = musdb.DB(
+        root=conf["train_conf"]["data"]["root"], subsets="test", is_wav=True
+    )
+    results = museval.EvalStore()
+
+    # Randomly choose the indexes of sentences to save.
+    eval_save_dir = os.path.join(conf["exp_dir"], conf["out_dir"])
+    Path(eval_save_dir).mkdir(exist_ok=True, parents=True)
+
+    txtout = os.path.join(eval_save_dir, "results.txt")
+    fp = open(txtout, "w")
+
+    torch.no_grad().__enter__()
+    for track in test_set:
+        input_file = os.path.join(
+            conf["train_conf"]["data"]["root"], "test", track.name, "mixture.wav"
+        )
+        # Forward the network on the mixture.
+        mix, rate = sf.read(input_file, always_2d=True, start=0, stop=None)
+
+        # Separate
+        mix = torch.tensor(mix.T, dtype=torch.float).to(model_device)
+
+        est_sources = model.forward(mix.unsqueeze(0))
+        est_sources = est_sources.squeeze(0).cpu().data.numpy()
+
+        estimates = {}
+        estimates["vocals"] = est_sources[0].T
+        estimates["accompaniment"] = est_sources[1].T
+
+        output_path = Path(os.path.join(eval_save_dir, track.name))
+        output_path.mkdir(exist_ok=True, parents=True)
+
+        print(f"Processing... {track.name}", file=sys.stderr)
+        print(track.name, file=fp)
+
+        for target, estimate in estimates.items():
+            sf.write(
+                str(output_path / Path(target).with_suffix(".wav")),
+                estimate,
+                conf["train_conf"]["data"]["sample_rate"],
+            )
+        track_scores = museval.eval_mus_track(track, estimates)
+        results.add_track(track_scores.df)
+        print(track_scores, file=sys.stderr)
+        print(track_scores, file=fp)
+    print(results, file=sys.stderr)
+    print(results, file=fp)
+    results.save(os.path.join(eval_save_dir, "results.pandas"))
+    results.frames_agg = "mean"
+    print(results, file=sys.stderr)
+    print(results, file=fp)
+    fp.close()
+
+
+if __name__ == "__main__":
+    args = parser.parse_args()
+    arg_dic = dict(vars(args))
+    # Load training config
+    conf_path = os.path.join(args.exp_dir, "conf.yml")
+    with open(conf_path) as f:
+        train_conf = yaml.safe_load(f)
+    arg_dic["sample_rate"] = train_conf["data"]["sample_rate"]
+    arg_dic["train_conf"] = train_conf
+
+    main(arg_dic)
+
+    print("Done!")
diff --git a/recipes/cad2/task1/ConvTasNet/local/__init__.py b/recipes/cad2/task1/ConvTasNet/local/__init__.py
new file mode 100644
index 000000000..4dd3e199d
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/__init__.py
@@ -0,0 +1,11 @@
+from .musdb18_dataset import Compose, MUSDB18Dataset, augment_channelswap, augment_gain
+from .tasnet import ConvTasNetStereo, overlap_and_add
+
+__all__ = [
+    "MUSDB18Dataset",
+    "Compose",
+    "augment_gain",
+    "augment_channelswap",
+    "ConvTasNetStereo",
+    "overlap_and_add",
+]
diff --git a/recipes/cad2/task1/ConvTasNet/local/conf.yml b/recipes/cad2/task1/ConvTasNet/local/conf.yml
new file mode 100644
index 000000000..8b7e8ad2f
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/conf.yml
@@ -0,0 +1,36 @@
+# ConvTasNet config
+convtasnet:
+  N: 256
+  L: 20
+  B: 256
+  H: 512
+  P: 3
+  X: 10
+  R: 4
+  C: 2
+  audio_channels: 2 #  stereo
+  norm_type: gLN
+  causal: False
+  mask_nonlinear: 'relu'
+
+# Training config
+training:
+  epochs: 200
+  batch_size: 4       # Aggregate 2 iteration, effective batch size = 8
+  num_workers: 4
+  half_lr: yes
+  early_stop: yes
+  aggregate: 2        # aggregate gradiante every 2 iterations
+# Optim config
+optim:
+  lr: 0.0005
+# Data config
+data:
+  root: /path/to/MUSDB18
+  mix_background: true
+  segment: 5.0            # Seconds
+  samples_per_track: 64
+  sample_rate: 44100
+
+
+
diff --git a/recipes/cad2/task1/ConvTasNet/local/musdb18_dataset.py b/recipes/cad2/task1/ConvTasNet/local/musdb18_dataset.py
new file mode 100644
index 000000000..5d337f1f1
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/musdb18_dataset.py
@@ -0,0 +1,288 @@
+import random
+from pathlib import Path
+
+import soundfile as sf
+import torch
+import torch.utils.data
+import tqdm
+
+
+class Compose:
+    """Composes several augmentation transforms.
+    Args:
+        augmentations: list of augmentations to compose.
+    """
+
+    def __init__(self, transforms):
+        self.transforms = transforms
+
+    def __call__(self, audio):
+        for transform in self.transforms:
+            audio = transform(audio)
+        return audio
+
+
+def augment_gain(audio, low=0.25, high=1.25):
+    """Applies a random gain to each source between `low` and `high`"""
+    gain = low + torch.rand(1) * (high - low)
+    return audio * gain
+
+
+def augment_channelswap(audio):
+    """Randomly swap channels of stereo sources"""
+    if audio.shape[0] == 2 and torch.FloatTensor(1).uniform_() < 0.5:
+        return torch.flip(audio, [0])
+
+    return audio
+
+
+class MUSDB18Dataset(torch.utils.data.Dataset):
+    """MUSDB18 music separation dataset
+
+    The dataset consists of 150 full lengths music tracks (~10h duration) of
+    different genres along with their isolated stems:
+        `drums`, `bass`, `vocals` and `others`.
+
+    Out-of-the-box, asteroid does only support MUSDB18-HQ which comes as
+    uncompressed WAV files. To use the MUSDB18, please convert it to WAV first:
+
+    - MUSDB18 HQ: https://zenodo.org/record/3338373
+    - MUSDB18     https://zenodo.org/record/1117372
+
+    .. note::
+        The datasets are hosted on Zenodo and require that users
+        request access, since the tracks can only be used for academic purposes.
+        We manually check this requests.
+
+    This dataset asssumes music tracks in (sub)folders where each folder
+    has a fixed number of sources (defaults to 4). For each track, a list
+    of `sources` and a common `suffix` can be specified.
+    A linear mix is performed on the fly by summing up the sources
+
+    Due to the fact that all tracks comprise the exact same set
+    of sources, random track mixing can be used can be used,
+    where sources from different tracks are mixed together.
+
+    Folder Structure:
+        >>> #train/1/vocals.wav ---------|
+        >>> #train/1/drums.wav ----------+--> input (mix), output[target]
+        >>> #train/1/bass.wav -----------|
+        >>> #train/1/other.wav ---------/
+
+    Args:
+        root (str): Root path of dataset
+        sources (:obj:`list` of :obj:`str`, optional): List of source names
+            that composes the mixture.
+            Defaults to MUSDB18 4 stem scenario: `vocals`, `drums`, `bass`, `other`.
+        targets (list or None, optional): List of source names to be used as
+            targets. If None, a dict with the 4 stems is returned.
+             If e.g [`vocals`, `drums`], a tensor with stacked `vocals` and
+             `drums` is returned instead of a dict. Defaults to None.
+        suffix (str, optional): Filename suffix, defaults to `.wav`.
+        split (str, optional): Dataset subfolder, defaults to `train`.
+        subset (:obj:`list` of :obj:`str`, optional): Selects a specific of
+            list of tracks to be loaded, defaults to `None` (loads all tracks).
+        segment (float, optional): Duration of segments in seconds,
+            defaults to ``None`` which loads the full-length audio tracks.
+        samples_per_track (int, optional):
+            Number of samples yielded from each track, can be used to increase
+            dataset size, defaults to `1`.
+        random_segments (boolean, optional): Enables random offset for track segments.
+        random_track_mix boolean: enables mixing of random sources from
+            different tracks to assemble mix.
+        source_augmentations (:obj:`list` of :obj:`callable`): list of augmentation
+            function names, defaults to no-op augmentations (input = output)
+        sample_rate (int, optional): Samplerate of files in dataset.
+
+    Attributes:
+        root (str): Root path of dataset
+        sources (:obj:`list` of :obj:`str`, optional): List of source names.
+            Defaults to MUSDB18 4 stem scenario: `vocals`, `drums`, `bass`, `other`.
+        suffix (str, optional): Filename suffix, defaults to `.wav`.
+        split (str, optional): Dataset subfolder, defaults to `train`.
+        subset (:obj:`list` of :obj:`str`, optional): Selects a specific of
+            list of tracks to be loaded, defaults to `None` (loads all tracks).
+        segment (float, optional): Duration of segments in seconds,
+            defaults to ``None`` which loads the full-length audio tracks.
+        samples_per_track (int, optional):
+            Number of samples yielded from each track, can be used to increase
+            dataset size, defaults to `1`.
+        random_segments (boolean, optional): Enables random offset for track segments.
+        random_track_mix boolean: enables mixing of random sources from
+            different tracks to assemble mix.
+        source_augmentations (:obj:`list` of :obj:`callable`): list of augmentation
+            function names, defaults to no-op augmentations (input = output)
+        sample_rate (int, optional): Samplerate of files in dataset.
+        tracks (:obj:`list` of :obj:`Dict`): List of track metadata
+
+    References
+        "The 2018 Signal Separation Evaluation Campaign" Stoter et al. 2018.
+
+    Notes:
+        This is a modified versions of the MUSDB18 dataset from the version
+        in Asteroid. It extend the Asteroid version to allow for targets
+        ```vocals``` and ```background```. The background is the sum of all
+        sources except vocals. This is useful for training models that
+        separate vocals from background.
+    """
+
+    dataset_name = "MUSDB18"
+
+    def __init__(
+        self,
+        root: str,
+        sources=None,
+        targets=None,
+        mix_background=False,
+        suffix=".wav",
+        split="train",
+        subset=None,
+        exclude_tracks=None,
+        segment=None,
+        samples_per_track=1,
+        random_segments=False,
+        random_track_mix=False,
+        source_augmentations=lambda audio: audio,
+        sample_rate=44100,
+    ):
+        if sources is None:
+            sources = ["vocals", "bass", "drums", "other"]
+        self.root: Path = Path(root).expanduser()
+        self.split = split
+        self.sample_rate = sample_rate
+        self.segment = segment
+        self.random_track_mix = random_track_mix
+        self.random_segments = random_segments
+        self.source_augmentations = source_augmentations
+        self.sources = sources
+        self.targets = targets
+        self.mix_background = mix_background
+        self.suffix = suffix
+        self.subset = subset
+        self.samples_per_track = samples_per_track
+        if exclude_tracks is None:
+            exclude_tracks = []
+
+        self.tracks = [
+            x for x in list(self.get_tracks()) if x["path"].stem not in exclude_tracks
+        ]
+        if not self.tracks:
+            raise RuntimeError("No tracks found.")
+
+    def __getitem__(self, index):
+        # assemble the mixture of target and interferers
+        audio_sources = {}
+
+        # get track_id
+        track_id = index // self.samples_per_track
+        if self.random_segments:
+            start = random.uniform(
+                0, self.tracks[track_id]["min_duration"] - self.segment
+            )
+        else:
+            start = 0
+
+        self.mixture_path = self.tracks[track_id]["path"]
+
+        # load sources
+        for source in self.sources:
+            # optionally select a random track for each source
+            if self.random_track_mix:
+                # load a different track
+                track_id = random.choice(range(len(self.tracks)))
+                if self.random_segments:
+                    start = random.uniform(
+                        0, self.tracks[track_id]["min_duration"] - self.segment
+                    )
+
+            # loads the full track duration
+            start_sample = int(start * self.sample_rate)
+            # check if dur is none
+            if self.segment:
+                # stop in soundfile is calc in samples, not seconds
+                stop_sample = start_sample + int(self.segment * self.sample_rate)
+            else:
+                # set to None for reading complete file
+                stop_sample = None
+
+            # load actual audio
+            audio, _ = sf.read(
+                Path(self.tracks[track_id]["path"] / source).with_suffix(self.suffix),
+                always_2d=True,
+                start=start_sample,
+                stop=stop_sample,
+            )
+            # convert to torch tensor
+            audio = torch.tensor(audio.T, dtype=torch.float)
+            # apply source-wise augmentations
+            audio = self.source_augmentations(audio)
+            if self.mix_background:
+                if source == "vocals":
+                    audio_sources["vocals"] = audio
+                else:
+                    if "background" not in audio_sources:
+                        audio_sources["background"] = audio
+                    else:
+                        audio_sources["background"] += audio
+            else:
+                audio_sources[source] = audio
+
+        # apply linear mix over source index=0
+        audio_mix = torch.stack(list(audio_sources.values())).sum(0)
+        if self.mix_background:
+            audio_sources = torch.stack(
+                [audio_sources["vocals"], audio_sources["background"]],
+                dim=0,
+            )
+        elif self.targets:
+            audio_sources = torch.stack(
+                [wav for src, wav in audio_sources.items() if src in self.targets],
+                dim=0,
+            )
+        return audio_mix, audio_sources
+
+    def __len__(self):
+        return len(self.tracks) * self.samples_per_track
+
+    def get_tracks(self):
+        """Loads input and output tracks"""
+        p = Path(self.root, self.split)
+        for track_path in tqdm.tqdm(p.iterdir()):
+            if track_path.is_dir():
+                if self.subset and track_path.stem not in self.subset:
+                    # skip this track
+                    continue
+
+                source_paths = [track_path / (s + self.suffix) for s in self.sources]
+                if not all(sp.exists() for sp in source_paths):
+                    print("Exclude track due to non-existing source", track_path)
+                    continue
+
+                # get metadata
+                infos = list(map(sf.info, source_paths))
+                if not all(i.samplerate == self.sample_rate for i in infos):
+                    print("Exclude track due to different sample rate ", track_path)
+                    continue
+
+                if self.segment is not None:
+                    # get minimum duration of track
+                    min_duration = min(i.duration for i in infos)
+                    if min_duration > self.segment:
+                        yield ({"path": track_path, "min_duration": min_duration})
+                else:
+                    yield ({"path": track_path, "min_duration": None})
+
+    def get_infos(self):
+        """Get dataset infos (for publishing models).
+
+        Returns:
+            dict, dataset infos with keys `dataset`, `task` and `licences`.
+        """
+        infos = dict()
+        infos["dataset"] = self.dataset_name
+        infos["task"] = "enhancement"
+        infos["licenses"] = [musdb_license]
+        return infos
+
+
+musdb_license: dict = dict()
diff --git a/recipes/cad2/task1/ConvTasNet/local/prepare_data.sh b/recipes/cad2/task1/ConvTasNet/local/prepare_data.sh
new file mode 100644
index 000000000..9c00bd974
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/prepare_data.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+dampvsep_root=
+python_path=python
+
+. ./utils/parse_options.sh
+if [ ! -d DAMP-VSEP-Singles ]; then
+    # Clone preprocessed DAMP-VSEP-Singles repo
+    git clone https://github.com/groadabike/DAMP-VSEP-Singles.git
+fi
+
+if [ ! -d metadata ]; then
+  # Generate the splits
+  . DAMP-VSEP-Singles/generate_dampvsep_singles.sh $dampvsep_root ../metadata $python_path
+fi
diff --git a/recipes/cad2/task1/ConvTasNet/local/system.py b/recipes/cad2/task1/ConvTasNet/local/system.py
new file mode 100644
index 000000000..f4e87e247
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/system.py
@@ -0,0 +1,228 @@
+from collections.abc import MutableMapping
+
+import pytorch_lightning as pl
+import torch
+from torch.optim.lr_scheduler import ReduceLROnPlateau
+
+
+def flatten_dict(d, parent_key="", sep="_"):
+    """Flattens a dictionary into a single-level dictionary while preserving
+    parent keys. Taken from
+    `SO <https://stackoverflow.com/questions/6027558/flatten-nested-dictionaries-compressing-keys>`_
+
+    Args:
+        d (MutableMapping): Dictionary to be flattened.
+        parent_key (str): String to use as a prefix to all subsequent keys.
+        sep (str): String to use as a separator between two key levels.
+
+    Returns:
+        dict: Single-level dictionary, flattened.
+    """
+    items = []
+    for k, v in d.items():
+        new_key = parent_key + sep + k if parent_key else k
+        if isinstance(v, MutableMapping):
+            items.extend(flatten_dict(v, new_key, sep=sep).items())
+        else:
+            items.append((new_key, v))
+    return dict(items)
+
+
+class System(pl.LightningModule):
+    """Base class for deep learning systems.
+    Contains a model, an optimizer, a loss function, training and validation
+    dataloaders and learning rate scheduler.
+
+    Note that by default, any PyTorch-Lightning hooks are *not* passed to the model.
+    If you want to use Lightning hooks, add the hooks to a subclass::
+
+        class MySystem(System):
+            def on_train_batch_start(self, batch, batch_idx, dataloader_idx):
+                return self.model.on_train_batch_start(batch, batch_idx, dataloader_idx)
+
+    Args:
+        model (torch.nn.Module): Instance of model.
+        optimizer (torch.optim.Optimizer): Instance or list of optimizers.
+        loss_func (callable): Loss function with signature
+            (est_targets, targets).
+        train_loader (torch.utils.data.DataLoader): Training dataloader.
+        val_loader (torch.utils.data.DataLoader): Validation dataloader.
+        scheduler (torch.optim.lr_scheduler._LRScheduler): Instance, or list
+            of learning rate schedulers. Also supports dict or list of dict as
+            ``{"interval": "step", "scheduler": sched}`` where ``interval=="step"``
+            for step-wise schedulers and ``interval=="epoch"`` for classical ones.
+        config: Anything to be saved with the checkpoints during training.
+            The config dictionary to re-instantiate the run for example.
+
+    .. note:: By default, ``training_step`` (used by ``pytorch-lightning`` in the
+        training loop) and ``validation_step`` (used for the validation loop)
+        share ``common_step``. If you want different behavior for the training
+        loop and the validation loop, overwrite both ``training_step`` and
+        ``validation_step`` instead.
+
+    For more info on its methods, properties and hooks, have a look at lightning's docs:
+    https://pytorch-lightning.readthedocs.io/en/stable/lightning_module.html#lightningmodule-api
+    """
+
+    default_monitor: str = "val_loss"
+
+    def __init__(
+        self,
+        model,
+        optimizer,
+        loss_func,
+        train_loader,
+        val_loader=None,
+        scheduler=None,
+        config=None,
+    ):
+        super().__init__()
+        self.model = model
+        self.optimizer = optimizer
+        self.loss_func = loss_func
+        self.train_loader = train_loader
+        self.val_loader = val_loader
+        self.scheduler = scheduler
+        self.config = {} if config is None else config
+        # Save lightning's AttributeDict under self.hparams
+        self.save_hyperparameters(self.config_to_hparams(self.config))
+
+    def forward(self, *args, **kwargs):
+        """Applies forward pass of the model.
+
+        Returns:
+            :class:`torch.Tensor`
+        """
+        return self.model(*args, **kwargs)
+
+    def common_step(self, batch, batch_nb, train=True):
+        """Common forward step between training and validation.
+
+        The function of this method is to unpack the data given by the loader,
+        forward the batch through the model and compute the loss.
+        Pytorch-lightning handles all the rest.
+
+        Args:
+            batch: the object returned by the loader (a list of torch.Tensor
+                in most cases) but can be something else.
+            batch_nb (int): The number of the batch in the epoch.
+            train (bool): Whether in training mode. Needed only if the training
+                and validation steps are fundamentally different, otherwise,
+                pytorch-lightning handles the usual differences.
+
+        Returns:
+            :class:`torch.Tensor` : The loss value on this batch.
+
+        .. note::
+            This is typically the method to overwrite when subclassing
+            ``System``. If the training and validation steps are somehow
+            different (except for ``loss.backward()`` and ``optimzer.step()``),
+            the argument ``train`` can be used to switch behavior.
+            Otherwise, ``training_step`` and ``validation_step`` can be overwriten.
+        """
+        inputs, targets = batch
+        est_targets = self(inputs)
+        loss = self.loss_func(est_targets, targets)
+        return loss
+
+    def training_step(self, batch, batch_nb):
+        """Pass data through the model and compute the loss.
+
+        Backprop is **not** performed (meaning PL will do it for you).
+
+        Args:
+            batch: the object returned by the loader (a list of torch.Tensor
+                in most cases) but can be something else.
+            batch_nb (int): The number of the batch in the epoch.
+
+        Returns:
+            torch.Tensor, the value of the loss.
+        """
+        loss = self.common_step(batch, batch_nb, train=True)
+        self.log("loss", loss, logger=True)
+        return loss
+
+    def validation_step(self, batch, batch_nb):
+        """Need to overwrite PL validation_step to do validation.
+
+        Args:
+            batch: the object returned by the loader (a list of torch.Tensor
+                in most cases) but can be something else.
+            batch_nb (int): The number of the batch in the epoch.
+        """
+        loss = self.common_step(batch, batch_nb, train=False)
+        self.log("val_loss", loss, on_epoch=True, prog_bar=True)
+
+    def on_validation_epoch_end(self):
+        """Log hp_metric to tensorboard for hparams selection."""
+        hp_metric = self.trainer.callback_metrics.get("val_loss", None)
+        if hp_metric is not None:
+            self.trainer.logger.log_metrics(
+                {"hp_metric": hp_metric}, step=self.trainer.global_step
+            )
+
+    def configure_optimizers(self):
+        """Initialize optimizers, batch-wise and epoch-wise schedulers."""
+        if self.scheduler is None:
+            return self.optimizer
+
+        if not isinstance(self.scheduler, (list, tuple)):
+            self.scheduler = [self.scheduler]  # support multiple schedulers
+
+        epoch_schedulers = []
+        for sched in self.scheduler:
+            if not isinstance(sched, dict):
+                if isinstance(sched, ReduceLROnPlateau):
+                    sched = {"scheduler": sched, "monitor": self.default_monitor}
+                epoch_schedulers.append(sched)
+            else:
+                sched.setdefault("monitor", self.default_monitor)
+                sched.setdefault("frequency", 1)
+                # Backward compat
+                if sched["interval"] == "batch":
+                    sched["interval"] = "step"
+                assert sched["interval"] in [
+                    "epoch",
+                    "step",
+                ], "Scheduler interval should be either step or epoch"
+                epoch_schedulers.append(sched)
+        return [self.optimizer], epoch_schedulers
+
+    def lr_scheduler_step(self, scheduler, metric):
+        if metric is None:
+            scheduler.step()
+        else:
+            scheduler.step(metric)
+
+    def train_dataloader(self):
+        """Training dataloader"""
+        return self.train_loader
+
+    def val_dataloader(self):
+        """Validation dataloader"""
+        return self.val_loader
+
+    def on_save_checkpoint(self, checkpoint):
+        """Overwrite if you want to save more things in the checkpoint."""
+        checkpoint["training_config"] = self.config
+        return checkpoint
+
+    @staticmethod
+    def config_to_hparams(dic):
+        """Sanitizes the config dict to be handled correctly by torch
+        SummaryWriter. It flatten the config dict, converts ``None`` to
+        ``"None"`` and any list and tuple into torch.Tensors.
+
+        Args:
+            dic (dict): Dictionary to be transformed.
+
+        Returns:
+            dict: Transformed dictionary.
+        """
+        dic = flatten_dict(dic)
+        for k, v in dic.items():
+            if v is None:
+                dic[k] = str(v)
+            elif isinstance(v, (list, tuple)):
+                dic[k] = torch.tensor(v)
+        return dic
diff --git a/recipes/cad2/task1/ConvTasNet/local/tasnet.py b/recipes/cad2/task1/ConvTasNet/local/tasnet.py
new file mode 100644
index 000000000..23c1a2c57
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/tasnet.py
@@ -0,0 +1,534 @@
+# Copyright (c) Facebook, Inc. and its affiliates.
+# All rights reserved.
+#
+# This source code is licensed under the license found in the
+# LICENSE file in the root directory of this source tree.
+#
+# Created on 2018/12
+# Author: Kaituo XU
+# Modified on 2019/11 by Alexandre Defossez, added support for multiple output channels
+# Here is the original license:
+# The MIT License (MIT)
+#
+# Copyright (c) 2018 Kaituo XU
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import math
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from huggingface_hub import PyTorchModelHubMixin
+
+EPS = 1e-8
+
+
+def overlap_and_add(signal, frame_step):
+    outer_dimensions = signal.size()[:-2]
+    frames, frame_length = signal.size()[-2:]
+
+    subframe_length = math.gcd(frame_length, frame_step)  # gcd=Greatest Common Divisor
+    subframe_step = frame_step // subframe_length
+    subframes_per_frame = frame_length // subframe_length
+    output_size = frame_step * (frames - 1) + frame_length
+    output_subframes = output_size // subframe_length
+
+    subframe_signal = signal.view(*outer_dimensions, -1, subframe_length)
+
+    frame = torch.arange(0, output_subframes, device=signal.device).unfold(
+        0, subframes_per_frame, subframe_step
+    )
+    frame = frame.long()  # signal may in GPU or CPU
+    frame = frame.contiguous().view(-1)
+
+    result = signal.new_zeros(*outer_dimensions, output_subframes, subframe_length)
+    result.index_add_(-2, frame, subframe_signal)
+    result = result.view(*outer_dimensions, -1)
+    return result
+
+
+class ConvTasNetStereo(nn.Module, PyTorchModelHubMixin):
+    def __init__(
+        self,
+        N=256,
+        L=20,
+        B=256,
+        H=512,
+        P=3,
+        X=8,
+        R=4,
+        C=4,
+        audio_channels=1,
+        samplerate=44100,
+        norm_type="gLN",
+        causal=False,
+        mask_nonlinear="relu",
+    ):
+        """
+        Args:
+            N: Number of filters in autoencoder
+            L: Length of the filters (in samples)
+            B: Number of channels in bottleneck 1 × 1-conv block
+            H: Number of channels in convolutional blocks
+            P: Kernel size in convolutional blocks
+            X: Number of convolutional blocks in each repeat
+            R: Number of repeats
+            C: Number of speakers
+            norm_type: BN, gLN, cLN
+            causal: causal or non-causal
+            mask_nonlinear: use which non-linear function to generate mask
+        """
+        super().__init__()
+        # Hyper-parameter
+        self.N, self.L, self.B, self.H, self.P, self.X, self.R, self.C = (
+            N,
+            L,
+            B,
+            H,
+            P,
+            X,
+            R,
+            C,
+        )
+        self.norm_type = norm_type
+        self.causal = causal
+        self.mask_nonlinear = mask_nonlinear
+        self.audio_channels = audio_channels
+        self.samplerate = samplerate
+        # Components
+        self.encoder = Encoder(L, N, audio_channels)
+        self.separator = TemporalConvNet(
+            N, B, H, P, X, R, C, norm_type, causal, mask_nonlinear
+        )
+        self.decoder = Decoder(N, L, audio_channels)
+        # init
+        for p in self.parameters():
+            if p.dim() > 1:
+                nn.init.xavier_normal_(p)
+
+    def valid_length(self, length):
+        return length
+
+    def forward(self, mixture):
+        """
+        Args:
+            mixture: [M, T], M is batch size, T is #samples
+        Returns:
+            est_source: [M, C, T]
+        """
+        mixture_w = self.encoder(mixture)
+        est_mask = self.separator(mixture_w)
+        est_source = self.decoder(mixture_w, est_mask)
+
+        # T changed after conv1d in encoder, fix it here
+        T_origin = mixture.size(-1)
+        T_conv = est_source.size(-1)
+        est_source = F.pad(est_source, (0, T_origin - T_conv))
+        return est_source
+
+    def serialize(self):
+        """Serialize model and output dictionary.
+
+        Returns:
+            dict, serialized model with keys `model_args` and `state_dict`.
+        """
+        import pytorch_lightning as pl  # Not used in torch.hub
+
+        model_conf = dict(
+            model_name=self.__class__.__name__,
+            state_dict=self.get_state_dict(),
+            # model_args=self.get_model_args(),
+        )
+        # Additional infos
+        infos = dict()
+        infos["software_versions"] = dict(
+            torch_version=torch.__version__,
+            pytorch_lightning_version=pl.__version__,
+            asteroid_version="0.7.0",
+        )
+        model_conf["infos"] = infos
+        return model_conf
+
+    def get_state_dict(self):
+        """In case the state dict needs to be modified before sharing the model."""
+        return self.state_dict()
+
+    def get_model_args(self):
+        """Arguments needed to re-instantiate the model."""
+        fb_config = self.encoder.filterbank.get_config()
+        masknet_config = self.masker.get_config()
+        # Assert both dict are disjoint
+        if not all(k not in fb_config for k in masknet_config):
+            raise AssertionError(
+                "Filterbank and Mask network config share common keys. Merging them is"
+                " not safe."
+            )
+        # Merge all args under model_args.
+        model_args = {
+            **fb_config,
+            **masknet_config,
+            "encoder_activation": self.encoder_activation,
+        }
+        return model_args
+
+
+class Encoder(nn.Module):
+    """Estimation of the nonnegative mixture weight by a 1-D conv layer."""
+
+    def __init__(self, L, N, audio_channels):
+        super().__init__()
+        # Hyper-parameter
+        self.L, self.N = L, N
+        # Components
+        # 50% overlap
+        self.conv1d_U = nn.Conv1d(
+            audio_channels, N, kernel_size=L, stride=L // 2, bias=False
+        )
+
+    def forward(self, mixture):
+        """
+        Args:
+            mixture: [M, T], M is batch size, T is #samples
+        Returns:
+            mixture_w: [M, N, K], where K = (T-L)/(L/2)+1 = 2T/L-1
+        """
+        mixture_w = F.relu(self.conv1d_U(mixture))  # [M, N, K]
+        return mixture_w
+
+
+class Decoder(nn.Module):
+    def __init__(self, N, L, audio_channels):
+        super().__init__()
+        # Hyper-parameter
+        self.N, self.L = N, L
+        self.audio_channels = audio_channels
+        # Components
+        self.basis_signals = nn.Linear(N, audio_channels * L, bias=False)
+
+    def forward(self, mixture_w, est_mask):
+        """
+        Args:
+            mixture_w: [M, N, K]
+            est_mask: [M, C, N, K]
+        Returns:
+            est_source: [M, C, T]
+        """
+        # D = W * M
+        source_w = torch.unsqueeze(mixture_w, 1) * est_mask  # [M, C, N, K]
+        source_w = torch.transpose(source_w, 2, 3)  # [M, C, K, N]
+        # S = DV
+        est_source = self.basis_signals(source_w)  # [M, C, K, ac * L]
+        m, c, k, _ = est_source.size()
+        est_source = (
+            est_source.view(m, c, k, self.audio_channels, -1)
+            .transpose(2, 3)
+            .contiguous()
+        )
+        est_source = overlap_and_add(est_source, self.L // 2)  # M x C x ac x T
+        return est_source
+
+
+class TemporalConvNet(nn.Module):
+    def __init__(
+        self, N, B, H, P, X, R, C, norm_type="gLN", causal=False, mask_nonlinear="relu"
+    ):
+        """
+        Args:
+            N: Number of filters in autoencoder
+            B: Number of channels in bottleneck 1 × 1-conv block
+            H: Number of channels in convolutional blocks
+            P: Kernel size in convolutional blocks
+            X: Number of convolutional blocks in each repeat
+            R: Number of repeats
+            C: Number of speakers
+            norm_type: BN, gLN, cLN
+            causal: causal or non-causal
+            mask_nonlinear: use which non-linear function to generate mask
+        """
+        super().__init__()
+        # Hyper-parameter
+        self.C = C
+        self.mask_nonlinear = mask_nonlinear
+        # Components
+        # [M, N, K] -> [M, N, K]
+        layer_norm = ChannelwiseLayerNorm(N)
+        # [M, N, K] -> [M, B, K]
+        bottleneck_conv1x1 = nn.Conv1d(N, B, 1, bias=False)
+        # [M, B, K] -> [M, B, K]
+        repeats = []
+        for _r in range(R):
+            blocks = []
+            for x in range(X):
+                dilation = 2**x
+                padding = (P - 1) * dilation if causal else (P - 1) * dilation // 2
+                blocks += [
+                    TemporalBlock(
+                        B,
+                        H,
+                        P,
+                        stride=1,
+                        padding=padding,
+                        dilation=dilation,
+                        norm_type=norm_type,
+                        causal=causal,
+                    )
+                ]
+            repeats += [nn.Sequential(*blocks)]
+        temporal_conv_net = nn.Sequential(*repeats)
+        # [M, B, K] -> [M, C*N, K]
+        mask_conv1x1 = nn.Conv1d(B, C * N, 1, bias=False)
+        # Put together
+        self.network = nn.Sequential(
+            layer_norm, bottleneck_conv1x1, temporal_conv_net, mask_conv1x1
+        )
+
+    def forward(self, mixture_w):
+        """
+        Keep this API same with TasNet
+        Args:
+            mixture_w: [M, N, K], M is batch size
+        returns:
+            est_mask: [M, C, N, K]
+        """
+        M, N, K = mixture_w.size()
+        score = self.network(mixture_w)  # [M, N, K] -> [M, C*N, K]
+        score = score.view(M, self.C, N, K)  # [M, C*N, K] -> [M, C, N, K]
+        if self.mask_nonlinear == "softmax":
+            est_mask = F.softmax(score, dim=1)
+        elif self.mask_nonlinear == "relu":
+            est_mask = F.relu(score)
+        else:
+            raise ValueError("Unsupported mask non-linear function")
+        return est_mask
+
+
+class TemporalBlock(nn.Module):
+    def __init__(
+        self,
+        in_channels,
+        out_channels,
+        kernel_size,
+        stride,
+        padding,
+        dilation,
+        norm_type="gLN",
+        causal=False,
+    ):
+        super().__init__()
+        # [M, B, K] -> [M, H, K]
+        conv1x1 = nn.Conv1d(in_channels, out_channels, 1, bias=False)
+        prelu = nn.PReLU()
+        norm = chose_norm(norm_type, out_channels)
+        # [M, H, K] -> [M, B, K]
+        dsconv = DepthwiseSeparableConv(
+            out_channels,
+            in_channels,
+            kernel_size,
+            stride,
+            padding,
+            dilation,
+            norm_type,
+            causal,
+        )
+        # Put together
+        self.net = nn.Sequential(conv1x1, prelu, norm, dsconv)
+
+    def forward(self, x):
+        """
+        Args:
+            x: [M, B, K]
+        Returns:
+            [M, B, K]
+        """
+        residual = x
+        out = self.net(x)
+        # TODO: when P = 3 here works fine, but when P = 2 maybe need to pad?
+        return out + residual  # look like w/o F.relu is better than w/ F.relu
+        # return F.relu(out + residual)
+
+
+class DepthwiseSeparableConv(nn.Module):
+    def __init__(
+        self,
+        in_channels,
+        out_channels,
+        kernel_size,
+        stride,
+        padding,
+        dilation,
+        norm_type="gLN",
+        causal=False,
+    ):
+        super().__init__()
+        # Use `groups` option to implement depthwise convolution
+        # [M, H, K] -> [M, H, K]
+        depthwise_conv = nn.Conv1d(
+            in_channels,
+            in_channels,
+            kernel_size,
+            stride=stride,
+            padding=padding,
+            dilation=dilation,
+            groups=in_channels,
+            bias=False,
+        )
+        if causal:
+            chomp = Chomp1d(padding)
+        prelu = nn.PReLU()
+        norm = chose_norm(norm_type, in_channels)
+        # [M, H, K] -> [M, B, K]
+        pointwise_conv = nn.Conv1d(in_channels, out_channels, 1, bias=False)
+        # Put together
+        if causal:
+            self.net = nn.Sequential(depthwise_conv, chomp, prelu, norm, pointwise_conv)
+        else:
+            self.net = nn.Sequential(depthwise_conv, prelu, norm, pointwise_conv)
+
+    def forward(self, x):
+        """
+        Args:
+            x: [M, H, K]
+        Returns:
+            result: [M, B, K]
+        """
+        return self.net(x)
+
+
+class Chomp1d(nn.Module):
+    """To ensure the output length is the same as the input."""
+
+    def __init__(self, chomp_size):
+        super().__init__()
+        self.chomp_size = chomp_size
+
+    def forward(self, x):
+        """
+        Args:
+            x: [M, H, Kpad]
+        Returns:
+            [M, H, K]
+        """
+        return x[:, :, : -self.chomp_size].contiguous()
+
+
+def chose_norm(norm_type, channel_size):
+    """The input of normlization will be (M, C, K), where M is batch size,
+    C is channel size and K is sequence length.
+    """
+    if norm_type == "gLN":
+        return GlobalLayerNorm(channel_size)
+    elif norm_type == "cLN":
+        return ChannelwiseLayerNorm(channel_size)
+    elif norm_type == "id":
+        return nn.Identity()
+    else:  # norm_type == "BN":
+        # Given input (M, C, K), nn.BatchNorm1d(C) will accumulate statics
+        # along M and K, so this BN usage is right.
+        return nn.BatchNorm1d(channel_size)
+
+
+# TODO: Use nn.LayerNorm to impl cLN to speed up
+class ChannelwiseLayerNorm(nn.Module):
+    """Channel-wise Layer Normalization (cLN)"""
+
+    def __init__(self, channel_size):
+        super().__init__()
+        self.gamma = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.beta = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.reset_parameters()
+
+    def reset_parameters(self):
+        self.gamma.data.fill_(1)
+        self.beta.data.zero_()
+
+    def forward(self, y):
+        """
+        Args:
+            y: [M, N, K], M is batch size, N is channel size, K is length
+        Returns:
+            cLN_y: [M, N, K]
+        """
+        mean = torch.mean(y, dim=1, keepdim=True)  # [M, 1, K]
+        var = torch.var(y, dim=1, keepdim=True, unbiased=False)  # [M, 1, K]
+        cLN_y = self.gamma * (y - mean) / torch.pow(var + EPS, 0.5) + self.beta
+        return cLN_y
+
+
+class GlobalLayerNorm(nn.Module):
+    """Global Layer Normalization (gLN)"""
+
+    def __init__(self, channel_size):
+        super().__init__()
+        self.gamma = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.beta = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.reset_parameters()
+
+    def reset_parameters(self):
+        self.gamma.data.fill_(1)
+        self.beta.data.zero_()
+
+    def forward(self, y):
+        """
+        Args:
+            y: [M, N, K], M is batch size, N is channel size, K is length
+        Returns:
+            gLN_y: [M, N, K]
+        """
+        # TODO: in torch 1.0, torch.mean() support dim list
+        mean = y.mean(dim=1, keepdim=True).mean(dim=2, keepdim=True)  # [M, 1, 1]
+        var = (
+            (torch.pow(y - mean, 2)).mean(dim=1, keepdim=True).mean(dim=2, keepdim=True)
+        )
+        gLN_y = self.gamma * (y - mean) / torch.pow(var + EPS, 0.5) + self.beta
+        return gLN_y
+
+
+if __name__ == "__main__":
+    """Small sample to test the Conv-TasNet model."""
+    torch.manual_seed(123)
+    M, N, L, T = 2, 3, 4, 12
+    K = 2 * T // L - 1
+    B, H, P, X, R, C, norm_type, causal = 2, 3, 3, 3, 2, 2, "gLN", False
+    mixture = torch.randint(3, (M, T))
+    # test Encoder
+    encoder = Encoder(L, N, 1)
+    encoder.conv1d_U.weight.data = torch.randint(2, encoder.conv1d_U.weight.size())
+    mixture_w = encoder(mixture)
+    print("mixture", mixture)
+    print("U", encoder.conv1d_U.weight)
+    print("mixture_w", mixture_w)
+    print("mixture_w size", mixture_w.size())
+
+    # test TemporalConvNet
+    separator = TemporalConvNet(N, B, H, P, X, R, C, norm_type=norm_type, causal=causal)
+    est_mask = separator(mixture_w)
+    print("est_mask", est_mask)
+
+    # test Decoder
+    decoder = Decoder(N, L, audio_channels=1)
+    est_mask = torch.randint(2, (B, K, C, N))
+    est_source = decoder(mixture_w, est_mask)
+    print("est_source", est_source)
+
+    # test Conv-TasNet
+    conv_tasnet = ConvTasNetStereo(N, L, B, H, P, X, R, C, norm_type=norm_type)
+    est_source = conv_tasnet(mixture)
+    print("est_source", est_source)
+    print("est_source size", est_source.size())
diff --git a/recipes/cad2/task1/ConvTasNet/requirements.txt b/recipes/cad2/task1/ConvTasNet/requirements.txt
new file mode 100644
index 000000000..ea912483c
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/requirements.txt
@@ -0,0 +1,3 @@
+asteroid
+musdb
+museval
diff --git a/recipes/cad2/task1/ConvTasNet/train.py b/recipes/cad2/task1/ConvTasNet/train.py
new file mode 100644
index 000000000..3681dbff3
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/train.py
@@ -0,0 +1,219 @@
+"""
+Script to train causal and noncausal ConvTasNet on MUSDB18.
+Model is trained to separate the vocals from the background music.
+"""
+
+import argparse
+import json
+import os
+from pathlib import Path
+
+import pytorch_lightning as pl
+import torch
+import yaml
+from asteroid.engine.system import System
+from pytorch_lightning.callbacks import EarlyStopping, ModelCheckpoint
+from torch.optim.lr_scheduler import ReduceLROnPlateau
+from torch.utils.data import DataLoader
+
+from recipes.cad2.task1.ConvTasNet.local import (
+    Compose,
+    ConvTasNetStereo,
+    MUSDB18Dataset,
+    augment_channelswap,
+    augment_gain,
+)
+
+validation_tracks = [
+    "Actions - One Minute Smile",
+    "Clara Berry And Wooldog - Waltz For My Victims",
+    "Johnny Lokke - Promises & Lies",
+    "Patrick Talbot - A Reason To Leave",
+    "Triviul - Angelsaint",
+    "Alexander Ross - Goodbye Bolero",
+    "Fergessen - Nos Palpitants",
+    "Leaf - Summerghost",
+    "Skelpolu - Human Mistakes",
+    "Young Griffo - Pennies",
+    "ANiMAL - Rockshow",
+    "James May - On The Line",
+    "Meaxic - Take A Step",
+    "Traffic Experiment - Sirens",
+]
+
+# Keys which are not in the conf.yml file can be added here.
+# In the hierarchical dictionary created when parsing, the key `key` can be
+# found at dic['main_args'][key]
+# By default train.py will use all available GPUs. The `id` option in run.sh
+# will limit the number of available GPUs for train.py .
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    "--exp_dir", default="exp/tmp", help="Full path to save best validation model"
+)
+
+
+def create_datasets_and_loaders(conf: dict) -> tuple:
+    """Create train and val datasets and loaders."""
+    source_augmentations = Compose([augment_gain, augment_channelswap])
+
+    dataset_kwargs = {
+        "root": Path(conf["data"]["root"]),
+        "mix_background": conf["data"]["mix_background"],
+        "sample_rate": conf["data"]["sample_rate"],
+    }
+
+    train_set = MUSDB18Dataset(
+        split="train",
+        random_segments=True,
+        random_track_mix=True,
+        exclude_tracks=validation_tracks,
+        source_augmentations=source_augmentations,
+        segment=conf["data"]["segment"],
+        samples_per_track=conf["data"]["samples_per_track"],
+        **dataset_kwargs,
+    )
+
+    val_set = MUSDB18Dataset(
+        split="train",
+        subset=validation_tracks,
+        segment=None,
+        samples_per_track=1,
+        **dataset_kwargs,
+    )
+
+    train_loader = DataLoader(
+        train_set,
+        shuffle=True,
+        batch_size=conf["training"]["batch_size"],
+        num_workers=conf["training"]["num_workers"],
+        drop_last=True,
+        pin_memory=True,
+    )
+
+    val_loader = DataLoader(
+        val_set,
+        shuffle=False,
+        batch_size=1,
+        num_workers=conf["training"]["num_workers"],
+        pin_memory=True,
+    )
+
+    return train_loader, train_set, val_loader, val_set
+
+
+def create_model_and_optimizer(conf: dict) -> tuple:
+    """Create model, optimizer and lr_scheduler."""
+    model = ConvTasNetStereo(
+        **conf["convtasnet"], samplerate=conf["data"]["sample_rate"]
+    )
+    optimizer = torch.optim.Adam(model.parameters(), lr=conf["optim"]["lr"])
+    scheduler = None
+    if conf["training"]["half_lr"]:
+        scheduler = ReduceLROnPlateau(optimizer=optimizer, factor=0.5, patience=5)
+    return model, optimizer, scheduler
+
+
+def create_trainer(conf: dict, callbacks: list) -> pl.Trainer:
+    """Create PyTorch Lightning trainer."""
+    return pl.Trainer(
+        max_epochs=conf["training"]["epochs"],
+        callbacks=callbacks,
+        default_root_dir=conf["main_args"]["exp_dir"],
+        accelerator="gpu" if torch.cuda.is_available() else "cpu",
+        strategy="auto",
+        devices="auto",
+        gradient_clip_val=5.0,
+        accumulate_grad_batches=conf["training"]["aggregate"],
+    )
+
+
+def get_loss_func() -> torch.nn.L1Loss:
+    """Return the loss function."""
+    return torch.nn.L1Loss()
+
+
+def create_callbacks(conf: dict, exp_dir: str) -> tuple:
+    """Create callbacks for the training."""
+    # Define callbacks
+    callbacks = []
+    checkpoint_dir = os.path.join(exp_dir, "checkpoints/")
+    checkpoint = ModelCheckpoint(
+        checkpoint_dir, monitor="val_loss", mode="min", save_top_k=10, verbose=True
+    )
+    callbacks.append(checkpoint)
+    if conf["training"]["early_stop"]:
+        callbacks.append(
+            EarlyStopping(monitor="val_loss", mode="min", patience=20, verbose=True)
+        )
+    return callbacks, checkpoint
+
+
+def save_best_model(system, checkpoint, exp_dir, train_set):
+    best_k = {k: v.item() for k, v in checkpoint.best_k_models.items()}
+    with open(os.path.join(exp_dir, "best_k_models.json"), "w") as f:
+        json.dump(best_k, f, indent=0)
+
+    state_dict = torch.load(checkpoint.best_model_path)
+    system.load_state_dict(state_dict=state_dict["state_dict"])
+    system.cpu()
+
+    to_save = system.model.serialize()
+    to_save.update(train_set.get_infos())
+    torch.save(to_save, os.path.join(exp_dir, "best_model.pth"))
+
+
+def load_config(config_path):
+    with open(config_path) as f:
+        return yaml.safe_load(f)
+
+
+def main(conf):
+    train_loader, train_set, val_loader, _ = create_datasets_and_loaders(conf)
+    model, optimizer, scheduler = create_model_and_optimizer(conf)
+
+    # Just after instantiating, save the args. Easy loading in the future.
+    exp_dir = conf["main_args"]["exp_dir"]
+    os.makedirs(exp_dir, exist_ok=True)
+    conf_path = os.path.join(exp_dir, "conf.yml")
+    with open(conf_path, "w") as outfile:
+        yaml.safe_dump(conf, outfile)
+
+    # Define Loss function.
+    loss_func = get_loss_func()
+    system = System(
+        model=model,
+        loss_func=loss_func,
+        optimizer=optimizer,
+        train_loader=train_loader,
+        val_loader=val_loader,
+        scheduler=scheduler,
+        config=conf,
+    )
+
+    # Define callbacks
+    callbacks, checkpoint = create_callbacks(conf, exp_dir)
+    trainer = create_trainer(conf, callbacks)
+    trainer.fit(system)
+
+    save_best_model(system, checkpoint, exp_dir, train_set)
+
+
+if __name__ == "__main__":
+    from pprint import pprint as print
+
+    from asteroid.utils import parse_args_as_dict, prepare_parser_from_dict
+
+    # We start with opening the config file conf.yml as a dictionary from
+    # which we can create parsers. Each top level key in the dictionary defined
+    # by the YAML file creates a group in the parser.
+    def_conf = load_config("local/conf.yml")
+    parser = prepare_parser_from_dict(def_conf, parser=parser)
+    # Arguments are then parsed into a hierarchical dictionary (instead of
+    # flat, as returned by argparse) to facilitate calls to the different
+    # asteroid methods (see in main).
+    # plain_args is the direct output of parser.parse_args() and contains all
+    # the attributes in an non-hierarchical structure. It can be useful to also
+    # have it so we included it here but it is not used.
+    arg_dic, plain_args = parse_args_as_dict(parser, return_plain_args=True)
+    print(arg_dic)
+    main(arg_dic)
diff --git a/recipes/cad2/task1/ConvTasNet/utils/parse_options.sh b/recipes/cad2/task1/ConvTasNet/utils/parse_options.sh
new file mode 100755
index 000000000..c2c3b31f2
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/utils/parse_options.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+# Copyright 2012  Johns Hopkins University (Author: Daniel Povey);
+#                 Arnab Ghoshal, Karel Vesely
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+# THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+# WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+# MERCHANTABLITY OR NON-INFRINGEMENT.
+# See the Apache 2 License for the specific language governing permissions and
+# limitations under the License.
+
+
+# Parse command-line options.
+# To be sourced by another script (as in ". parse_options.sh").
+# Option format is: --option-name arg
+# and shell variable "option_name" gets set to value "arg."
+# The exception is --help, which takes no arguments, but prints the
+# $help_message variable (if defined).
+
+
+###
+### The --config file options have lower priority to command line
+### options, so we need to import them first...
+###
+
+# Now import all the configs specified by command-line, in left-to-right order
+for ((argpos=1; argpos<$#; argpos++)); do
+  if [ "${!argpos}" == "--config" ]; then
+    argpos_plus1=$((argpos+1))
+    config=${!argpos_plus1}
+    [ ! -r $config ] && echo "$0: missing config '$config'" && exit 1
+    . $config  # source the config file.
+  fi
+done
+
+
+###
+### Now we process the command line options
+###
+while true; do
+  [ -z "${1:-}" ] && break;  # break if there are no arguments
+  case "$1" in
+    # If the enclosing script is called with --help option, print the help
+    # message and exit.  Scripts should put help messages in $help_message
+    --help|-h) if [ -z "$help_message" ]; then echo "No help found." 1>&2;
+      else printf "$help_message\n" 1>&2 ; fi;
+      exit 0 ;;
+    --*=*) echo "$0: options to scripts must be of the form --name value, got '$1'"
+      exit 1 ;;
+    # If the first command-line argument begins with "--" (e.g. --foo-bar),
+    # then work out the variable name as $name, which will equal "foo_bar".
+    --*) name=`echo "$1" | sed s/^--// | sed s/-/_/g`;
+      # Next we test whether the variable in question is undefned-- if so it's
+      # an invalid option and we die.  Note: $0 evaluates to the name of the
+      # enclosing script.
+      # The test [ -z ${foo_bar+xxx} ] will return true if the variable foo_bar
+      # is undefined.  We then have to wrap this test inside "eval" because
+      # foo_bar is itself inside a variable ($name).
+      eval '[ -z "${'$name'+xxx}" ]' && echo "$0: invalid option $1" 1>&2 && exit 1;
+
+      oldval="`eval echo \\$$name`";
+      # Work out whether we seem to be expecting a Boolean argument.
+      if [ "$oldval" == "true" ] || [ "$oldval" == "false" ]; then
+        was_bool=true;
+      else
+        was_bool=false;
+      fi
+
+      # Set the variable to the right value-- the escaped quotes make it work if
+      # the option had spaces, like --cmd "queue.pl -sync y"
+      eval $name=\"$2\";
+
+      # Check that Boolean-valued arguments are really Boolean.
+      if $was_bool && [[ "$2" != "true" && "$2" != "false" ]]; then
+        echo "$0: expected \"true\" or \"false\": $1 $2" 1>&2
+        exit 1;
+      fi
+      shift 2;
+      ;;
+  *) break;
+  esac
+done
+
+
+# Check for an empty argument to the --cmd option, which can easily occur as a
+# result of scripting errors.
+[ ! -z "${cmd+xxx}" ] && [ -z "$cmd" ] && echo "$0: empty argument to --cmd option" 1>&2 && exit 1;
+
+
+true; # so this script returns exit code 0.
\ No newline at end of file
diff --git a/recipes/cad2/task1/ConvTasNet/utils/prepare_python_env.sh b/recipes/cad2/task1/ConvTasNet/utils/prepare_python_env.sh
new file mode 100755
index 000000000..3dc223334
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/utils/prepare_python_env.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Usage ./utils/install_env.sh --install_dir A --asteroid_root B --pip_requires C
+install_dir=~
+asteroid_root=../../../../
+pip_requires=../../../requirements.txt  # Expects a requirement.txt
+
+. utils/parse_options.sh || exit 1
+
+mkdir -p $install_dir
+cd $install_dir
+echo "Download and install latest version of miniconda3 into ${install_dir}"
+wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
+
+bash Miniconda3-latest-Linux-x86_64.sh -b -p miniconda3
+pip_path=$PWD/miniconda3/bin/pip
+
+rm Miniconda3-latest-Linux-x86_64.sh
+cd -
+
+if [[ ! -z ${pip_requires} ]]; then
+	$pip_path install -r $pip_requires
+fi
+$pip_path install soundfile
+$pip_path install -e $asteroid_root
+#$pip_path install ${asteroid_root}/\[""evaluate""\]
+echo -e "\nAsteroid has been installed in editable mode. Feel free to apply your changes !"
\ No newline at end of file
diff --git a/recipes/cad2/task1/README.md b/recipes/cad2/task1/README.md
new file mode 100644
index 000000000..99c947a00
--- /dev/null
+++ b/recipes/cad2/task1/README.md
@@ -0,0 +1,122 @@
+# The Second Cadenza Challenge (CAD2) - Task 1: Lyrics Intelligibility
+
+Cadenza code for the Second Cadenza Challenge (CAD2) Task1.
+
+For more information please visit the [challenge website](https://cadenzachallenge.org/docs/cadenza2/intro).
+
+## 1. Data structure
+
+The Second Cadenza Challenge - task 1 is using the transcribed version of the MUSDB18-HQ dataset.
+This extension comprises 96 manual transcriptions of English songs by
+non-native English speakers, following the same split for training, validation and test as museval,
+totalling 366 minutes of audio.
+
+To download the data, please visit [here](https://forms.gle/BzGrtdzqLvdjH6ja8).
+The data is contained in the package `cadenza_cad2_task1_train.v1_0.tar.gz`.
+The package contains the transcribe version of the MUSDB18-HQ dataset in FLAC format
+and the metadata for the systems.
+
+Unpack packages under the same root directory using
+
+```bash
+tar -xvzf <PACKAGE_NAME>
+```
+
+### 1.1 Necessary data
+
+- **Music** contains the MUSDB18-HQ music dataset for training, validation and evaluation.
+
+```text
+cadenza_data
+└───cad2
+    └───task1
+        └───audio
+            └───musdb18hq
+                └───train
+                     ├───audio
+                     └───lyrics
+```
+
+- **Metadata** contains the metadata for the systems.
+
+```text
+cadenza_data
+└───cad2
+    └───task1
+        └───metadata
+            ├───alpha.json
+            ├───compressor_params.train.json
+            ├───compressor_params.valid.json
+            ├───listeners.train.json
+            ├───listeners.valid.json
+            ├───music.train.json
+            ├───music.valid.json
+            ├───scene.train.json
+            ├───scene.valid.json
+            ├───scene_listeners.train.json
+            └───scene_listeners.valid.json
+```
+
+## 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 enhancement system uses an audio source separation model to separate the vocals from the music.
+Next, using the alpha parameter, it changes the levels of the vocals and music to
+simulate different mixing scenarios. Then, uses a multiband dynamic range compressor to
+amplify the audio signal. The final signals is saved in FLAC, 44100 Hz, 16-bit format.
+
+The audio source separation model correspond to a ConvTasNet model trained on the MUSDB18-HQ dataset
+to separate the vocals from the background.
+
+To run the baseline enhancement system first, make sure that `paths.root` in `config.yaml` points to
+where you have installed the Cadenza data. You can also define your own `path.exp_folder`
+to store the enhanced signals and evaluated results.
+
+You can modify the code to add your own enhancement system or your own remixing strategy
+using the alpha parameter.
+
+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
+
+A csv file containing the HAAQI scores and Whisper correct transcribed words 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.
+This is due to the  random noises generated within HAAQI, but the differences should be sufficiently small.
+For reproducibility, in the given recipe, the random seed for each signal is set as the last eight digits
+of the song md5.
+
+### 2.3 ConvTasNet Source Separation
+
+In directory `ConvTasNet/` we provide the code to train the ConvTasNet model
+on the MUSDB18-HQ dataset.
+
+This recipe is based on the stereo modification of the ConvTasNet done by Alexandre Defossez
+and available in Demucs' branch [`v1`](https://github.com/facebookresearch/demucs/blob/110f8fee0815d4c0d4ed3e2d478e37df247cd269/demucs/tasnet.py)
+
+The training is based in an Asteroid recipe.
diff --git a/recipes/cad2/task1/baseline/config.yaml b/recipes/cad2/task1/baseline/config.yaml
index 9a169a109..8803f9a8c 100644
--- a/recipes/cad2/task1/baseline/config.yaml
+++ b/recipes/cad2/task1/baseline/config.yaml
@@ -1,7 +1,48 @@
 path:
-  root: ??
-  exp_folder: ./exp_${separator.model} # folder to store enhanced signals and final results
+  root: ???   # Set to the root of the dataset
+  metadata_dir: ${path.root}/metadata
+  music_dir: ${path.root}/audio
+  musics_file: ${path.metadata_dir}/music.valid.json
+  alphas_file: ${path.metadata_dir}/alpha.json
+  listeners_file: ${path.metadata_dir}/listeners.valid.json
+  enhancer_params_file: ${path.metadata_dir}/compressor_params.valid.json
+  scenes_file: ${path.metadata_dir}/scene.valid.json
+  scene_listeners_file: ${path.metadata_dir}/scene_listeners.valid.json
+  exp_folder: ./exp_${separator.causality} # folder to store enhanced signals and final results
 
 input_sample_rate: 44100   # sample rate of the input mixture
-output_sample_rate: 32000  # sample rate output stems
+remix_sample_rate: 44100  # sample rate for the output remixed signal
+HAAQI_sample_rate: 24000  # sample rate for computing HAAQI score
 
+separator:
+  causality: causal
+  device: ~
+  separation:
+    number_sources: 2
+    segment: 6.0
+    overlap: 0.1
+    sample_rate: ${input_sample_rate}
+
+enhancer:
+  crossover_frequencies: [ 353.55, 707.11, 1414.21, 2828.43, 5656.85 ] # [250, 500, 1000, 2000, 4000] * sqrt(2)
+  attack: [ 11, 11, 14, 13, 11, 11 ]
+  release: [ 80, 80, 80, 80, 100, 100 ]
+  threshold: [ -30, -30, -30, -30, -30, -30 ]
+
+soft_clip: False
+
+evaluate:
+  whisper_version: base.en
+  set_random_seed: True
+  small_test: False
+  save_intermediate: False
+  equiv_0db_spl: 100
+  batch_size: 1  # Number of batches
+  batch: 0       # Batch number to evaluate
+
+# hydra config
+hydra:
+  run:
+    dir: ${path.exp_folder}
+  job:
+    chdir: True
\ No newline at end of file
diff --git a/recipes/cad2/task1/baseline/enhance.py b/recipes/cad2/task1/baseline/enhance.py
index dfd2a4eef..d4155f531 100644
--- a/recipes/cad2/task1/baseline/enhance.py
+++ b/recipes/cad2/task1/baseline/enhance.py
@@ -2,17 +2,329 @@
 
 from __future__ import annotations
 
+import json
+import logging
+from pathlib import Path
+
 import hydra
+import numpy as np
+import torch
+from numpy import ndarray
 from omegaconf import DictConfig
+from torchaudio.transforms import Fade
+
+from clarity.enhancer.multiband_compressor import MultibandCompressor
+from clarity.utils.flac_encoder import read_flac_signal, save_flac_signal
+from recipes.cad2.task1.baseline.evaluate import (
+    make_scene_listener_list,
+    normalise_luft,
+)
+from recipes.cad2.task1.ConvTasNet.local.tasnet import ConvTasNetStereo
+
+logging.captureWarnings(True)
+logger = logging.getLogger(__name__)
+
+
+def separate_sources(
+    model: torch.nn.Module,
+    mix: torch.Tensor | ndarray,
+    sample_rate: int,
+    segment: float = 10.0,
+    overlap: float = 0.1,
+    number_sources: int = 4,
+    device: torch.device | str | None = None,
+):
+    """
+    Apply model to a given mixture.
+    Use fade, and add segments together in order to add model segment by segment.
+
+    Args:
+        model (torch.nn.Module): model to use for separation
+        mix (torch.Tensor): mixture to separate, shape (batch, channels, time)
+        sample_rate (int): sampling rate of the mixture
+        segment (float): segment length in seconds
+        overlap (float): overlap between segments, between 0 and 1
+        number_sources (int): number of sources to separate
+        device (torch.device, str, or None): if provided, device on which to
+            execute the computation, otherwise `mix.device` is assumed.
+            When `device` is different from `mix.device`, only local computations will
+            be on `device`, while the entire tracks will be stored on `mix.device`.
+
+    Returns:
+        torch.Tensor: estimated sources
+
+    Based on https://pytorch.org/audio/main/tutorials/hybrid_demucs_tutorial.html
+    """
+    device = mix.device if device is None else torch.device(device)
+    mix = torch.as_tensor(mix, dtype=torch.float, device=device)
+
+    if mix.ndim == 1:
+        # one track and mono audio
+        mix = mix.unsqueeze(0).unsqueeze(0)
+    elif mix.ndim == 2:
+        # one track and stereo audio
+        mix = mix.unsqueeze(0)
+
+    batch, channels, length = mix.shape
+
+    chunk_len = int(sample_rate * segment * (1 + overlap))
+    start = 0
+    end = chunk_len
+    overlap_frames = overlap * sample_rate
+    fade = Fade(fade_in_len=0, fade_out_len=int(overlap_frames), fade_shape="linear")
+
+    final = torch.zeros(batch, number_sources, channels, length, device=device)
+
+    while start < length - overlap_frames:
+        chunk = mix[:, :, start:end]
+        with torch.no_grad():
+            out = model.forward(chunk)
+        out = fade(out)
+        final[:, :, :, start:end] += out
+        if start == 0:
+            fade.fade_in_len = int(overlap_frames)
+            start += int(chunk_len - overlap_frames)
+        else:
+            start += chunk_len
+        end += chunk_len
+        if end >= length:
+            fade.fade_out_len = 0
+
+    return final
+
 
+def get_device(device: str) -> tuple:
+    """Get the Torch device.
 
-@hydra.main(config_path="", config_name="config")
+    Args:
+        device (str): device type, e.g. "cpu", "gpu0", "gpu1", etc.
+
+    Returns:
+        torch.device: torch.device() appropiate to the hardware available.
+        str: device type selected, e.g. "cpu", "cuda".
+    """
+    if device is None:
+        if torch.cuda.is_available():
+            return torch.device("cuda"), "cuda"
+        return torch.device("cpu"), "cpu"
+
+    if device.startswith("gpu"):
+        device_index = int(device.replace("gpu", ""))
+        if device_index > torch.cuda.device_count():
+            raise ValueError(f"GPU device index {device_index} is not available.")
+        return torch.device(f"cuda:{device_index}"), "cuda"
+
+    if device == "cpu":
+        return torch.device("cpu"), "cpu"
+
+    raise ValueError(f"Unsupported device type: {device}")
+
+
+def load_separation_model(causality: str, device: torch.device) -> ConvTasNetStereo:
+    """
+    Load the separation model.
+    Args:
+        causality (str): Causality of the model (causal or noncausal).
+        device (torch.device): Device to load the model.
+
+    Returns:
+        model: Separation model.
+    """
+    if causality == "causal":
+        model = ConvTasNetStereo.from_pretrained(
+            "cadenzachallenge/ConvTasNet_LyricsSeparation_Causal",
+            force_download=True,
+        ).to(device)
+    else:
+        model = ConvTasNetStereo.from_pretrained(
+            "cadenzachallenge/ConvTasNet_LyricsSeparation_NonCausal"
+        ).to(device)
+    return model
+
+
+def downmix_signal(
+    vocals: ndarray,
+    accompaniment: ndarray,
+    beta: float,
+) -> ndarray:
+    """
+    Downmix the vocals and accompaniment to stereo.
+    Args:
+        vocals (np.ndarray): Vocal signal.
+        accompaniment (np.ndarray): Accompaniment signal.
+        beta (float): Downmix parameter.
+
+    Returns:
+        np.ndarray: Downmixed signal.
+
+    Notes:
+        When beta is 0, the downmix is the accompaniment.
+        When beta is 1, the downmix is the vocals.
+    """
+    # Vocals +1Db, Accompaniment -1Db
+    # vocal amplification
+    vamp = beta**2 + 1
+    # accompaniment amplification
+    aamp = 2 - vamp
+    return vocals * vamp + accompaniment * aamp
+
+
+@hydra.main(config_path="", config_name="config", version_base=None)
 def enhance(config: DictConfig) -> None:
-    print(config)
+    """
+    Run the music enhancement.
+    The system decomposes the music into vocals and accompaniment.
+    Then, vocals are enhanced according to alpha values.
+    Finally, the music is amplified according hearing loss and downmix to stereo.
+
+    Args:
+        config (dict): Dictionary of configuration options for enhancing music.
 
-    pass
+    """
+    if config.separator.causality not in ["causal", "noncausal"]:
+        raise ValueError(
+            f"Causality must be causal or noncausal, {config.separator.causality} was"
+            " provided."
+        )
+
+    device, _ = get_device(config.separator.device)
+
+    # Set folder to save the enhanced music
+    enhanced_folder = Path("enhanced_signals")
+    enhanced_folder.mkdir(parents=True, exist_ok=True)
+
+    # Load listener dictionary
+    # To load the metadata of all listeners
+    # listener_dict = Listener.load_listener_dict(config.path.listeners_file)
+
+    # Load alphas
+    with Path(config.path.alphas_file).open("r", encoding="utf-8") as file:
+        alphas = json.load(file)
+
+    # Load scenes
+    with Path(config.path.scenes_file).open("r", encoding="utf-8") as file:
+        scenes = json.load(file)
+
+    # Load scene-listeners
+    with Path(config.path.scene_listeners_file).open("r", encoding="utf-8") as file:
+        scenes_listeners = json.load(file)
+
+    # Load songs
+    with Path(config.path.musics_file).open("r", encoding="utf-8") as file:
+        songs = json.load(file)
+
+    # Load compressor params
+    with Path(config.path.enhancer_params_file).open("r", encoding="utf-8") as file:
+        enhancer_params = json.load(file)
+
+    # Load separation model
+    separation_model = load_separation_model(config.separator.causality, device)
+
+    # create hearing aid
+    enhancer = MultibandCompressor(
+        crossover_frequencies=config.enhancer.crossover_frequencies,
+        sample_rate=config.input_sample_rate,
+    )
+
+    # Make the list of scene-listener pairings to process
+    scene_listener_pairs = make_scene_listener_list(
+        scenes_listeners, config.evaluate.small_test
+    )
+
+    scene_listener_pairs = scene_listener_pairs[
+        config.evaluate.batch :: config.evaluate.batch_size
+    ]
+
+    # Process each scene-listener pair
+    for idx, scene_listener_ids in enumerate(scene_listener_pairs, 1):
+        logger.info(
+            f"[{idx:04d}/{len(scene_listener_pairs):04d}] Processing scene-listener"
+            f" pair: {scene_listener_ids}"
+        )
+
+        scene_id, listener_id = scene_listener_ids
+        scene = scenes[scene_id]
+
+        # This recipe is not using the listener metadata
+        # But you can load it as follows:
+        # listener = listener_dict[listener_id]
+
+        alpha = alphas[scene["alpha"]]
+
+        # Load the music
+        input_mixture, input_sample_rate = read_flac_signal(
+            Path(config.path.music_dir)
+            / songs[scene["segment_id"]]["path"]
+            / "mixture.flac"
+        )
+        start_sample = int(
+            songs[scene["segment_id"]]["start_time"] * config.input_sample_rate
+        )
+        end_time = int(
+            (songs[scene["segment_id"]]["end_time"]) * config.input_sample_rate
+        )
+
+        input_mixture = input_mixture[start_sample:end_time, :]
+        assert input_sample_rate == config.input_sample_rate
+
+        # normalise input mixture to -40 dB LUFS
+        input_mixture = normalise_luft(
+            input_mixture, config.input_sample_rate, target_luft=-40
+        )
+
+        # Separate the music
+        est_sources = separate_sources(
+            separation_model,
+            input_mixture.T,
+            device=device,
+            **config.separator.separation,
+        )
+        vocals, accompaniment = est_sources.squeeze(0).cpu().detach().numpy()
+
+        # Get the listener's compressor params
+        mbc_params_listener: dict[str, dict] = {"left": {}, "right": {}}
+
+        for ear in ["left", "right"]:
+            mbc_params_listener[ear]["release"] = config.enhancer.release
+            mbc_params_listener[ear]["attack"] = config.enhancer.attack
+            mbc_params_listener[ear]["threshold"] = config.enhancer.threshold
+        mbc_params_listener["left"]["ratio"] = enhancer_params[listener_id]["cr_l"]
+        mbc_params_listener["right"]["ratio"] = enhancer_params[listener_id]["cr_r"]
+        mbc_params_listener["left"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_l"
+        ]
+        mbc_params_listener["right"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_r"
+        ]
+
+        # Downmix to stereo
+        enhanced_signal = downmix_signal(vocals, accompaniment, beta=alpha)
+
+        # Apply Amplification
+        enhancer.set_compressors(**mbc_params_listener["left"])
+        left_enhanced = enhancer(signal=enhanced_signal[0, :])
+
+        enhancer.set_compressors(**mbc_params_listener["right"])
+        right_enhanced = enhancer(signal=enhanced_signal[1, :])
+
+        enhanced_signal = np.stack((left_enhanced[0], right_enhanced[0]), axis=1)
+
+        # Save the enhanced music
+        filename = enhanced_folder / f"{scene_id}_{listener_id}_A{alpha}_remix.flac"
+        filename.parent.mkdir(parents=True, exist_ok=True)
+
+        save_flac_signal(
+            signal=enhanced_signal,
+            filename=filename,
+            signal_sample_rate=config.input_sample_rate,
+            output_sample_rate=config.remix_sample_rate,
+            do_clip_signal=True,
+            do_soft_clip=config.soft_clip,
+        )
 
 
 # pylint: disable = no-value-for-parameter
 if __name__ == "__main__":
     enhance()
+
+    logger.info("Enhancement completed.")
diff --git a/recipes/cad2/task1/baseline/evaluate.py b/recipes/cad2/task1/baseline/evaluate.py
index e69de29bb..e45e29e92 100644
--- a/recipes/cad2/task1/baseline/evaluate.py
+++ b/recipes/cad2/task1/baseline/evaluate.py
@@ -0,0 +1,400 @@
+"""Evaluate the enhanced signals using HAAQI and Whisper"""
+
+from __future__ import annotations
+
+import json
+import logging
+from pathlib import Path
+
+import hydra
+import numpy as np
+import pyloudnorm as pyln
+import torch.nn
+import whisper
+from jiwer import compute_measures
+from omegaconf import DictConfig
+
+from clarity.enhancer.multiband_compressor import MultibandCompressor
+from clarity.evaluator.haaqi import compute_haaqi
+from clarity.evaluator.msbg.msbg import Ear
+from clarity.utils.audiogram import Listener
+from clarity.utils.flac_encoder import read_flac_signal, save_flac_signal
+from clarity.utils.results_support import ResultsFile
+from clarity.utils.signal_processing import compute_rms, resample
+
+logger = logging.getLogger(__name__)
+
+
+def make_scene_listener_list(scenes_listeners: dict, small_test: bool = False) -> list:
+    """Make the list of scene-listener pairing to process
+
+    Args:
+        scenes_listeners (dict): Dictionary of scenes and listeners.
+        small_test (bool): Whether to use a small test set.
+
+    Returns:
+        list: List of scene-listener pairings.
+
+    """
+    scene_listener_pairs = [
+        (scene, listener)
+        for scene in scenes_listeners
+        for listener in scenes_listeners[scene]
+    ]
+
+    # Can define a standard 'small_test' with just 1/50 of the data
+    if small_test:
+        scene_listener_pairs = scene_listener_pairs[::400]
+
+    return scene_listener_pairs
+
+
+def compute_intelligibility(
+    enhanced_signal: np.ndarray,
+    segment_metadata: dict,
+    scorer: torch.nn.Module,
+    listener: Listener,
+    sample_rate: int,
+    save_intermediate: bool = False,
+    path_intermediate: str | Path | None = None,
+    equiv_0db_spl: float = 100,
+) -> tuple[float, float]:
+    """
+    Compute the Intelligibility score for the enhanced signal
+    using the Whisper model.
+
+    To the enhanced signal, we apply the MSGB hearing loss model
+    before transcribing with Whisper.
+
+    Args:
+        enhanced_signal: The enhanced signal
+        segment_metadata: The metadata of the segment
+        scorer: The Whisper model
+        listener: The listener
+        sample_rate: The sample rate of the signal
+        save_intermediate: Save the intermediate signal
+        path_intermediate: The path to save the intermediate signal
+        equiv_0db_spl: The equivalent 0 dB SPL
+
+    Returns:
+        The intelligibility score for the left and right channels
+    """
+    if path_intermediate is None:
+        path_intermediate = Path.cwd()
+    if isinstance(path_intermediate, str):
+        path_intermediate = Path(path_intermediate)
+
+    ear = Ear(
+        equiv_0db_spl=equiv_0db_spl,
+        sample_rate=sample_rate,
+    )
+
+    reference = segment_metadata["text"]
+
+    # Compute left ear
+    ear.set_audiogram(listener.audiogram_left)
+    enhanced_left = ear.process(enhanced_signal[:, 0])[0]
+    left_path = Path(f"{path_intermediate.as_posix()}_left.flac")
+    save_flac_signal(
+        enhanced_signal,
+        left_path,
+        44100,
+        sample_rate,
+    )
+    hipothesis = scorer.transcribe(left_path, fp16=False)["text"]
+    left_results = compute_measures(reference, hipothesis)
+
+    # Compute right ear
+    ear.set_audiogram(listener.audiogram_right)
+    enhanced_right = ear.process(enhanced_signal[:, 1])[0]
+    right_path = Path(f"{path_intermediate.as_posix()}_right.flac")
+    save_flac_signal(
+        enhanced_signal,
+        right_path,
+        44100,
+        sample_rate,
+    )
+    hipothesis = scorer.transcribe(right_path, fp16=False)["text"]
+    right_results = compute_measures(reference, hipothesis)
+
+    # Compute the average score for both ears
+    total_words = (
+        right_results["substitutions"]
+        + right_results["deletions"]
+        + right_results["hits"]
+    )
+
+    if save_intermediate:
+        enhanced_signal = np.stack([enhanced_left, enhanced_right], axis=1)
+        save_flac_signal(
+            enhanced_signal,
+            path_intermediate,
+            44100,
+            sample_rate,
+        )
+
+    Path(left_path).unlink()
+    Path(right_path).unlink()
+
+    return left_results["hits"] / total_words, right_results["hits"] / total_words
+
+
+def compute_quality(
+    reference_signal: np.ndarray,
+    enhanced_signal: np.ndarray,
+    listener: Listener,
+    config: DictConfig,
+) -> tuple[float, float]:
+    """Compute the HAAQI score for the left and right channels"""
+    scores = []
+
+    for channel in range(2):
+        audiogram = (
+            listener.audiogram_left if channel == 0 else listener.audiogram_right
+        )
+        s = compute_haaqi(
+            processed_signal=resample(
+                enhanced_signal[:, channel],
+                config.remix_sample_rate,
+                config.HAAQI_sample_rate,
+            ),
+            reference_signal=resample(
+                reference_signal[:, channel],
+                config.input_sample_rate,
+                config.HAAQI_sample_rate,
+            ),
+            processed_sample_rate=config.HAAQI_sample_rate,
+            reference_sample_rate=config.HAAQI_sample_rate,
+            audiogram=audiogram,
+            equalisation=2,
+            level1=65 - 20 * np.log10(compute_rms(reference_signal[:, channel])),
+        )
+        scores.append(s)
+
+    return scores[0], scores[1]
+
+
+def load_reference_signal(
+    path: str | Path,
+    start_sample: int | None,
+    end_sample: int | None,
+    level_luft: float = -40.0,
+) -> np.ndarray:
+    """Load the reference signal"""
+
+    if isinstance(path, str):
+        path = Path(path)
+
+    if start_sample is None:
+        start_sample = 0
+    if end_sample is None:
+        end_sample = -1
+
+    vocal, _ = read_flac_signal(path / "vocals.flac")
+    accompaniment = np.zeros_like(vocal)
+
+    for instrument in ["bass", "drums", "other"]:
+        instrument_signal, sample_rate = read_flac_signal(path / f"{instrument}.flac")
+        accompaniment += instrument_signal
+
+    mixture = vocal * 10 ** (1 / 20) + accompaniment * 10 ** (-1 / 20)
+    mixture = normalise_luft(mixture, sample_rate, level_luft)
+    return mixture[start_sample:end_sample, :]
+
+
+def normalise_luft(
+    signal: np.ndarray, sample_rate: float, target_luft=-40
+) -> np.ndarray:
+    """
+    Normalise the signal to a target loudness level.
+    Args:
+        signal: input signal to normalise
+        sample_rate: sample rate of the signal
+        target_luft: target loudness level in LUFS
+
+    Returns:
+        np.ndarray: normalised signal
+    """
+    level_meter = pyln.Meter(int(sample_rate))
+    input_level = level_meter.integrated_loudness(signal)
+    return signal * (10 ** ((target_luft - input_level) / 20))
+
+
+@hydra.main(config_path="", config_name="config", version_base=None)
+def run_compute_scores(config: DictConfig) -> None:
+    """Compute the scores for the enhanced signals"""
+
+    enhanced_folder = Path("enhanced_signals")
+    logger.info(f"Evaluating from {enhanced_folder} directory")
+
+    # Load listener audiograms and songs
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
+
+    # Load alphas
+    with Path(config.path.alphas_file).open("r", encoding="utf-8") as file:
+        alphas = json.load(file)
+
+    # Load scenes
+    with Path(config.path.scenes_file).open("r", encoding="utf-8") as file:
+        scenes = json.load(file)
+
+    # Load scene-listeners
+    with Path(config.path.scene_listeners_file).open("r", encoding="utf-8") as file:
+        scenes_listeners = json.load(file)
+
+    # Load songs
+    with Path(config.path.musics_file).open("r", encoding="utf-8") as file:
+        songs = json.load(file)
+
+    # Load compressor params
+    with Path(config.path.enhancer_params_file).open("r", encoding="utf-8") as file:
+        enhancer_params = json.load(file)
+
+    scores_headers = [
+        "scene",
+        "song",
+        "listener",
+        "haaqi_left",
+        "haaqi_right",
+        "haaqi_avg",
+        "whisper_left",
+        "whisper_rigth",
+        "whisper_be",
+        "alpha",
+        "score",
+    ]
+
+    if config.evaluate.batch_size == 1:
+        results_file = ResultsFile(
+            "scores.csv",
+            header_columns=scores_headers,
+        )
+    else:
+        results_file = ResultsFile(
+            f"scores_{config.evaluate.batch + 1}-{config.evaluate.batch_size}.csv",
+            header_columns=scores_headers,
+        )
+
+    # Create the list of scene-listener pairs
+    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
+    ]
+
+    # create hearing aid
+    enhancer = MultibandCompressor(
+        crossover_frequencies=config.enhancer.crossover_frequencies,
+        sample_rate=config.input_sample_rate,
+    )
+
+    intelligibility_scorer = whisper.load_model(config.evaluate.whisper_version)
+
+    # Loop over the scene-listener pairs
+    for idx, scene_listener_ids in enumerate(scene_listener_pairs, 1):
+        # Iterate over the scene-listener pairs
+        # The reference is the original signal
+        logger.info(
+            f"[{idx:04d}/{len(scene_listener_pairs):04d}] Processing scene-listener"
+            f" pair: {scene_listener_ids}"
+        )
+
+        scene_id, listener_id = scene_listener_ids
+
+        # Load scene details
+        scene = scenes[scene_id]
+        listener = listener_dict[listener_id]
+        alpha = alphas[scene["alpha"]]
+
+        #############################################################
+        # REFERENCE SIGNAL
+
+        # Load the reference signal
+        start_sample = int(
+            songs[scene["segment_id"]]["start_time"] * config.input_sample_rate
+        )
+        end_sample = int(
+            songs[scene["segment_id"]]["end_time"] * config.input_sample_rate
+        )
+        reference = load_reference_signal(
+            Path(config.path.music_dir) / songs[scene["segment_id"]]["path"],
+            start_sample,
+            end_sample,
+        )
+
+        # Get the listener's compressor params
+        mbc_params_listener: dict[str, dict] = {"left": {}, "right": {}}
+
+        for ear in ["left", "right"]:
+            mbc_params_listener[ear]["release"] = config.enhancer.release
+            mbc_params_listener[ear]["attack"] = config.enhancer.attack
+            mbc_params_listener[ear]["threshold"] = config.enhancer.threshold
+        mbc_params_listener["left"]["ratio"] = enhancer_params[listener_id]["cr_l"]
+        mbc_params_listener["right"]["ratio"] = enhancer_params[listener_id]["cr_r"]
+        mbc_params_listener["left"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_l"
+        ]
+        mbc_params_listener["right"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_r"
+        ]
+
+        # Apply compressor to reference signal
+        enhancer.set_compressors(**mbc_params_listener["left"])
+        left_reference = enhancer(signal=reference[:, 0])
+
+        enhancer.set_compressors(**mbc_params_listener["right"])
+        right_reference = enhancer(signal=reference[:, 1])
+
+        # Reference signal amplified
+        reference = np.stack([left_reference[0], right_reference[0]], axis=1)
+
+        #############################################################
+        # ENHANCED SIGNAL
+
+        # Load the enhanced signals
+        enhanced_signal_path = (
+            enhanced_folder / f"{scene_id}_{listener_id}_A{alpha}_remix.flac"
+        )
+        enhanced_signal, _ = read_flac_signal(enhanced_signal_path)
+
+        #############################################################
+        # COMPUTE SCORES
+
+        # Compute the HAAQI and Whisper scores
+        haaqi_scores = compute_quality(reference, enhanced_signal, listener, config)
+        whisper_scores = compute_intelligibility(
+            enhanced_signal=enhanced_signal,
+            segment_metadata=songs[scene["segment_id"]],
+            scorer=intelligibility_scorer,
+            listener=listener,
+            sample_rate=config.remix_sample_rate,
+            save_intermediate=config.evaluate.save_intermediate,
+            path_intermediate=enhanced_signal_path.parent
+            / f"{scene_id}_{listener_id}_A{alpha}_remix_hl.flac",
+            equiv_0db_spl=config.evaluate.equiv_0db_spl,
+        )
+
+        results_file.add_result(
+            {
+                "scene": scene_id,
+                "song": songs[scene["segment_id"]]["track_name"],
+                "listener": listener_id,
+                "haaqi_left": haaqi_scores[0],
+                "haaqi_right": haaqi_scores[1],
+                "haaqi_avg": np.mean(haaqi_scores),
+                "whisper_left": whisper_scores[0],
+                "whisper_rigth": whisper_scores[1],
+                "whisper_be": np.max(whisper_scores),
+                "alpha": alpha,
+                "score": alpha * np.max(whisper_scores)
+                + (1 - alpha) * np.mean(haaqi_scores),
+            }
+        )
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    run_compute_scores()
+
+    logger.info("Evaluation completed")
diff --git a/recipes/cad2/task1/requirements.txt b/recipes/cad2/task1/requirements.txt
new file mode 100644
index 000000000..12ce2cb5e
--- /dev/null
+++ b/recipes/cad2/task1/requirements.txt
@@ -0,0 +1,4 @@
+huggingface-hub
+jiwer
+openai-whisper
+safetensors
diff --git a/recipes/cad2/task2/ConvTasNet/README.md b/recipes/cad2/task2/ConvTasNet/README.md
new file mode 100644
index 000000000..f056ed8b0
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/README.md
@@ -0,0 +1,39 @@
+# CAD2-TASK2 target instrument/accompaniment separation model
+
+This recipe contains the necessary content to replicate the separation models used in CAD2-Task1.
+
+- The system is based on Asteroid Source Separation system.
+- ConvTasNet implementation is based on stereo adaptation by Alexandre Defossez <https://github.com/facebookresearch/demucs/blob/v1/demucs/tasnet.py>
+
+You can replicate the Causal and Non-Causal model by running:
+
+- **To replicate the Non-Causal model**
+
+```bash
+python train.py \
+    --exp_dir /path/to/save/exps \
+    --batch_size 4 \
+    --aggregate 2 \
+    --lr 0.0005 \
+    --root /path/to/MUSDB18 \
+    --sample_rate 44100 \
+    --segment 5.0 \
+    --samples_per_track 64
+```
+
+- **To replicate the Causal model**
+
+```bash
+python train.py \
+    --exp_dir /path/to/save/exps \
+    --batch_size 4 \
+    --aggregate 1 \
+    --lr 0.0005 \
+    --root /path/to/MUSDB18 \
+    --sample_rate 44100 \
+    --segment 4.0 \
+    --samples_per_track 64 \
+    --causal True \
+    --n_src 2 \
+    --norm_type cLN
+```
diff --git a/recipes/cad2/task2/ConvTasNet/__init__.py b/recipes/cad2/task2/ConvTasNet/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad2/task2/ConvTasNet/eval.py b/recipes/cad2/task2/ConvTasNet/eval.py
new file mode 100644
index 000000000..44cdee20b
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/eval.py
@@ -0,0 +1,188 @@
+"""Model evaluation script for Conv-TasNet."""
+
+import argparse
+import os
+import sys
+from pathlib import Path
+
+import museval
+import soundfile as sf
+import torch
+import yaml
+from local import ConvTasNetStereo, RebalanceMusicDataset
+from museval import TrackStore, evaluate
+from tqdm import tqdm
+
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    "--out_dir",
+    default="out",
+    type=str,
+    help="Directory in exp_dir where the eval results will be stored",
+)
+parser.add_argument(
+    "--use_gpu", type=int, default=0, help="Whether to use the GPU for model execution"
+)
+parser.add_argument("--exp_dir", default="exp/tmp", help="Experiment root")
+parser.add_argument(
+    "--n_save_ex",
+    type=int,
+    default=10,
+    help="Number of audio examples to save, -1 means all",
+)
+
+compute_metrics = ["si_sdr", "sdr", "sir", "sar"]
+
+
+def main(conf):
+    model_path = os.path.join(conf["exp_dir"], "best_model.pth")
+
+    model = ConvTasNetStereo(
+        **conf["train_conf"]["convtasnet"],
+        samplerate=conf["train_conf"]["data"]["sample_rate"],
+    )
+
+    saved = torch.load(model_path, map_location="cpu")
+    model.load_state_dict(saved["state_dict"])
+
+    # Handle device placement
+    if conf["use_gpu"]:
+        model.cuda()
+
+    model_device = next(model.parameters()).device
+
+    # Evaluation is mode using 'remix' mixture
+    target_inst = conf["train_conf"]["data"]["target"]
+    dataset_kwargs = {
+        "root_path": Path(conf["train_conf"]["data"]["root_path"]),
+        "sample_rate": conf["train_conf"]["data"]["sample_rate"],
+        "target": target_inst,
+    }
+
+    test_set = RebalanceMusicDataset(
+        split="test",
+        music_tracks_file=(
+            f"{conf['train_conf']['data']['music_tracks_file']}/music.eval.json"
+        ),
+        **dataset_kwargs,
+        segment_length=None,
+        samples_per_track=1,
+        random_segments=False,
+        random_track_mix=False,
+    )
+
+    # Randomly choose the indexes of sentences to save.
+    eval_save_dir = os.path.join(conf["exp_dir"], conf["out_dir"])
+    Path(eval_save_dir).mkdir(exist_ok=True, parents=True)
+
+    txtout = os.path.join(eval_save_dir, "results.txt")
+    fp = open(txtout, "w")
+
+    results = museval.EvalStore(frames_agg="median", tracks_agg="median")
+
+    torch.no_grad().__enter__()
+    for idx in tqdm(range(len(test_set))):
+        # Forward the network on the mixture.
+        mix, sources = test_set[idx]
+        mix = mix.to(model_device)
+        est_sources = model.forward(mix.unsqueeze(0))
+        sources_np = sources.cpu().data.numpy()
+        est_sources_np = est_sources.squeeze(0).cpu().data.numpy()
+
+        estimates = {}
+        estimates[target_inst] = est_sources_np[0].T
+        estimates["accompaniment"] = est_sources_np[1].T
+
+        references = {}
+        references[target_inst] = sources_np[0].T
+        references["accompaniment"] = sources_np[1].T
+
+        output_path = Path(os.path.join(eval_save_dir, test_set.track_name))
+        output_path.mkdir(exist_ok=True, parents=True)
+
+        print(f"Processing... {test_set.track_name}", file=sys.stderr)
+        print(test_set.track_name, file=fp)
+
+        for target, estimate in estimates.items():
+            sf.write(
+                str(output_path / Path(target).with_suffix(".wav")),
+                estimate,
+                conf["train_conf"]["data"]["sample_rate"],
+            )
+        # Evaluate using museval
+        track_scores = eval_track(
+            references,
+            estimates,
+            eval_targets=[target_inst, "accompaniment"],
+            track_name=test_set.track_name,
+            sample_rate=conf["sample_rate"],
+        )
+
+        results.add_track(track_scores.df)
+        print(track_scores, file=sys.stderr)
+        print(track_scores, file=fp)
+    print(results, file=sys.stderr)
+    print(results, file=fp)
+    results.save(os.path.join(eval_save_dir, "results.pandas"))
+    results.frames_agg = "mean"
+    print(results, file=sys.stderr)
+    print(results, file=fp)
+    fp.close()
+
+
+def eval_track(
+    reference,
+    user_estimates,
+    eval_targets=None,
+    track_name="",
+    mode="v4",
+    win=1.0,
+    hop=1.0,
+    sample_rate=44100,
+):
+    if eval_targets is None:
+        eval_targets = []
+    audio_estimates = []
+    audio_reference = []
+
+    data = TrackStore(win=win, hop=hop, track_name=track_name)
+
+    if len(eval_targets) >= 2:
+        # compute evaluation of remaining targets
+        for target in eval_targets:
+            audio_estimates.append(user_estimates[target] + 1e-15)
+            audio_reference.append(reference[target] + 1e-15)
+
+        SDR, ISR, SIR, SAR = evaluate(
+            audio_reference,
+            audio_estimates,
+            win=int(win * sample_rate),
+            hop=int(hop * sample_rate),
+            mode=mode,
+        )
+
+        # iterate over all evaluation results except for vocals
+        for i, target in enumerate(eval_targets):
+            values = {
+                "SDR": SDR[i].tolist(),
+                "SIR": SIR[i].tolist(),
+                "ISR": ISR[i].tolist(),
+                "SAR": SAR[i].tolist(),
+            }
+
+            data.add_target(target_name=target, values=values)
+
+    return data
+
+
+if __name__ == "__main__":
+    args = parser.parse_args()
+    arg_dic = dict(vars(args))
+    # Load training config
+    conf_path = os.path.join(args.exp_dir, "conf.yml")
+    with open(conf_path) as f:
+        train_conf = yaml.safe_load(f)
+    arg_dic["sample_rate"] = train_conf["data"]["sample_rate"]
+    arg_dic["train_conf"] = train_conf
+
+    main(arg_dic)
diff --git a/recipes/cad2/task2/ConvTasNet/local/__init__.py b/recipes/cad2/task2/ConvTasNet/local/__init__.py
new file mode 100644
index 000000000..7b1b8f3df
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/local/__init__.py
@@ -0,0 +1,16 @@
+from .cad2task2_dataloader import (
+    Compose,
+    RebalanceMusicDataset,
+    augment_channelswap,
+    augment_gain,
+)
+from .tasnet import ConvTasNetStereo, overlap_and_add
+
+__all__ = [
+    "ConvTasNetStereo",
+    "overlap_and_add",
+    "RebalanceMusicDataset",
+    "Compose",
+    "augment_gain",
+    "augment_channelswap",
+]
diff --git a/recipes/cad2/task2/ConvTasNet/local/cad2task2_dataloader.py b/recipes/cad2/task2/ConvTasNet/local/cad2task2_dataloader.py
new file mode 100644
index 000000000..09d047701
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/local/cad2task2_dataloader.py
@@ -0,0 +1,331 @@
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+import librosa
+import numpy as np
+import soundfile as sf
+import torch
+from torch.utils import data
+
+
+class Compose:
+    """Composes several augmentation transforms.
+    Args:
+        augmentations: list of augmentations to compose.
+    """
+
+    def __init__(self, transforms):
+        self.transforms = transforms
+
+    def __call__(self, audio):
+        for transform in self.transforms:
+            audio = transform(audio)
+        return audio
+
+
+def augment_gain(audio, low=0.25, high=1.25):
+    """Applies a random gain to each source between `low` and `high`"""
+    if torch.FloatTensor(1).uniform_() < 0.5:
+        gain_1 = low + torch.rand(1) * (high - low)
+        gain_2 = low + torch.rand(1) * (high - low)
+        return torch.stack([audio[0] * gain_1, audio[1] * gain_2])
+
+    return audio
+
+
+def augment_channelswap(audio):
+    """Randomly swap channels of stereo sources"""
+    if audio.shape[0] == 2 and torch.FloatTensor(1).uniform_() < 0.5:
+        return torch.flip(audio, [0])
+
+    return audio
+
+
+def get_audio_durations(track_path: Path | str) -> float:
+    if isinstance(track_path, str):
+        track_path = Path(track_path)
+    return librosa.get_duration(path=track_path.as_posix())
+
+
+class RebalanceMusicDataset(data.Dataset):
+    """
+    Dataset to process EnsembleSet and CadenzaWoodwind datasets for CAD2 Task2 baseline
+    The dataset is composed of a target source and a random number of
+    accompaniment sources.
+
+    Args:
+        root_path (str): Path to the root directory of the dataset
+        music_tracks_file (str): Path to the json file containing the music tracks
+        target (str): Target source to be extracted
+        samples_per_track (int): Number of samples to extract from each track
+        segment_length (float): Length of the segment to extract
+        random_segments (bool): If True, extract random segments from the tracks
+        random_track_mix (bool): If True, mix random accompaniment tracks
+        split (str): Split of the dataset to use
+        sample_rate (int): Sample rate of the audio files
+
+    """
+
+    dataset_name = "EnsembleSet & CadenzaWoodwind"
+
+    def __init__(
+        self,
+        root_path: Path | str,
+        music_tracks_file: Path | str,
+        target: str,
+        samples_per_track: int = 1,
+        segment_length: float | None = 5.0,
+        random_segments=False,
+        random_track_mix=False,
+        split: str = "train",
+        source_augmentations=lambda audio: audio,
+        sample_rate: int = 44100,
+    ):
+        self.instruments = [
+            "Bassoon",
+            "Cello",
+            "Clarinet",
+            "Flute",
+            "Oboe",
+            "Sax",
+            "Viola",
+            "Violin",
+        ]
+        self.repeated_instruments: list[str] = []  # ["Violin", "Viola", "Flute", "Sax"]
+
+        if isinstance(root_path, str):
+            root_path = Path(root_path)
+
+        self.root_path: Path = root_path
+        if split == "train":
+            self.root_path = root_path / "train"
+
+        self.music_tracks_file = Path(music_tracks_file)
+
+        self.target = target
+
+        self.samples_per_track = samples_per_track
+        self.segment_length = segment_length
+        self.random_segments = random_segments
+        self.random_track_mix = random_track_mix
+        self.split = split
+        self.sample_rate = sample_rate
+        self.source_augmentations = source_augmentations
+
+        with open(self.music_tracks_file) as f:
+            self.tracks = json.load(f)
+        self.tracks_list = list(self.tracks.keys())
+
+        self.target_tracks = []
+        for k, v in self.tracks.items():
+            for type_source, source in v.items():
+                if type_source == "mixture":
+                    continue
+
+                if self.target in source["instrument"]:
+                    self.target_tracks.append(k)
+
+        if self.split == "train":
+            self.min_src = 1
+            self.max_src = 3
+
+            self.accompaniment_tracks: dict = {
+                i: [] for i in self.instruments if i != self.target
+            }
+            for k, v in self.tracks.items():
+                for type_source, source in v.items():
+                    if type_source == "mixture":
+                        continue
+
+                    if self.target not in source["instrument"]:
+                        self.accompaniment_tracks[
+                            source["instrument"].split("_")[0]
+                        ].append(k)
+
+    def __len__(self):
+        return len(self.target_tracks) * self.samples_per_track
+
+    def __getitem__(self, idx):
+        # assemble the mixture of target and interferers
+        audio_sources = {}
+        track_idx = idx // self.samples_per_track
+
+        # Load the target source
+        target_name = self.target_tracks[track_idx]
+
+        if self.split == "test":
+            self.track_name = target_name
+
+        accompaniment_tracks = []
+        target_track = None
+        for type_source, source in self.tracks[target_name].items():
+            if type_source == "mixture":
+                continue
+
+            if self.target in source["instrument"]:
+                target_track = source
+            else:
+                accompaniment_tracks.append(source)
+
+        segment_length = self.segment_length
+        if self.segment_length is None:
+            segment_length = target_track["duration"]
+
+        target_duration = target_track["duration"]
+        start = target_track["start"]
+
+        if self.random_segments:
+            start = np.floor(
+                np.random.uniform(0, int(target_duration) - segment_length)
+            )
+
+        segment_length = self.segment_length
+        if self.segment_length is None:
+            segment_length = target_track["duration"]
+
+        start_sample = int(start * self.sample_rate)
+        stop_sample = int((start + segment_length) * self.sample_rate)
+
+        target_signal, _ = sf.read(
+            self.root_path / target_track["track"],
+            always_2d=True,
+            start=start_sample,
+            stop=stop_sample,
+        )
+
+        # convert to torch tensor
+        target_signal = torch.tensor(target_signal.T, dtype=torch.float)
+        # target_signal = F.pad(target_signal, (target_len - target_signal.size(1), 0))
+        target_signal = self.source_augmentations(target_signal)
+        target_len = int(segment_length * self.sample_rate)
+        audio_sources["target"] = torch.zeros(2, target_len)
+        audio_sources["target"] = target_signal
+        audio_sources["accompaniment"] = torch.zeros_like(target_signal)
+
+        # ***************************************
+        # Load accompaniments
+        # If random mixture, change `accompaniment_tracks` variable with
+        # random instruments and tracks
+
+        if self.random_track_mix:
+            num_accomp = np.random.randint(self.min_src, self.max_src + 1)
+            accompaniment_instruments = np.random.choice(
+                [
+                    x
+                    for x in self.instruments + self.repeated_instruments
+                    if self.target not in x
+                ],
+                num_accomp,
+                replace=False,
+            )
+            accompaniment_tracks = []
+            for accomp_instrument in accompaniment_instruments:
+                a_t = np.random.choice(self.accompaniment_tracks[accomp_instrument])
+                for item in self.tracks[a_t].values():
+                    if accomp_instrument in item["instrument"]:
+                        accompaniment_tracks.append(item)
+
+        # Load accompaniment files
+        for accomp_track in accompaniment_tracks:
+            accomp_duration = accomp_track["duration"]
+
+            # new random segment
+            start = accomp_track["start"]
+            if self.random_segments:
+                start = np.floor(np.random.uniform(0, accomp_duration - segment_length))
+
+            start_sample = int(start * self.sample_rate)
+            stop_sample = int((start + segment_length) * self.sample_rate)
+
+            try:
+                accomp_signal, _ = sf.read(
+                    self.root_path / accomp_track["track"],
+                    always_2d=True,
+                    start=start_sample,
+                    stop=stop_sample,
+                )
+
+                accomp_signal = torch.tensor(accomp_signal.T, dtype=torch.float)
+
+                accomp_signal = self.source_augmentations(accomp_signal)
+
+                audio_sources["accompaniment"] += accomp_signal
+            except Exception as err:
+                print(accomp_track, flush=True)
+                print(f"start {start}", flush=True)
+                print(f"len {segment_length}", flush=True)
+                print(err)
+
+        # prepare mixture
+        audio_mix = torch.stack(list(audio_sources.values())).sum(0)
+
+        audio_sources = torch.stack(
+            [audio_sources["target"], audio_sources["accompaniment"]],
+            dim=0,
+        )
+        return audio_mix, audio_sources
+
+    def get_infos(self):
+        """Get dataset infos (for publishing models).
+
+        Returns:
+            dict, dataset infos with keys `dataset`, `task` and `licences`.
+        """
+        infos = dict()
+        infos["dataset"] = self.dataset_name
+        infos["task"] = "separation"
+        infos["licenses"] = [ensembleset_license, woodwing_licence]
+        return infos
+
+
+ensembleset_license = dict(
+    title="EnsembleSet",
+    title_link="https://zenodo.org/records/6519024",
+    author="Saurjya Sarkar, Emmanouil Benetos, Mark Sandler",
+    licence="CC BY-NC 4.0",
+    licence_link="https://creativecommons.org/licenses/by-nc/4.0/",
+    non_commercial=False,
+)
+
+woodwing_licence = dict(
+    title="CadenzaWoodwind",
+    title_link="",
+    author="Alex Miller, Trevor Cox, Gerardo Roa Dabike",
+    licence="CC NC 4.0",
+    licence_link="https://creativecommons.org/licenses/by/4.0/",
+    non_commercial=False,
+)
+
+
+if __name__ == "__main__":
+    _root_path = Path(
+        "/media/gerardoroadabike/Extreme"
+        " SSD1/Challenges/CAD2/cadenza_data/cad2/task2/audio"
+    )
+    _music_tracks_file = Path(
+        "/media/gerardoroadabike/Extreme"
+        " SSD1/Challenges/CAD2/cadenza_data/cad2/task2/metadata/music.valid.json"
+    )
+    for target_source in [
+        "Bassoon",
+        # "Cello",
+        # "Clarinet",
+        # "Flute",
+        # "Oboe",
+        # "Sax",
+        # "Viola",
+        # "Violin",
+    ]:
+        dataset = RebalanceMusicDataset(
+            _root_path,
+            _music_tracks_file,
+            target_source,
+            split="valid",
+            random_track_mix=False,
+            random_segments=False,
+            segment_length=None,
+        )
+        for x, y in dataset:
+            print(x.shape, y.shape)
diff --git a/recipes/cad2/task2/ConvTasNet/local/conf.yml b/recipes/cad2/task2/ConvTasNet/local/conf.yml
new file mode 100644
index 000000000..c039de7dd
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/local/conf.yml
@@ -0,0 +1,36 @@
+# ConvTasNet config
+convtasnet:
+  N: 256
+  L: 20
+  B: 256
+  H: 512
+  P: 3
+  X: 10
+  R: 4
+  C: 2
+  audio_channels: 2 #  stereo
+  norm_type: gLN
+  causal: False
+  mask_nonlinear: 'relu'
+
+# Training config
+training:
+  epochs: 200
+  batch_size: 4
+  num_workers: 4
+  half_lr: yes
+  early_stop: yes
+  aggregate: 1
+# Optim config
+optim:
+  lr: 0.0001
+# Data config
+data:
+  root_path: path/to/cad2/task2/audio/data # path to Cadenza dataset, audio folder
+  music_tracks_file: path/to/cad2/task2/metadata/data # path to metadata file in cadenza format
+  sample_rate: 44100
+  target: 'Bassoon'
+  samples_per_track: 64
+  segment_length: 3.0
+
+
diff --git a/recipes/cad2/task2/ConvTasNet/local/tasnet.py b/recipes/cad2/task2/ConvTasNet/local/tasnet.py
new file mode 100644
index 000000000..6e386c2be
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/local/tasnet.py
@@ -0,0 +1,533 @@
+# Copyright (c) Facebook, Inc. and its affiliates.
+# All rights reserved.
+#
+# This source code is licensed under the license found in the
+# LICENSE file in the root directory of this source tree.
+#
+# Created on 2018/12
+# Author: Kaituo XU
+# Modified on 2019/11 by Alexandre Defossez, added support for multiple output channels
+# Here is the original license:
+# The MIT License (MIT)
+#
+# Copyright (c) 2018 Kaituo XU
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in all
+# copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+# SOFTWARE.
+
+import math
+
+import torch
+import torch.nn as nn
+import torch.nn.functional as F
+from huggingface_hub import PyTorchModelHubMixin
+
+EPS = 1e-8
+
+
+def overlap_and_add(signal, frame_step):
+    outer_dimensions = signal.size()[:-2]
+    frames, frame_length = signal.size()[-2:]
+
+    subframe_length = math.gcd(frame_length, frame_step)  # gcd=Greatest Common Divisor
+    subframe_step = frame_step // subframe_length
+    subframes_per_frame = frame_length // subframe_length
+    output_size = frame_step * (frames - 1) + frame_length
+    output_subframes = output_size // subframe_length
+
+    subframe_signal = signal.view(*outer_dimensions, -1, subframe_length)
+
+    frame = torch.arange(0, output_subframes, device=signal.device).unfold(
+        0, subframes_per_frame, subframe_step
+    )
+    frame = frame.long()  # signal may in GPU or CPU
+    frame = frame.contiguous().view(-1)
+
+    result = signal.new_zeros(*outer_dimensions, output_subframes, subframe_length)
+    result.index_add_(-2, frame, subframe_signal)
+    result = result.view(*outer_dimensions, -1)
+    return result
+
+
+class ConvTasNetStereo(nn.Module, PyTorchModelHubMixin):
+    def __init__(
+        self,
+        N=256,
+        L=20,
+        B=256,
+        H=512,
+        P=3,
+        X=8,
+        R=4,
+        C=4,
+        audio_channels=1,
+        samplerate=44100,
+        norm_type="gLN",
+        causal=False,
+        mask_nonlinear="relu",
+    ):
+        """
+        Args:
+            N: Number of filters in autoencoder
+            L: Length of the filters (in samples)
+            B: Number of channels in bottleneck 1 × 1-conv block
+            H: Number of channels in convolutional blocks
+            P: Kernel size in convolutional blocks
+            X: Number of convolutional blocks in each repeat
+            R: Number of repeats
+            C: Number of speakers
+            norm_type: BN, gLN, cLN
+            causal: causal or non-causal
+            mask_nonlinear: use which non-linear function to generate mask
+        """
+        super().__init__()
+        # Hyper-parameter
+        self.N, self.L, self.B, self.H, self.P, self.X, self.R, self.C = (
+            N,
+            L,
+            B,
+            H,
+            P,
+            X,
+            R,
+            C,
+        )
+        self.norm_type = norm_type
+        self.causal = causal
+        self.mask_nonlinear = mask_nonlinear
+        self.audio_channels = audio_channels
+        self.samplerate = samplerate
+        # Components
+        self.encoder = Encoder(L, N, audio_channels)
+        self.separator = TemporalConvNet(
+            N, B, H, P, X, R, C, norm_type, causal, mask_nonlinear
+        )
+        self.decoder = Decoder(N, L, audio_channels)
+        # init
+        for p in self.parameters():
+            if p.dim() > 1:
+                nn.init.xavier_normal_(p)
+
+    def valid_length(self, length):
+        return length
+
+    def forward(self, mixture):
+        """
+        Args:
+            mixture: [M, T], M is batch size, T is #samples
+        Returns:
+            est_source: [M, C, T]
+        """
+        mixture_w = self.encoder(mixture)
+        est_mask = self.separator(mixture_w)
+        est_source = self.decoder(mixture_w, est_mask)
+
+        # T changed after conv1d in encoder, fix it here
+        T_origin = mixture.size(-1)
+        T_conv = est_source.size(-1)
+        est_source = F.pad(est_source, (0, T_origin - T_conv))
+        return est_source
+
+    def serialize(self):
+        """Serialize model and output dictionary.
+
+        Returns:
+            dict, serialized model with keys `model_args` and `state_dict`.
+        """
+        import pytorch_lightning as pl  # Not used in torch.hub
+
+        model_conf = dict(
+            model_name=self.__class__.__name__,
+            state_dict=self.get_state_dict(),
+            # model_args=self.get_model_args(),
+        )
+        # Additional infos
+        infos = dict()
+        infos["software_versions"] = dict(
+            torch_version=torch.__version__,
+            pytorch_lightning_version=pl.__version__,
+            asteroid_version="0.7.0",
+        )
+        model_conf["infos"] = infos
+        return model_conf
+
+    def get_state_dict(self):
+        """In case the state dict needs to be modified before sharing the model."""
+        return self.state_dict()
+
+    def get_model_args(self):
+        """Arguments needed to re-instantiate the model."""
+        fb_config = self.encoder.filterbank.get_config()
+        masknet_config = self.masker.get_config()
+        # Assert both dict are disjoint
+        if not all(k not in fb_config for k in masknet_config):
+            raise AssertionError(
+                "Filterbank and Mask network config share common keys. Merging them is"
+                " not safe."
+            )
+        # Merge all args under model_args.
+        model_args = {
+            **fb_config,
+            **masknet_config,
+            "encoder_activation": self.encoder_activation,
+        }
+        return model_args
+
+
+class Encoder(nn.Module):
+    """Estimation of the nonnegative mixture weight by a 1-D conv layer."""
+
+    def __init__(self, L, N, audio_channels):
+        super().__init__()
+        # Hyper-parameter
+        self.L, self.N = L, N
+        # Components
+        # 50% overlap
+        self.conv1d_U = nn.Conv1d(
+            audio_channels, N, kernel_size=L, stride=L // 2, bias=False
+        )
+
+    def forward(self, mixture):
+        """
+        Args:
+            mixture: [M, T], M is batch size, T is #samples
+        Returns:
+            mixture_w: [M, N, K], where K = (T-L)/(L/2)+1 = 2T/L-1
+        """
+        mixture_w = F.relu(self.conv1d_U(mixture))  # [M, N, K]
+        return mixture_w
+
+
+class Decoder(nn.Module):
+    def __init__(self, N, L, audio_channels):
+        super().__init__()
+        # Hyper-parameter
+        self.N, self.L = N, L
+        self.audio_channels = audio_channels
+        # Components
+        self.basis_signals = nn.Linear(N, audio_channels * L, bias=False)
+
+    def forward(self, mixture_w, est_mask):
+        """
+        Args:
+            mixture_w: [M, N, K]
+            est_mask: [M, C, N, K]
+        Returns:
+            est_source: [M, C, T]
+        """
+        # D = W * M
+        source_w = torch.unsqueeze(mixture_w, 1) * est_mask  # [M, C, N, K]
+        source_w = torch.transpose(source_w, 2, 3)  # [M, C, K, N]
+        # S = DV
+        est_source = self.basis_signals(source_w)  # [M, C, K, ac * L]
+        m, c, k, _ = est_source.size()
+        est_source = (
+            est_source.view(m, c, k, self.audio_channels, -1)
+            .transpose(2, 3)
+            .contiguous()
+        )
+        est_source = overlap_and_add(est_source, self.L // 2)  # M x C x ac x T
+        return est_source
+
+
+class TemporalConvNet(nn.Module):
+    def __init__(
+        self, N, B, H, P, X, R, C, norm_type="gLN", causal=False, mask_nonlinear="relu"
+    ):
+        """
+        Args:
+            N: Number of filters in autoencoder
+            B: Number of channels in bottleneck 1 × 1-conv block
+            H: Number of channels in convolutional blocks
+            P: Kernel size in convolutional blocks
+            X: Number of convolutional blocks in each repeat
+            R: Number of repeats
+            C: Number of speakers
+            norm_type: BN, gLN, cLN
+            causal: causal or non-causal
+            mask_nonlinear: use which non-linear function to generate mask
+        """
+        super().__init__()
+        # Hyper-parameter
+        self.C = C
+        self.mask_nonlinear = mask_nonlinear
+        # Components
+        # [M, N, K] -> [M, N, K]
+        layer_norm = ChannelwiseLayerNorm(N)
+        # [M, N, K] -> [M, B, K]
+        bottleneck_conv1x1 = nn.Conv1d(N, B, 1, bias=False)
+        # [M, B, K] -> [M, B, K]
+        repeats = []
+        for _r in range(R):
+            blocks = []
+            for x in range(X):
+                dilation = 2**x
+                padding = (P - 1) * dilation if causal else (P - 1) * dilation // 2
+                blocks += [
+                    TemporalBlock(
+                        B,
+                        H,
+                        P,
+                        stride=1,
+                        padding=padding,
+                        dilation=dilation,
+                        norm_type=norm_type,
+                        causal=causal,
+                    )
+                ]
+            repeats += [nn.Sequential(*blocks)]
+        temporal_conv_net = nn.Sequential(*repeats)
+        # [M, B, K] -> [M, C*N, K]
+        mask_conv1x1 = nn.Conv1d(B, C * N, 1, bias=False)
+        # Put together
+        self.network = nn.Sequential(
+            layer_norm, bottleneck_conv1x1, temporal_conv_net, mask_conv1x1
+        )
+
+    def forward(self, mixture_w):
+        """
+        Keep this API same with TasNet
+        Args:
+            mixture_w: [M, N, K], M is batch size
+        returns:
+            est_mask: [M, C, N, K]
+        """
+        M, N, K = mixture_w.size()
+        score = self.network(mixture_w)  # [M, N, K] -> [M, C*N, K]
+        score = score.view(M, self.C, N, K)  # [M, C*N, K] -> [M, C, N, K]
+        if self.mask_nonlinear == "softmax":
+            est_mask = F.softmax(score, dim=1)
+        elif self.mask_nonlinear == "relu":
+            est_mask = F.relu(score)
+        else:
+            raise ValueError("Unsupported mask non-linear function")
+        return est_mask
+
+
+class TemporalBlock(nn.Module):
+    def __init__(
+        self,
+        in_channels,
+        out_channels,
+        kernel_size,
+        stride,
+        padding,
+        dilation,
+        norm_type="gLN",
+        causal=False,
+    ):
+        super().__init__()
+        # [M, B, K] -> [M, H, K]
+        conv1x1 = nn.Conv1d(in_channels, out_channels, 1, bias=False)
+        prelu = nn.PReLU()
+        norm = chose_norm(norm_type, out_channels)
+        # [M, H, K] -> [M, B, K]
+        dsconv = DepthwiseSeparableConv(
+            out_channels,
+            in_channels,
+            kernel_size,
+            stride,
+            padding,
+            dilation,
+            norm_type,
+            causal,
+        )
+        # Put together
+        self.net = nn.Sequential(conv1x1, prelu, norm, dsconv)
+
+    def forward(self, x):
+        """
+        Args:
+            x: [M, B, K]
+        Returns:
+            [M, B, K]
+        """
+        residual = x
+        out = self.net(x)
+        # TODO: when P = 3 here works fine, but when P = 2 maybe need to pad?
+        return out + residual  # look like w/o F.relu is better than w/ F.relu
+        # return F.relu(out + residual)
+
+
+class DepthwiseSeparableConv(nn.Module):
+    def __init__(
+        self,
+        in_channels,
+        out_channels,
+        kernel_size,
+        stride,
+        padding,
+        dilation,
+        norm_type="gLN",
+        causal=False,
+    ):
+        super().__init__()
+        # Use `groups` option to implement depthwise convolution
+        # [M, H, K] -> [M, H, K]
+        depthwise_conv = nn.Conv1d(
+            in_channels,
+            in_channels,
+            kernel_size,
+            stride=stride,
+            padding=padding,
+            dilation=dilation,
+            groups=in_channels,
+            bias=False,
+        )
+        if causal:
+            chomp = Chomp1d(padding)
+        prelu = nn.PReLU()
+        norm = chose_norm(norm_type, in_channels)
+        # [M, H, K] -> [M, B, K]
+        pointwise_conv = nn.Conv1d(in_channels, out_channels, 1, bias=False)
+        # Put together
+        if causal:
+            self.net = nn.Sequential(depthwise_conv, chomp, prelu, norm, pointwise_conv)
+        else:
+            self.net = nn.Sequential(depthwise_conv, prelu, norm, pointwise_conv)
+
+    def forward(self, x):
+        """
+        Args:
+            x: [M, H, K]
+        Returns:
+            result: [M, B, K]
+        """
+        return self.net(x)
+
+
+class Chomp1d(nn.Module):
+    """To ensure the output length is the same as the input."""
+
+    def __init__(self, chomp_size):
+        super().__init__()
+        self.chomp_size = chomp_size
+
+    def forward(self, x):
+        """
+        Args:
+            x: [M, H, Kpad]
+        Returns:
+            [M, H, K]
+        """
+        return x[:, :, : -self.chomp_size].contiguous()
+
+
+def chose_norm(norm_type, channel_size):
+    """The input of normlization will be (M, C, K), where M is batch size,
+    C is channel size and K is sequence length.
+    """
+    if norm_type == "gLN":
+        return GlobalLayerNorm(channel_size)
+    elif norm_type == "cLN":
+        return ChannelwiseLayerNorm(channel_size)
+    elif norm_type == "id":
+        return nn.Identity()
+    else:  # norm_type == "BN":
+        # Given input (M, C, K), nn.BatchNorm1d(C) will accumulate statics
+        # along M and K, so this BN usage is right.
+        return nn.BatchNorm1d(channel_size)
+
+
+# TODO: Use nn.LayerNorm to impl cLN to speed up
+class ChannelwiseLayerNorm(nn.Module):
+    """Channel-wise Layer Normalization (cLN)"""
+
+    def __init__(self, channel_size):
+        super().__init__()
+        self.gamma = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.beta = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.reset_parameters()
+
+    def reset_parameters(self):
+        self.gamma.data.fill_(1)
+        self.beta.data.zero_()
+
+    def forward(self, y):
+        """
+        Args:
+            y: [M, N, K], M is batch size, N is channel size, K is length
+        Returns:
+            cLN_y: [M, N, K]
+        """
+        mean = torch.mean(y, dim=1, keepdim=True)  # [M, 1, K]
+        var = torch.var(y, dim=1, keepdim=True, unbiased=False)  # [M, 1, K]
+        cLN_y = self.gamma * (y - mean) / torch.pow(var + EPS, 0.5) + self.beta
+        return cLN_y
+
+
+class GlobalLayerNorm(nn.Module):
+    """Global Layer Normalization (gLN)"""
+
+    def __init__(self, channel_size):
+        super().__init__()
+        self.gamma = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.beta = nn.Parameter(torch.Tensor(1, channel_size, 1))  # [1, N, 1]
+        self.reset_parameters()
+
+    def reset_parameters(self):
+        self.gamma.data.fill_(1)
+        self.beta.data.zero_()
+
+    def forward(self, y):
+        """
+        Args:
+            y: [M, N, K], M is batch size, N is channel size, K is length
+        Returns:
+            gLN_y: [M, N, K]
+        """
+        # TODO: in torch 1.0, torch.mean() support dim list
+        mean = y.mean(dim=1, keepdim=True).mean(dim=2, keepdim=True)  # [M, 1, 1]
+        var = (
+            (torch.pow(y - mean, 2)).mean(dim=1, keepdim=True).mean(dim=2, keepdim=True)
+        )
+        gLN_y = self.gamma * (y - mean) / torch.pow(var + EPS, 0.5) + self.beta
+        return gLN_y
+
+
+if __name__ == "__main__":
+    torch.manual_seed(123)
+    M, N, L, T = 2, 3, 4, 12
+    K = 2 * T // L - 1
+    B, H, P, X, R, C, norm_type, causal = 2, 3, 3, 3, 2, 2, "gLN", False
+    mixture = torch.randint(3, (M, T))
+    # test Encoder
+    encoder = Encoder(L, N, 1)
+    encoder.conv1d_U.weight.data = torch.randint(2, encoder.conv1d_U.weight.size())
+    mixture_w = encoder(mixture)
+    print("mixture", mixture)
+    print("U", encoder.conv1d_U.weight)
+    print("mixture_w", mixture_w)
+    print("mixture_w size", mixture_w.size())
+
+    # test TemporalConvNet
+    separator = TemporalConvNet(N, B, H, P, X, R, C, norm_type=norm_type, causal=causal)
+    est_mask = separator(mixture_w)
+    print("est_mask", est_mask)
+
+    # test Decoder
+    decoder = Decoder(N, L, audio_channels=1)
+    est_mask = torch.randint(2, (B, K, C, N))
+    est_source = decoder(mixture_w, est_mask)
+    print("est_source", est_source)
+
+    # test Conv-TasNet
+    conv_tasnet = ConvTasNetStereo(N, L, B, H, P, X, R, C, norm_type=norm_type)
+    est_source = conv_tasnet(mixture)
+    print("est_source", est_source)
+    print("est_source size", est_source.size())
diff --git a/recipes/cad2/task2/ConvTasNet/run.sh b/recipes/cad2/task2/ConvTasNet/run.sh
new file mode 100755
index 000000000..f7d8c0833
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/run.sh
@@ -0,0 +1,104 @@
+#!/bin/bash
+
+set -e  # Exit on error
+set -o pipefail
+
+# Path to the python you'll use for the experiment. Defaults to the current python
+# You can run ./utils/prepare_python_env.sh to create a suitable python environment, paste the output here.
+python_path=python
+
+# Example usage
+# ./run.sh --stage 3 --tag my_tag
+
+# General
+stage=1  # Controls from which stage to start
+tag=""  # Controls the directory name associated to the experiment
+# You can ask for several GPUs using id (passed to CUDA_VISIBLE_DEVICES)
+id=$CUDA_VISIBLE_DEVICES
+out_dir=dampvsep # Controls the directory name associated to the evaluation results inside the experiment directory
+
+# Network config
+n_blocks=10
+n_repeats=4
+mask_act=relu
+
+# Training config
+epochs=200
+batch_size=7
+num_workers=10
+half_lr=yes
+early_stop=yes
+loss_alpha=0.5
+
+# Optim config
+optimizer=adam
+lr=0.0005
+weight_decay=0.
+
+# Data config
+task=separation   #'enh_vocal', 'separation'
+root_path=        # full path to DAMP-VSEP
+mixture=remix     # 'original' includes non-linear effects,
+                  # 'remix' add both sources together in runtime
+train_set=english   # 'english' uses the english subset ~77 hrs
+                    # 'singles' uses all singles performances subset ~174 hrs
+segment=3.0
+ex_per_track=5   # Number of examples obtained per track
+sample_rate=16000
+
+. utils/parse_options.sh
+
+if [[ $stage -le  0 ]]; then
+	echo "Stage 0: Generating preprocessed DAMPVSEP dataset"
+  . local/prepare_data.sh  --dampvsep_root $root_path
+fi
+
+# Generate a random ID for the run if no tag is specified
+uuid=$($python_path -c 'import uuid, sys; print(str(uuid.uuid4())[:8])')
+if [[ -z ${tag} ]]; then
+	tag=${uuid}
+fi
+
+expdir=exp/train_convtasnet_${tag}
+mkdir -p $expdir && echo $uuid >> $expdir/run_uuid.txt
+echo "Results from the following experiment will be stored in $expdir"
+
+if [[ $stage -le 1 ]]; then
+  echo "Stage 1: Training"
+  mkdir -p logs
+  CUDA_VISIBLE_DEVICES=$id $python_path train.py \
+        --exp_dir $expdir \
+        --n_blocks $n_blocks \
+        --n_repeats $n_repeats \
+        --mask_act $mask_act \
+        --epochs $epochs \
+        --batch_size $batch_size \
+        --num_workers $num_workers \
+        --half_lr $half_lr \
+        --early_stop $early_stop \
+        --loss_alpha $loss_alpha \
+        --optimizer $optimizer \
+        --lr $lr \
+        --weight_decay $weight_decay \
+        --task $task \
+        --root_path $root_path \
+        --mixture $mixture \
+        --sample_rate $sample_rate \
+        --train_set $train_set \
+        --segment $segment \
+        --ex_per_track $ex_per_track | tee logs/train_${tag}.log
+	cp logs/train_${tag}.log $expdir/train.log
+
+
+	# Get ready to publish
+	mkdir -p $expdir/publish_dir
+	echo "damp-vsep/ConvTasNet" > $expdir/publish_dir/recipe_name.txt
+fi
+
+
+if [[ $stage -le 2 ]]; then
+   echo "Stage 2 : Evaluation"
+  $python_path -u eval.py --exp_dir $expdir --use_gpu 1 \
+  	--out_dir $out_dir --n_save_ex 5 | tee logs/eval_${tag}.log
+	cp logs/eval_${tag}.log $expdir/eval.log
+fi
diff --git a/recipes/cad2/task2/ConvTasNet/train.py b/recipes/cad2/task2/ConvTasNet/train.py
new file mode 100644
index 000000000..d38e97a5a
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/train.py
@@ -0,0 +1,161 @@
+import argparse
+import json
+import os
+from pathlib import Path
+from pprint import pprint as print
+
+import pytorch_lightning as pl
+import torch
+import yaml
+from asteroid.engine.system import System
+from asteroid.utils import parse_args_as_dict, prepare_parser_from_dict
+from local import (
+    Compose,
+    ConvTasNetStereo,
+    RebalanceMusicDataset,
+    augment_channelswap,
+    augment_gain,
+)
+from pytorch_lightning.callbacks import EarlyStopping, ModelCheckpoint
+from torch.optim.lr_scheduler import ReduceLROnPlateau
+from torch.utils.data import DataLoader
+
+# Keys which are not in the conf.yml file can be added here.
+# In the hierarchical dictionary created when parsing, the key `key` can be
+# found at dic['main_args'][key]
+# By default train.py will use all available GPUs. The `id` option in run.sh
+# will limit the number of available GPUs for train.py .
+parser = argparse.ArgumentParser()
+parser.add_argument(
+    "--exp_dir", default="exp/tmp", help="Full path to save best validation model"
+)
+
+
+def main(conf):
+    source_augmentations = Compose([augment_gain, augment_channelswap])
+
+    dataset_kwargs = {
+        "root_path": Path(conf["data"]["root_path"]),
+        "sample_rate": conf["data"]["sample_rate"],
+        "target": conf["data"]["target"],
+    }
+
+    train_set = RebalanceMusicDataset(
+        split="train",
+        music_tracks_file=f"{conf['data']['music_tracks_file']}/music.train.json",
+        samples_per_track=conf["data"]["samples_per_track"],
+        segment_length=conf["data"]["segment_length"],
+        random_segments=True,
+        random_track_mix=True,
+        source_augmentations=source_augmentations,
+        **dataset_kwargs,
+    )
+
+    val_set = RebalanceMusicDataset(
+        music_tracks_file=f"{conf['data']['music_tracks_file']}/music.valid.json",
+        split="valid",
+        segment_length=None,
+        **dataset_kwargs,
+    )
+
+    train_loader = DataLoader(
+        train_set,
+        shuffle=True,
+        batch_size=conf["training"]["batch_size"],
+        num_workers=conf["training"]["num_workers"],
+        drop_last=True,
+        pin_memory=True,
+    )
+
+    val_loader = DataLoader(
+        val_set,
+        shuffle=False,
+        batch_size=conf["training"]["batch_size"],
+        num_workers=conf["training"]["num_workers"],
+        pin_memory=True,
+        drop_last=False,
+    )
+
+    model = ConvTasNetStereo(
+        **conf["convtasnet"], samplerate=conf["data"]["sample_rate"]
+    )
+
+    optimizer = torch.optim.Adam(model.parameters(), conf["optim"]["lr"])
+
+    # Define scheduler
+    scheduler = None
+    if conf["training"]["half_lr"]:
+        scheduler = ReduceLROnPlateau(optimizer=optimizer, factor=0.5, patience=5)
+    # Just after instantiating, save the args. Easy loading in the future.
+    exp_dir = conf["main_args"]["exp_dir"]
+    os.makedirs(exp_dir, exist_ok=True)
+    conf_path = os.path.join(exp_dir, "conf.yml")
+    with open(conf_path, "w") as outfile:
+        yaml.safe_dump(conf, outfile)
+
+    # Define Loss function.
+    loss_func = torch.nn.L1Loss()
+    # loss_func = PITLossWrapper(pairwise_neg_sisdr, pit_from="pw_mtx")
+    system = System(
+        model=model,
+        loss_func=loss_func,
+        optimizer=optimizer,
+        train_loader=train_loader,
+        val_loader=val_loader,
+        scheduler=scheduler,
+        config=conf,
+    )
+
+    # Define callbacks
+    callbacks = []
+    checkpoint_dir = os.path.join(exp_dir, "checkpoints/")
+    checkpoint = ModelCheckpoint(
+        checkpoint_dir, monitor="val_loss", mode="min", save_top_k=1, verbose=True
+    )
+    callbacks.append(checkpoint)
+    if conf["training"]["early_stop"]:
+        callbacks.append(
+            EarlyStopping(monitor="val_loss", mode="min", patience=20, verbose=True)
+        )
+
+    trainer = pl.Trainer(
+        max_epochs=conf["training"]["epochs"],
+        callbacks=callbacks,
+        default_root_dir=exp_dir,
+        accelerator="gpu" if torch.cuda.is_available() else "cpu",
+        strategy="auto",
+        devices="auto",
+        gradient_clip_val=5.0,
+        accumulate_grad_batches=conf["training"]["aggregate"],
+    )
+    trainer.fit(system)
+
+    best_k = {k: v.item() for k, v in checkpoint.best_k_models.items()}
+    with open(os.path.join(exp_dir, "best_k_models.json"), "w") as f:
+        json.dump(best_k, f, indent=0)
+
+    state_dict = torch.load(checkpoint.best_model_path)
+    system.load_state_dict(state_dict=state_dict["state_dict"])
+    system.cpu()
+
+    to_save = system.model.serialize()
+    to_save.update(train_set.get_infos())
+    torch.save(to_save, os.path.join(exp_dir, "best_model.pth"))
+
+
+if __name__ == "__main__":
+    # We start with opening the config file conf.yml as a dictionary from
+    # which we can create parsers. Each top level key in the dictionary defined
+    # by the YAML file creates a group in the parser.
+    with open("local/conf.yml") as f:
+        def_conf = yaml.safe_load(f)
+    parser = prepare_parser_from_dict(def_conf, parser=parser)
+    # Arguments are then parsed into a hierarchical dictionary (instead of
+    # flat, as returned by argparse) to facilitate calls to the different
+    # asteroid methods (see in main).
+    # plain_args is the direct output of parser.parse_args() and contains all
+    # the attributes in an non-hierarchical structure. It can be useful to also
+    # have it so we included it here but it is not used.
+    arg_dic, plain_args = parse_args_as_dict(parser, return_plain_args=True)
+    print(arg_dic)
+    main(arg_dic)
diff --git a/recipes/cad2/task2/ConvTasNet/utils/parse_options.sh b/recipes/cad2/task2/ConvTasNet/utils/parse_options.sh
new file mode 100755
index 000000000..c2c3b31f2
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/utils/parse_options.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+# Copyright 2012  Johns Hopkins University (Author: Daniel Povey);
+#                 Arnab Ghoshal, Karel Vesely
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#  http://www.apache.org/licenses/LICENSE-2.0
+#
+# THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
+# WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
+# MERCHANTABLITY OR NON-INFRINGEMENT.
+# See the Apache 2 License for the specific language governing permissions and
+# limitations under the License.
+
+
+# Parse command-line options.
+# To be sourced by another script (as in ". parse_options.sh").
+# Option format is: --option-name arg
+# and shell variable "option_name" gets set to value "arg."
+# The exception is --help, which takes no arguments, but prints the
+# $help_message variable (if defined).
+
+
+###
+### The --config file options have lower priority to command line
+### options, so we need to import them first...
+###
+
+# Now import all the configs specified by command-line, in left-to-right order
+for ((argpos=1; argpos<$#; argpos++)); do
+  if [ "${!argpos}" == "--config" ]; then
+    argpos_plus1=$((argpos+1))
+    config=${!argpos_plus1}
+    [ ! -r $config ] && echo "$0: missing config '$config'" && exit 1
+    . $config  # source the config file.
+  fi
+done
+
+
+###
+### Now we process the command line options
+###
+while true; do
+  [ -z "${1:-}" ] && break;  # break if there are no arguments
+  case "$1" in
+    # If the enclosing script is called with --help option, print the help
+    # message and exit.  Scripts should put help messages in $help_message
+    --help|-h) if [ -z "$help_message" ]; then echo "No help found." 1>&2;
+      else printf "$help_message\n" 1>&2 ; fi;
+      exit 0 ;;
+    --*=*) echo "$0: options to scripts must be of the form --name value, got '$1'"
+      exit 1 ;;
+    # If the first command-line argument begins with "--" (e.g. --foo-bar),
+    # then work out the variable name as $name, which will equal "foo_bar".
+    --*) name=`echo "$1" | sed s/^--// | sed s/-/_/g`;
+      # Next we test whether the variable in question is undefned-- if so it's
+      # an invalid option and we die.  Note: $0 evaluates to the name of the
+      # enclosing script.
+      # The test [ -z ${foo_bar+xxx} ] will return true if the variable foo_bar
+      # is undefined.  We then have to wrap this test inside "eval" because
+      # foo_bar is itself inside a variable ($name).
+      eval '[ -z "${'$name'+xxx}" ]' && echo "$0: invalid option $1" 1>&2 && exit 1;
+
+      oldval="`eval echo \\$$name`";
+      # Work out whether we seem to be expecting a Boolean argument.
+      if [ "$oldval" == "true" ] || [ "$oldval" == "false" ]; then
+        was_bool=true;
+      else
+        was_bool=false;
+      fi
+
+      # Set the variable to the right value-- the escaped quotes make it work if
+      # the option had spaces, like --cmd "queue.pl -sync y"
+      eval $name=\"$2\";
+
+      # Check that Boolean-valued arguments are really Boolean.
+      if $was_bool && [[ "$2" != "true" && "$2" != "false" ]]; then
+        echo "$0: expected \"true\" or \"false\": $1 $2" 1>&2
+        exit 1;
+      fi
+      shift 2;
+      ;;
+  *) break;
+  esac
+done
+
+
+# Check for an empty argument to the --cmd option, which can easily occur as a
+# result of scripting errors.
+[ ! -z "${cmd+xxx}" ] && [ -z "$cmd" ] && echo "$0: empty argument to --cmd option" 1>&2 && exit 1;
+
+
+true; # so this script returns exit code 0.
\ No newline at end of file
diff --git a/recipes/cad2/task2/ConvTasNet/utils/prepare_python_env.sh b/recipes/cad2/task2/ConvTasNet/utils/prepare_python_env.sh
new file mode 100755
index 000000000..3dc223334
--- /dev/null
+++ b/recipes/cad2/task2/ConvTasNet/utils/prepare_python_env.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+# Usage ./utils/install_env.sh --install_dir A --asteroid_root B --pip_requires C
+install_dir=~
+asteroid_root=../../../../
+pip_requires=../../../requirements.txt  # Expects a requirement.txt
+
+. utils/parse_options.sh || exit 1
+
+mkdir -p $install_dir
+cd $install_dir
+echo "Download and install latest version of miniconda3 into ${install_dir}"
+wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
+
+bash Miniconda3-latest-Linux-x86_64.sh -b -p miniconda3
+pip_path=$PWD/miniconda3/bin/pip
+
+rm Miniconda3-latest-Linux-x86_64.sh
+cd -
+
+if [[ ! -z ${pip_requires} ]]; then
+	$pip_path install -r $pip_requires
+fi
+$pip_path install soundfile
+$pip_path install -e $asteroid_root
+#$pip_path install ${asteroid_root}/\[""evaluate""\]
+echo -e "\nAsteroid has been installed in editable mode. Feel free to apply your changes !"
\ No newline at end of file
diff --git a/recipes/cad2/task2/README.md b/recipes/cad2/task2/README.md
new file mode 100644
index 000000000..42bfc4258
--- /dev/null
+++ b/recipes/cad2/task2/README.md
@@ -0,0 +1,137 @@
+# The Second Cadenza Challenge (CAD2) - Task 2: Rebalancing Classical Music
+
+Cadenza challenge code for the Second Cadenza Challenge (CAD2) Task2.
+
+For more information please visit the [challenge website](https://cadenzachallenge.org/docs/cadenza2/intro).
+
+## 1. Data structure
+
+The Second Cadenza Challenge - task 2 is using two datasets for training:
+
+- EnsembleSet: This contains 80 pieces of synthesised classical music.
+- CadenzaWoodwind: This contains 19 pieces of woodwind quartets.
+
+To download the data, please visit [here](https://forms.gle/taYK6MfBeW9sQk5PA).
+The data is contained in several packages:
+
+- CadenzaWoodwind.zip: containing the CadenzaWoodwind dataset.
+- EnsembleSet_Mix_1.zip: containing the EnsembleSet dataset for microphone `MIX_1`.
+- metadata.zip: containing the metadata for the systems.
+
+### 1.1 Generate data directory
+
+The baseline system expects the data in a specific directory structure.
+To generate this structure:
+
+1. Save the packages under the same directory.
+2. Run the scrip `process_dataset/process_zenodo_download.py`
+3. in config.yaml, set:
+   4. `path.zenodo_download_path` to the path where you saved the packages.
+   5. `path.root` to the path where you want to save the data.
+
+Note that the script will unzip the packages if they are not already unzipped.
+
+### 1.2 Necessary data
+
+The script `process_dataset/process_zenodo_download.py` will generate the following structure:
+
+- **audio** directory containing the audio files.
+
+```text
+cadenza_data
+└───cad2
+    └───task2
+        └───audio
+            ├───train
+            └───valid
+```
+
+- **Metadata** contains the metadata for the systems.
+
+```text
+cadenza_data
+└───cad2
+    └───task1
+        └───metadata
+            ├───compressor_params.train.json
+            ├───compressor_params.valid.json
+            ├───gains.json
+            ├───gains_meta.json
+            ├───listeners.train.json
+            ├───listeners.valid.json
+            ├───music.train.json
+            ├───music.valid.json
+            ├───music.valid.to_generate.json
+            ├───music_tracks.train.json
+            ├───music_tracks.valid.json
+            ├───scene.train.json
+            ├───scene.valid.json
+            ├───scene_listeners.train.json
+            └───scene_listeners.valid.json
+```
+
+## 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 enhancement system uses an audio source separation model to separate the different
+sources of the mixture. What instruments are contained in the mixture is given information.
+
+Next, using the requested gains, it changes the levels of the different sources and
+downmix to stereo. Then, uses a multiband dynamic range compressor to compress the audio signal.
+The final signals is saved in FLAC, 44100 Hz, 16-bit format.
+
+The audio source separation model correspond to 8 ConvTasNet models trained on EnsembleSet and CadenzaWoodwind.
+Each model is trained to separate a single target instrument from the music.
+
+To run the baseline enhancement system first, make sure that `paths.root` in `config.yaml` points to
+where you have installed the Cadenza data. You can also define your own `path.exp_folder`
+to store the enhanced signals and evaluated results.
+
+You can modify the code to add your own enhancement system or your own remixing strategy
+using the alpha parameter.
+
+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
+
+A csv file containing the eight HAAQI scores and the combined score 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.
+This is due to the  random noises generated within HAAQI, but the differences should be sufficiently small.
+For reproducibility, in the given recipe, the random seed for each signal is set as the last eight digits
+of the song md5.
+
+### 2.3 ConvTasNet Source Separation
+
+In directory `ConvTasNet/` we provide the code to train the ConvTasNet model
+on the MUSDB18-HQ dataset.
+
+This recipe is based on the stereo modification of the ConvTasNet done by Alexandre Defossez
+and available in Demucs' branch [`v1`](https://github.com/facebookresearch/demucs/blob/110f8fee0815d4c0d4ed3e2d478e37df247cd269/demucs/tasnet.py)
+
+The training is based in an Asteroid recipe.
diff --git a/recipes/cad2/task2/__init__.py b/recipes/cad2/task2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad2/task2/baseline/__init__.py b/recipes/cad2/task2/baseline/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad2/task2/baseline/config.yaml b/recipes/cad2/task2/baseline/config.yaml
new file mode 100644
index 000000000..950bc042e
--- /dev/null
+++ b/recipes/cad2/task2/baseline/config.yaml
@@ -0,0 +1,49 @@
+# Zenodo download path: path to the zenodo download folder.
+# root: root path of the dataset. This path will contain the audio and metadata folders
+path:
+  root: /media/gerardoroadabike/Extreme SSD1/Challenges/CAD2/cadenza_data/cad2/task2
+  metadata_dir: ${path.root}/metadata
+  music_dir: ${path.root}/audio
+  gains_file: ${path.metadata_dir}/gains.json
+  listeners_file: ${path.metadata_dir}/listeners.valid.json
+  enhancer_params_file: ${path.metadata_dir}/compressor_params.valid.json
+  music_file: ${path.metadata_dir}/music.valid.json
+  scenes_file: ${path.metadata_dir}/scenes.valid.json
+  scene_listeners_file: ${path.metadata_dir}/scene_listeners.valid.json
+  exp_folder: ./exp # folder to store enhanced signals and final results
+
+input_sample_rate: 44100
+remix_sample_rate: 32000
+HAAQI_sample_rate: 24000
+
+separator:
+  force_redownload: True
+  add_residual: 0.1
+  causality: noncausal
+  device: ~
+  separation:
+    number_sources: 2
+    segment: 6.0
+    overlap: 0.1
+    sample_rate: ${input_sample_rate}
+
+enhancer:
+  crossover_frequencies: [353.55, 707.11, 1414.21, 2828.43, 5656.85] # [250, 500, 1000, 2000, 4000] * sqrt(2)
+  attack: [11, 11, 14, 13, 11, 11]
+  release: [80, 80, 80, 80, 100, 100]
+  threshold: [-30, -30, -30, -30, -30, -30]
+
+soft_clip: False
+
+evaluate:
+  set_random_seed: True
+  small_test: False
+  batch_size: 1  # Number of batches
+  batch: 0       # Batch number to evaluate
+
+# hydra config
+hydra:
+  run:
+    dir: ${path.exp_folder}
+  job:
+    chdir: True
\ No newline at end of file
diff --git a/recipes/cad2/task2/baseline/enhance.py b/recipes/cad2/task2/baseline/enhance.py
new file mode 100644
index 000000000..ac99a9bd0
--- /dev/null
+++ b/recipes/cad2/task2/baseline/enhance.py
@@ -0,0 +1,407 @@
+""" Run the dummy enhancement. """
+
+from __future__ import annotations
+
+import json
+import logging
+from collections import Counter
+from pathlib import Path
+
+# pylint: disable=import-error
+import hydra
+import numpy as np
+import torch
+from numpy import ndarray
+from omegaconf import DictConfig
+from torchaudio.transforms import Fade
+
+from clarity.enhancer.multiband_compressor import MultibandCompressor
+from clarity.utils.audiogram import Listener
+from clarity.utils.flac_encoder import read_flac_signal, save_flac_signal
+from clarity.utils.source_separation_support import get_device
+from recipes.cad2.task2.baseline.evaluate import (
+    adjust_level,
+    apply_gains,
+    make_scene_listener_list,
+    remix_stems,
+)
+from recipes.cad2.task2.ConvTasNet.local.tasnet import ConvTasNetStereo
+
+logger = logging.getLogger(__name__)
+
+
+def check_repeated_source(gains: dict, source_list: dict) -> dict:
+    """Check if mixture has 2 voices of the same instrument.
+    Apply average gain to both voices.
+
+    Args:
+        gains (dict): Dictionary of original gains.
+        source_list (dict): Dictionary of sources in mixture.
+
+    Returns:
+        dict: Dictionary of modified gains.
+    """
+    count_dict = Counter(source_list.values())
+    two_voices = [key for key, value in source_list.items() if count_dict[value] > 1]
+    two_voices_gain = [gain for source, gain in gains.items() if source in two_voices]
+    two_voices_gain = np.mean(two_voices_gain)
+
+    new_gains = {}
+    for key, value in gains.items():
+        if key in two_voices:
+            new_gains[key] = two_voices_gain
+        else:
+            new_gains[key] = value
+    return new_gains
+
+
+def separate_sources(
+    model: torch.nn.Module,
+    mix: torch.Tensor | ndarray,
+    sample_rate: int,
+    segment: float = 10.0,
+    overlap: float = 0.1,
+    number_sources: int = 4,
+    device: torch.device | str | None = None,
+):
+    """
+    Apply model to a given mixture.
+    Use fade, and add segments together in order to add model segment by segment.
+
+    Args:
+        model (torch.nn.Module): model to use for separation
+        mix (torch.Tensor): mixture to separate, shape (batch, channels, time)
+        sample_rate (int): sampling rate of the mixture
+        segment (float): segment length in seconds
+        overlap (float): overlap between segments, between 0 and 1
+        number_sources (int): number of sources to separate
+        device (torch.device, str, or None): if provided, device on which to
+            execute the computation, otherwise `mix.device` is assumed.
+            When `device` is different from `mix.device`, only local computations will
+            be on `device`, while the entire tracks will be stored on `mix.device`.
+
+    Returns:
+        torch.Tensor: estimated sources
+
+    Based on https://pytorch.org/audio/main/tutorials/hybrid_demucs_tutorial.html
+    """
+    device = mix.device if device is None else torch.device(device)
+    mix = torch.as_tensor(mix, dtype=torch.float, device=device)
+
+    if mix.ndim == 1:
+        # one track and mono audio
+        mix = mix.unsqueeze(0).unsqueeze(0)
+    elif mix.ndim == 2:
+        # one track and stereo audio
+        mix = mix.unsqueeze(0)
+
+    batch, channels, length = mix.shape
+
+    chunk_len = int(sample_rate * segment * (1 + overlap))
+    start = 0
+    end = chunk_len
+    overlap_frames = overlap * sample_rate
+    fade = Fade(fade_in_len=0, fade_out_len=int(overlap_frames), fade_shape="linear")
+
+    final = torch.zeros(batch, number_sources, channels, length, device=device)
+
+    while start < length - overlap_frames:
+        chunk = mix[:, :, start:end]
+        with torch.no_grad():
+            out = model.forward(chunk)
+        out = fade(out)
+        final[:, :, :, start:end] += out
+        if start == 0:
+            fade.fade_in_len = int(overlap_frames)
+            start += int(chunk_len - overlap_frames)
+        else:
+            start += chunk_len
+        end += chunk_len
+        if end >= length:
+            fade.fade_out_len = 0
+
+    return final
+
+
+# pylint: disable=unused-argument
+def decompose_signal(
+    model: dict[str, ConvTasNetStereo],
+    signal: ndarray,
+    signal_sample_rate: int,
+    device: torch.device,
+    sources_list: dict,
+    listener: Listener,
+    add_residual: float = 0.0,
+) -> dict[str, ndarray]:
+    """
+    Decompose the signal into the estimated sources.
+
+    The listener is ignored by the baseline system as it
+     is not performing personalised decomposition.
+
+    Args:
+        model (dict): Dictionary of separation models.
+        model_sample_rate (int): Sample rate of the separation model.
+        signal (ndarray): Signal to decompose.
+        signal_sample_rate (int): Sample rate of the signal.
+        device (torch.device): Device to use for separation.
+        sources_list (dict): List of sources to separate.
+        listener (Listener): Listener audiogram.
+        add_residual (float): Add residual to the target estimated sources from
+            the accompaniment.
+
+    Returns:
+        dict: Dictionary of estimated sources.
+    """
+    out_sources = {}
+    for source in sources_list:
+        est_sources = separate_sources(
+            model=model[sources_list[source]],
+            mix=signal,
+            sample_rate=signal_sample_rate,
+            number_sources=2,
+            device=device,
+        )
+        target, accompaniment = est_sources.squeeze(0).cpu().detach().numpy()
+
+        if add_residual > 0.0:
+            target += accompaniment * add_residual
+
+        out_sources[source] = target.T
+    return out_sources
+
+
+def load_separation_model(
+    causality: str, device: torch.device, force_redownload: bool = True
+) -> dict[str, ConvTasNetStereo]:
+    """
+    Load the separation model.
+    Args:
+        causality (str): Causality of the model (causal or noncausal).
+        device (torch.device): Device to load the model.
+        force_redownload (bool): Whether to force redownload the model.
+
+    Returns:
+        model: Separation model.
+    """
+    models = {}
+    causal = {"causal": "Causal", "noncausal": "NonCausal"}
+
+    for instrument in [
+        "Bassoon",
+        "Cello",
+        "Clarinet",
+        "Flute",
+        "Oboe",
+        "Sax",
+        "Viola",
+        "Violin",
+    ]:
+        logger.info(
+            "Loading model "
+            f"cadenzachallenge/ConvTasNet_{instrument}_{causal[causality]}"
+        )
+        models[instrument] = ConvTasNetStereo.from_pretrained(
+            f"cadenzachallenge/ConvTasNet_{instrument}_{causal[causality]}",
+            force_download=force_redownload,
+        ).to(device)
+    return models
+
+
+def process_remix_for_listener(
+    signal: ndarray, enhancer: MultibandCompressor, enhancer_params: dict, listener
+) -> ndarray:
+    """Process the stems from sources.
+
+    Args:
+
+    Returns:
+        ndarray: Processed signal.
+    """
+
+    output = []
+    for side, ear in enumerate(["left", "right"]):
+        enhancer.set_compressors(**enhancer_params[ear])
+        output.append(enhancer(signal[:, side]))
+
+    return np.vstack(output).T
+
+
+@hydra.main(config_path="", config_name="config", version_base=None)
+def enhance(config: DictConfig) -> None:
+    """
+    Run the music enhancement.
+    The system decomposes the music into the estimated sources.
+    Next, applies the target gain per source.
+    Then, remixes the sources to create the enhanced signal.
+    Finally, the enhanced signal is amplified for the listener.
+
+    Args:
+        config (dict): Dictionary of configuration options for enhancing music.
+
+    """
+    if config.separator.causality not in ["causal", "noncausal"]:
+        raise ValueError(
+            f"Causality must be causal or noncausal, {config.separator.causality} was"
+            " provided."
+        )
+
+    # Set the output directory where processed signals will be saved
+    enhanced_folder = Path("enhanced_signals")
+    enhanced_folder.mkdir(parents=True, exist_ok=True)
+
+    device, _ = get_device(config.separator.device)
+
+    # Load listener audiograms and songs
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
+
+    # Load gains
+    with Path(config.path.gains_file).open("r", encoding="utf-8") as file:
+        gains = json.load(file)
+
+    # Load Scenes
+    with Path(config.path.scenes_file).open("r", encoding="utf-8") as file:
+        scenes = json.load(file)
+
+    # Load scene listeners
+    with Path(config.path.scene_listeners_file).open("r", encoding="utf-8") as file:
+        scenes_listeners = json.load(file)
+
+    # load songs
+    with Path(config.path.music_file).open("r", encoding="utf-8") as file:
+        songs = json.load(file)
+
+    # Load compressor params
+    with Path(config.path.enhancer_params_file).open("r", encoding="utf-8") as file:
+        enhancer_params = json.load(file)
+
+    # Load separation model
+    separation_models = load_separation_model(
+        config.separator.causality, device, config.separator.force_redownload
+    )
+
+    # create hearing aid
+    enhancer = MultibandCompressor(
+        crossover_frequencies=config.enhancer.crossover_frequencies,
+        sample_rate=config.input_sample_rate,
+    )
+
+    # Select a batch to process
+    scene_listener_pairs = make_scene_listener_list(
+        scenes_listeners, config.evaluate.small_test
+    )
+
+    scene_listener_pairs = scene_listener_pairs[
+        config.evaluate.batch :: config.evaluate.batch_size
+    ]
+
+    # Decompose each song into left and right vocal, drums, bass, and other stems
+    # and process each stem for the listener
+    num_scenes = len(scene_listener_pairs)
+    for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
+        scene_id, listener_id = scene_listener_pair
+
+        scene = scenes[scene_id]
+        song_name = scene["music"]
+
+        logger.info(
+            f"[{idx:03d}/{num_scenes:03d}] "
+            f"Processing {scene_id}: song {song_name} for listener {listener_id}"
+        )
+        # Get the listener's audiogram
+        listener = listener_dict[listener_id]
+
+        # Get the listener's compressor params
+        mbc_params_listener: dict[str, dict] = {"left": {}, "right": {}}
+
+        for ear in ["left", "right"]:
+            mbc_params_listener[ear]["release"] = config.enhancer.release
+            mbc_params_listener[ear]["attack"] = config.enhancer.attack
+            mbc_params_listener[ear]["threshold"] = config.enhancer.threshold
+        mbc_params_listener["left"]["ratio"] = enhancer_params[listener_id]["cr_l"]
+        mbc_params_listener["right"]["ratio"] = enhancer_params[listener_id]["cr_r"]
+        mbc_params_listener["left"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_l"
+        ]
+        mbc_params_listener["right"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_r"
+        ]
+
+        # Read the mixture signal
+        # Convert to 32-bit floating point and transpose
+        # from [samples, channels] to [channels, samples]
+        source_list = {
+            f"source_{idx}": s["instrument"].split("_")[0]
+            for idx, s in enumerate(songs[song_name].values(), 1)
+            if "Mixture" not in s["instrument"]
+        }
+
+        mixture_signal, mix_sample_rate = read_flac_signal(
+            filename=Path(config.path.music_dir) / songs[song_name]["mixture"]["track"]
+        )
+        assert mix_sample_rate == config.input_sample_rate
+
+        start = songs[song_name]["mixture"]["start"]
+        end = start + songs[song_name]["mixture"]["duration"]
+        mixture_signal = mixture_signal[
+            int(start * mix_sample_rate) : int(end * mix_sample_rate),
+            :,
+        ]
+
+        # Estimate the isolated sources
+        stems: dict[str, ndarray] = decompose_signal(
+            model=separation_models,
+            signal=mixture_signal.T,  # Channel first
+            signal_sample_rate=config.input_sample_rate,
+            device=device,
+            sources_list=source_list,
+            listener=listener,
+            add_residual=config.separator.add_residual,
+        )
+
+        # Apply gains to sources
+        gain_scene = check_repeated_source(gains[scene["gain"]], source_list)
+        stems = apply_gains(stems, config.input_sample_rate, gain_scene)
+
+        # Downmix to stereo
+        enhanced_signal = remix_stems(stems)
+
+        # adjust levels to get roughly -40 dB before compressor
+        enhanced_signal = adjust_level(enhanced_signal, gains[scene["gain"]])
+
+        # Apply compressor
+        enhanced_signal = process_remix_for_listener(
+            signal=enhanced_signal,
+            enhancer=enhancer,
+            enhancer_params=mbc_params_listener,
+            listener=listener,
+        )
+
+        # Save the enhanced signal in the corresponding directory
+        if 0 < int(scene_id[1:]) < 49999:
+            out_dir = "train"
+        elif 50000 < int(scene_id[1:]) < 59999:
+            out_dir = "valid"
+        else:
+            out_dir = "test"
+
+        filename = (
+            Path(enhanced_folder) / out_dir / f"{scene_id}_{listener.id}_remix.flac"
+        )
+
+        filename.parent.mkdir(parents=True, exist_ok=True)
+        save_flac_signal(
+            signal=enhanced_signal,
+            filename=filename,
+            signal_sample_rate=config.input_sample_rate,
+            output_sample_rate=config.remix_sample_rate,
+            do_clip_signal=True,
+            do_soft_clip=config.soft_clip,
+        )
+
+    logger.info("Done!")
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    enhance()
diff --git a/recipes/cad2/task2/baseline/evaluate.py b/recipes/cad2/task2/baseline/evaluate.py
new file mode 100644
index 000000000..bbae10bfc
--- /dev/null
+++ b/recipes/cad2/task2/baseline/evaluate.py
@@ -0,0 +1,310 @@
+"""Evaluate the enhanced signals using the HAAQI metric."""
+
+from __future__ import annotations
+
+# pylint: disable=import-error
+import hashlib
+import json
+import logging
+import warnings
+from pathlib import Path
+from typing import Any
+
+import hydra
+import numpy as np
+import pyloudnorm as pyln
+from numpy import ndarray
+from omegaconf import DictConfig
+
+from clarity.enhancer.multiband_compressor import MultibandCompressor
+from clarity.evaluator.haaqi import compute_haaqi
+from clarity.utils.audiogram import Listener
+from clarity.utils.flac_encoder import read_flac_signal
+from clarity.utils.results_support import ResultsFile
+from clarity.utils.signal_processing import compute_rms, resample
+
+logger = logging.getLogger(__name__)
+
+
+def apply_gains(stems: dict, sample_rate: float, gains: dict) -> dict:
+    """Apply gain to the signal by using LUFS.
+
+    Args:
+        stems (dict): Dictionary of stems.
+        sample_rate (float): Sample rate of the signal.
+        gains (dict): Dictionary of gains.
+
+    Returns:
+        dict: Dictionary of stems with applied gains.
+    """
+    meter = pyln.Meter(int(sample_rate))
+
+    stems_gain = {}
+    for stem_str, stem_signal in stems.items():
+        if stem_signal.shape[0] < stem_signal.shape[1]:
+            stem_signal = stem_signal.T
+
+        stem_lufs = meter.integrated_loudness(stem_signal)
+        if stem_lufs == -np.inf:
+            stem_lufs = -80
+
+        gain = stem_lufs + gains[stem_str]
+        with warnings.catch_warnings():
+            warnings.filterwarnings(
+                "ignore", message="Possible clipped samples in output"
+            )
+            stems_gain[stem_str] = pyln.normalize.loudness(stem_signal, stem_lufs, gain)
+    return stems_gain
+
+
+def remix_stems(stems: dict) -> ndarray:
+    """Remix the stems into a stereo signal.
+
+    The remixing is done by summing the stems.
+
+    Args:
+        stems (dict): Dictionary of stems.
+
+    Returns:
+        ndarray: Stereo signal.
+    """
+    remix_signal = np.zeros(stems["source_1"].shape)
+    for _, stem_signal in stems.items():
+        remix_signal += stem_signal
+    return remix_signal
+
+
+def make_scene_listener_list(scenes_listeners: dict, small_test: bool = False) -> list:
+    """Make the list of scene-listener pairing to process
+
+    Args:
+        scenes_listeners (dict): Dictionary of scenes and listeners.
+        small_test (bool): Whether to use a small test set.
+
+    Returns:
+        list: List of scene-listener pairings.
+
+    """
+    scene_listener_pairs = [
+        (scene, listener)
+        for scene in scenes_listeners
+        for listener in scenes_listeners[scene]
+    ]
+
+    # Can define a standard 'small_test' with just 1/50 of the data
+    if small_test:
+        scene_listener_pairs = scene_listener_pairs[::400]
+
+    return scene_listener_pairs
+
+
+def set_song_seed(song: str) -> None:
+    """Set a seed that is unique for the given song"""
+    song_encoded = hashlib.md5(song.encode("utf-8")).hexdigest()
+    song_md5 = int(song_encoded, 16) % (10**8)
+    np.random.seed(song_md5)
+
+
+def load_reference_stems(music_dir: str | Path, stems: dict) -> dict[Any, ndarray]:
+    """Load the reference stems for a given scene.
+
+    Args:
+        music_dir (str | Path): Path to the music directory.
+        stems (dict): Dictionary of stems
+    Returns:
+        dict: Dictionary of reference stems.
+    """
+    reference_stems = {}
+    for source_id, source_data in stems.items():
+        if source_id == "mixture":
+            continue
+
+        stem, _ = read_flac_signal(Path(music_dir) / source_data["track"])
+        reference_stems[source_id] = stem
+
+    return reference_stems
+
+
+def adjust_level(signal: np.ndarray, gains_scene: dict) -> np.ndarray:
+    """
+    Adjust the level of the signal to compensate the effect of amplifying the
+    sources
+    """
+    dbi = np.array(list(gains_scene.values()))
+    dbn = -10 * np.log10(np.sum(10 ** (dbi / 10)) / dbi.shape[0])
+    return signal * 10 ** (dbn / 20)
+
+
+@hydra.main(config_path="", config_name="config", version_base=None)
+def run_calculate_aq(config: DictConfig) -> None:
+    """Evaluate the enhanced signals using the HAAQI metric."""
+
+    enhanced_folder = Path("enhanced_signals")
+    logger.info(f"Evaluating from {enhanced_folder} directory")
+
+    # Load listener audiograms and songs
+    listener_dict = Listener.load_listener_dict(config.path.listeners_file)
+
+    with Path(config.path.gains_file).open("r", encoding="utf-8") as file:
+        gains = json.load(file)
+
+    with Path(config.path.scenes_file).open("r", encoding="utf-8") as file:
+        scenes = json.load(file)
+
+    with Path(config.path.scene_listeners_file).open("r", encoding="utf-8") as file:
+        scenes_listeners = json.load(file)
+
+    with Path(config.path.music_file).open("r", encoding="utf-8") as file:
+        songs = json.load(file)
+
+        # Load compressor params
+    with Path(config.path.enhancer_params_file).open("r", encoding="utf-8") as file:
+        enhancer_params = json.load(file)
+
+    enhancer = MultibandCompressor(
+        crossover_frequencies=config.enhancer.crossover_frequencies,
+        sample_rate=config.input_sample_rate,
+    )
+
+    scores_headers = [
+        "scene",
+        "song",
+        "listener",
+        "left_haaqi",
+        "right_haaqi",
+        "avg_haaqi",
+    ]
+    if config.evaluate.batch_size == 1:
+        results_file = ResultsFile(
+            "scores.csv",
+            header_columns=scores_headers,
+        )
+    else:
+        results_file = ResultsFile(
+            f"scores_{config.evaluate.batch + 1}-{config.evaluate.batch_size}.csv",
+            header_columns=scores_headers,
+        )
+
+    scene_listener_pairs = make_scene_listener_list(
+        scenes_listeners, config.evaluate.small_test
+    )
+    scene_listener_pairs = scene_listener_pairs[
+        config.evaluate.batch :: config.evaluate.batch_size
+    ]
+    num_scenes = len(scene_listener_pairs)
+    for idx, scene_listener_pair in enumerate(scene_listener_pairs, 1):
+        scene_id, listener_id = scene_listener_pair
+
+        scene = scenes[scene_id]
+        song_name = scene["music"]
+
+        logger.info(
+            f"[{idx:03d}/{num_scenes:03d}] "
+            f"Evaluating {scene_id} for listener {listener_id}"
+        )
+
+        # Load reference signals
+        reference_stems = load_reference_stems(
+            Path(config.path.music_dir), songs[song_name]
+        )
+        reference_stems = apply_gains(
+            reference_stems, config.input_sample_rate, gains[scene["gain"]]
+        )
+        reference_mixture = remix_stems(reference_stems)
+        reference_mixture = adjust_level(reference_mixture, gains[scene["gain"]])
+
+        # Set the random seed for the scene
+        if config.evaluate.set_random_seed:
+            set_song_seed(scene_id)
+
+        # Evaluate listener
+        listener = listener_dict[listener_id]
+
+        # Compressor params
+        mbc_params_listener: dict[str, dict] = {"left": {}, "right": {}}
+
+        for ear in ["left", "right"]:
+            mbc_params_listener[ear]["release"] = config.enhancer.release
+            mbc_params_listener[ear]["attack"] = config.enhancer.attack
+            mbc_params_listener[ear]["threshold"] = config.enhancer.threshold
+        mbc_params_listener["left"]["ratio"] = enhancer_params[listener_id]["cr_l"]
+        mbc_params_listener["right"]["ratio"] = enhancer_params[listener_id]["cr_r"]
+        mbc_params_listener["left"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_l"
+        ]
+        mbc_params_listener["right"]["makeup_gain"] = enhancer_params[listener_id][
+            "gain_r"
+        ]
+
+        # Get set directory
+        if 0 < int(scene_id[1:]) < 49999:
+            dataset_dir = "train"
+        elif 50000 < int(scene_id[1:]) < 59999:
+            dataset_dir = "valid"
+        else:
+            dataset_dir = "test"
+
+        # Load enhanced signal
+        enhanced_signal, _ = read_flac_signal(
+            Path(enhanced_folder) / dataset_dir / f"{scene_id}_{listener.id}_remix.flac"
+        )
+
+        # Apply hearing aid to reference signals
+        enhancer.set_compressors(**mbc_params_listener["left"])
+        left_reference = enhancer(
+            signal=reference_mixture[:, 0],
+        )
+        enhancer.set_compressors(**mbc_params_listener["right"])
+        right_reference = enhancer(signal=reference_mixture[:, 1])
+
+        # Compute the scores
+        left_score = compute_haaqi(
+            processed_signal=resample(
+                enhanced_signal[:, 0],
+                config.remix_sample_rate,
+                config.HAAQI_sample_rate,
+            ),
+            reference_signal=resample(
+                left_reference[0], config.input_sample_rate, config.HAAQI_sample_rate
+            ),
+            processed_sample_rate=config.HAAQI_sample_rate,
+            reference_sample_rate=config.HAAQI_sample_rate,
+            audiogram=listener.audiogram_left,
+            equalisation=2,
+            level1=65 - 20 * np.log10(compute_rms(reference_mixture[:, 0])),
+        )
+
+        right_score = compute_haaqi(
+            processed_signal=resample(
+                enhanced_signal[:, 1],
+                config.remix_sample_rate,
+                config.HAAQI_sample_rate,
+            ),
+            reference_signal=resample(
+                right_reference[0], config.input_sample_rate, config.HAAQI_sample_rate
+            ),
+            processed_sample_rate=config.HAAQI_sample_rate,
+            reference_sample_rate=config.HAAQI_sample_rate,
+            audiogram=listener.audiogram_right,
+            equalisation=2,
+            level1=65 - 20 * np.log10(compute_rms(reference_mixture[:, 1])),
+        )
+
+        # Save scores
+        results_file.add_result(
+            {
+                "scene": scene_id,
+                "song": song_name,
+                "listener": listener.id,
+                "left_haaqi": left_score,
+                "right_haaqi": right_score,
+                "avg_haaqi": float(np.mean([left_score, right_score])),
+            }
+        )
+
+    logger.info("Done!")
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    run_calculate_aq()
diff --git a/recipes/cad2/task2/baseline/merge_batches_results.py b/recipes/cad2/task2/baseline/merge_batches_results.py
new file mode 100644
index 000000000..ff29762da
--- /dev/null
+++ b/recipes/cad2/task2/baseline/merge_batches_results.py
@@ -0,0 +1,31 @@
+"""Join batches scores into a single file."""
+
+# pylint: disable=import-error
+import hydra
+import pandas as pd
+from omegaconf import DictConfig
+
+
+@hydra.main(config_path="", config_name="config")
+def join_batches(config: DictConfig) -> None:
+    """
+    Join batches scores into a single file.
+
+    Args:
+        config (DictConfig): Dictionary of configuration options.
+            The `.evaluate.batch_size` is extracted to determine how many
+            batches there are to combine.
+
+    """
+    batches_results = []
+    for batch in range(config.evaluate.batch_size):
+        batches_results.append(
+            pd.read_csv(f"scores_{batch + 1}-{config.evaluate.batch_size}.csv")
+        )
+    df_res = pd.concat(batches_results, ignore_index=True)
+    df_res.to_csv("scores.csv", index=False)
+
+
+# pylint: disable=no-value-for-parameter
+if __name__ == "__main__":
+    join_batches()
diff --git a/recipes/cad2/task2/process_dataset/__init__.py b/recipes/cad2/task2/process_dataset/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad2/task2/process_dataset/config.yaml b/recipes/cad2/task2/process_dataset/config.yaml
new file mode 100644
index 000000000..608ee2cfe
--- /dev/null
+++ b/recipes/cad2/task2/process_dataset/config.yaml
@@ -0,0 +1,19 @@
+# Zenodo download path: path to the zenodo download folder.
+# root: root path of the dataset. This path will contain the audio and metadata folders
+path:
+  zenodo_download_path: ??
+  root: ??
+  metadata_dir: ${path.root}/metadata
+  scene_metadata_file: ${path.metadata_dir}/scenes.train2.json
+  gain_metadata: ${path.metadata_dir}/gains_meta.json
+  music_dir: ${path.root}/audio
+  music_metadata: ${path.metadata_dir}/music.train.json
+
+seed_scene_generation: Cad2Task2_2024
+gains_per_track: 4
+listener_per_scene: 2
+
+# hydra config
+hydra:
+  job:
+    chdir: True
\ No newline at end of file
diff --git a/recipes/cad2/task2/process_dataset/generate_train_scenes.py b/recipes/cad2/task2/process_dataset/generate_train_scenes.py
new file mode 100644
index 000000000..b2758b300
--- /dev/null
+++ b/recipes/cad2/task2/process_dataset/generate_train_scenes.py
@@ -0,0 +1,99 @@
+"""
+Module to generate new train scenes"""
+
+from __future__ import annotations
+
+import hashlib
+
+# pylint: disable=import-error
+import json
+from pathlib import Path
+
+import hydra
+import numpy as np
+from omegaconf import DictConfig
+
+
+def set_song_seed(song: str) -> None:
+    """Set a seed that is unique for the given song"""
+    song_encoded = hashlib.md5(song.encode("utf-8")).hexdigest()
+    song_md5 = int(song_encoded, 16) % (10**8)
+    np.random.seed(song_md5)
+
+
+def choose_samples(source: list, number: int) -> list:
+    """Choose a number of samples from a source list"""
+    if number == 0:
+        return source
+    return np.random.choice(source, number, replace=False)
+
+
+def generate_scenes(cfg: DictConfig) -> dict:
+    """Generate scenes for the training set"""
+    scene_metadata = {}
+
+    scene_id = 10000
+    music_metadata = Path(cfg.path.music_metadata)
+
+    gains_metadata = Path(cfg.path.gain_metadata)
+    with open(gains_metadata) as f:
+        gains = json.load(f)
+
+    with open(music_metadata) as f:
+        music_meta = json.load(f)
+
+    for track, track_info in music_meta.items():
+        gains_per_track = int(cfg.gains_per_track)
+        instr_track = len(track_info) - 1
+        instr_track = min(5, instr_track)
+
+        for _ in range(gains_per_track):
+            scene_id += 1
+            scene_metadata[f"S{scene_id}"] = {
+                "music": track,
+                "gain": np.random.choice(list(gains[str(instr_track)].keys())),
+            }
+    return scene_metadata
+
+
+def generate_scene_listener(cfg: DictConfig) -> None:
+    """Generate listeners for the scenes"""
+    scene_metadata_path = Path(cfg.path.scene_metadata_file)
+
+    with open(scene_metadata_path) as f:
+        scenes = json.load(f)
+
+    with open(f"{cfg.path.metadata_dir}/listeners.train.json") as f:
+        listeners = list(json.load(f).keys())
+
+    scene_listeners = {}
+
+    set_song_seed("Cadenza2-scene")
+    for scene in scenes:
+        scene_listeners[scene] = list(
+            np.random.choice(listeners, cfg.listener_per_scene)
+        )
+
+    output_file = Path(cfg.path.metadata_dir) / "scene_listeners.train.json"
+    output_file.parent.mkdir(parents=True, exist_ok=True)
+    with open(output_file, "w") as f:
+        json.dump(scene_listeners, f, indent=4)
+
+
+@hydra.main(config_path="", config_name="config", version_base=None)
+def run(cfg: DictConfig) -> None:
+    set_song_seed(cfg.seed_scene_generation)
+
+    scene_metadata = generate_scenes(cfg)
+    scene_metadata_path = Path(cfg.path.scene_metadata_file)
+
+    with open(scene_metadata_path, "w") as f:
+        json.dump(scene_metadata, f, indent=4)
+
+    generate_scene_listener(cfg)
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    run()
+    print("Done!")
diff --git a/recipes/cad2/task2/process_dataset/process_zenodo_download.py b/recipes/cad2/task2/process_dataset/process_zenodo_download.py
new file mode 100644
index 000000000..0ac14fdb2
--- /dev/null
+++ b/recipes/cad2/task2/process_dataset/process_zenodo_download.py
@@ -0,0 +1,169 @@
+"""
+Module take the data downloaded from Zenodo and rearrange the data for the challenge.
+
+The module expects the data to be downloaded from Zenodo and stored in the
+following structure:
+
+- zenodo_download_path
+    - EnsembleSet.zip
+    - CadenzaWoodwind.zip
+    - metadata.zip
+    - URMP.zip
+    - BACH10.zip
+    - CadenzaWoodwind.json
+    - EnsembleSet.json
+"""
+
+from __future__ import annotations
+
+import json
+import shutil
+import zipfile
+from pathlib import Path
+
+import hydra
+import librosa
+import numpy as np
+from omegaconf import DictConfig
+from tqdm import tqdm
+
+from clarity.utils.flac_encoder import save_flac_signal
+
+
+def create_audio_train(cfg):
+    """Create the audio training dataset."""
+    audio_path = Path(cfg.path.music_dir)
+    audio_path.mkdir(parents=True, exist_ok=True)
+
+    zenodo_download_path = Path(cfg.path.zenodo_download_path)
+    train_metadata_path = zenodo_download_path / "metadata" / "music_tracks.train.json"
+
+    with open(train_metadata_path) as f:
+        train_metadata = json.load(f)
+
+    for _track, track_info in tqdm(
+        train_metadata.items(), desc="Copying training audio"
+    ):
+        dataset = track_info["dataset"]
+        for source in track_info["stems_path"]:
+            target = audio_path / "train" / source
+            target.parent.mkdir(parents=True, exist_ok=True)
+            if not target.exists():
+                shutil.copy(
+                    zenodo_download_path / dataset / source,
+                    audio_path / "train" / source,
+                )
+        if not (audio_path / "train" / track_info["mixture_path"]).exists():
+            shutil.copy(
+                zenodo_download_path / dataset / track_info["mixture_path"],
+                audio_path / "train" / track_info["mixture_path"],
+            )
+
+
+def create_audio_valid(cfg):
+    """Create the audio validation dataset"""
+    audio_path = Path(cfg.path.music_dir)
+    audio_path.mkdir(parents=True, exist_ok=True)
+
+    zenodo_download_path = Path(cfg.path.zenodo_download_path)
+    valid_samples_path = (
+        zenodo_download_path / "metadata" / "music.valid.to_generate.json"
+    )
+
+    with open(valid_samples_path) as f:
+        valid_samples = json.load(f)
+
+    for track, track_info in tqdm(
+        valid_samples.items(), desc="Creating validation audio"
+    ):
+        mixture = []
+        mixture_target = ""
+        for source, source_info in track_info.items():
+            if source == "mixture_target":
+                mixture_target = audio_path / source_info["track"]
+                continue
+
+            track_path = (
+                zenodo_download_path
+                / source_info["source_dataset"]
+                / source_info["track"]
+            )
+            target = (
+                audio_path
+                / "valid"
+                / track
+                / "/".join(source_info["track"].replace("/Mix_1", "").split("/")[1:])
+            )
+            target.parent.mkdir(parents=True, exist_ok=True)
+
+            start = source_info["start"]
+            duration = source_info["duration"]
+            signal, sr = librosa.load(
+                track_path, sr=None, offset=start, duration=duration, mono=False
+            )
+            mixture.append(signal)
+            if not target.exists():
+                save_flac_signal(
+                    signal.T,
+                    target,
+                    sr,
+                    sr,
+                )
+
+        mixture = np.stack(mixture)
+        mixture = mixture.sum(axis=0)
+        if not mixture_target.exists():
+            save_flac_signal(
+                mixture.T,
+                mixture_target,
+                sr,
+                sr,
+            )
+
+
+@hydra.main(config_path="", config_name="config", version_base=None)
+def prepare_cad2_dataset(config: DictConfig) -> None:
+    """
+    Prepare the dataset for the challenge.
+    """
+    # Zenodo data path
+    zenodo_path = Path(config.path.zenodo_download_path)
+
+    for directory in [
+        "CadenzaWoodwind",
+        "EnsembleSet_Mix_1",
+        "metadata",
+    ]:
+        if not (zenodo_path / directory.replace("_Mix_1", "")).exists():
+            if (zenodo_path / f"{directory}.zip").exists():
+                # Unzip the file
+                with zipfile.ZipFile(zenodo_path / f"{directory}.zip", "r") as zip_ref:
+                    for member in tqdm(
+                        zip_ref.infolist(), desc=f"Extracting {directory}"
+                    ):
+                        zip_ref.extract(member, zenodo_path)
+            else:
+                print(f"Skipping {directory} file. {directory}.zip not found")
+        else:
+            print(f"Directory {directory} already exists")
+
+    # Create the root path if it doesn't exist
+    root_path = Path(config.path.root)
+    root_path.mkdir(parents=True, exist_ok=True)
+
+    # Create train audio directory
+    create_audio_train(config)
+
+    # Create valid audio directory
+    create_audio_valid(config)
+
+    # Copy the metadata directory
+    print(f"Copying {directory} directory.")
+    shutil.copytree(
+        zenodo_path / "metadata", Path(config.path.metadata_dir), dirs_exist_ok=True
+    )
+
+
+# pylint: disable = no-value-for-parameter
+if __name__ == "__main__":
+    prepare_cad2_dataset()


Test patch:
diff --git a/tests/enhancer/multiband_compressor/test_compressor_qmul.py b/tests/enhancer/multiband_compressor/test_compressor_qmul.py
index 38dfd6a83..e6b0ebf2b 100644
--- a/tests/enhancer/multiband_compressor/test_compressor_qmul.py
+++ b/tests/enhancer/multiband_compressor/test_compressor_qmul.py
@@ -52,36 +52,6 @@ def test_initialization_custom(custom_compressor):
     assert custom_compressor.knee_width == 10.0
 
 
-def test_warning_threshold():
-    """Test the warning for a threshold outside the recommended range."""
-    with pytest.warns(UserWarning, match="Threshold outside the recommended range"):
-        Compressor(threshold=10.0)
-
-
-def test_warning_ratio():
-    """Test the warning for a ratio outside the recommended range."""
-    with pytest.warns(UserWarning, match="Ratio outside the recommended range"):
-        Compressor(ratio=0.5)
-
-
-def test_warning_attack():
-    """Test the warning for an attack outside the recommended range."""
-    with pytest.warns(UserWarning, match="Attack outside the recommended range"):
-        Compressor(attack=0.05)
-
-
-def test_warning_release():
-    """Test the warning for a release outside the recommended range."""
-    with pytest.warns(UserWarning, match="Release outside the recommended range"):
-        Compressor(release=1500.0)
-
-
-def test_warning_gain():
-    """Test the warning for a gain outside the recommended range."""
-    with pytest.warns(UserWarning, match="Make-up gain outside the recommended range"):
-        Compressor(makeup_gain=25.0)
-
-
 def test_call_default(default_compressor, random_signal):
     """Test the call method of the Compressor class with default parameters."""
     processed_signal = default_compressor(random_signal)