Repository: claritychallenge/clarity (language: python)

Issue:
Causal and non-causal vocal-background separation using ConvTasNet
This is the recipe to run the lyrics/accompaniment separtion models


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index aeef41c20..d69c44bcd 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/recipes/cad2/__init__.py b/recipes/cad2/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/recipes/cad2/task1/ConvTasNet/README.md b/recipes/cad2/task1/ConvTasNet/README.md
new file mode 100644
index 000000000..fbc42a8a5
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/README.md
@@ -0,0 +1,25 @@
+
+## Non-Causal
+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
+
+## Causal
+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..799257c72
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/eval.py
@@ -0,0 +1,120 @@
+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 ConvTasNet
+
+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 = ConvTasNet(
+        **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")
+
+    series_list = []
+    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..51349bc47
--- /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 ConvTasNet, overlap_and_add
+
+__all__ = [
+    "MUSDB18Dataset",
+    "Compose",
+    "augment_gain",
+    "augment_channelswap",
+    "ConvTasNet",
+    "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..3c36019c0
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/musdb18_dataset.py
@@ -0,0 +1,286 @@
+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=["vocals", "bass", "drums", "other"],
+        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,
+    ):
+        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..ef20293a3
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/local/tasnet.py
@@ -0,0 +1,532 @@
+# 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
+
+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 ConvTasNet(nn.Module):
+    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 = ConvTasNet(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..2d8c54fd9
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/requirements.txt
@@ -0,0 +1,2 @@
+musdb
+museval
diff --git a/recipes/cad2/task1/ConvTasNet/train.py b/recipes/cad2/task1/ConvTasNet/train.py
new file mode 100644
index 000000000..31bcc9dbf
--- /dev/null
+++ b/recipes/cad2/task1/ConvTasNet/train.py
@@ -0,0 +1,212 @@
+"""
+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 .local import (
+    Compose,
+    ConvTasNet,
+    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):
+    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):
+    model = ConvTasNet(**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, callbacks):
+    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():
+    return torch.nn.L1Loss()
+
+
+def create_callbacks(conf, exp_dir):
+    # 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/__init__.py b/recipes/cad2/task1/__init__.py
new file mode 100644
index 000000000..e69de29bb


Test patch:
diff --git a/tests/recipes/cad2/task1/ConvTasNet/test_task1_convtasnet.py b/tests/recipes/cad2/task1/ConvTasNet/test_task1_convtasnet.py
new file mode 100644
index 000000000..04876d3a8
--- /dev/null
+++ b/tests/recipes/cad2/task1/ConvTasNet/test_task1_convtasnet.py
@@ -0,0 +1,166 @@
+"""Module to test the ConvTasNet recipe."""
+
+import json
+import os
+import tempfile
+from unittest.mock import MagicMock
+
+import numpy as np
+import pytest
+import pytorch_lightning as pl
+import torch
+from pytorch_lightning.callbacks import ModelCheckpoint
+from scipy.io import wavfile
+
+from recipes.cad2.task1.ConvTasNet.local.tasnet import ConvTasNet
+from recipes.cad2.task1.ConvTasNet.train import (
+    create_callbacks,
+    create_datasets_and_loaders,
+    create_model_and_optimizer,
+    create_trainer,
+    get_loss_func,
+    save_best_model,
+)
+
+
+@pytest.fixture
+def mock_conf():
+    return {
+        "data": {
+            "root": "data/root",
+            "mix_background": True,
+            "sample_rate": 16000,
+            "segment": 0.5,
+            "samples_per_track": 1,
+        },
+        "convtasnet": {
+            "N": 256,
+            "L": 20,
+            "B": 256,
+            "H": 512,
+            "P": 3,
+            "X": 10,
+            "R": 4,
+            "C": 2,
+            "audio_channels": 2,
+            "mask_nonlinear": "relu",
+            "norm_type": "gLN",
+            "causal": False,
+        },
+        "training": {
+            "batch_size": 4,
+            "num_workers": 2,
+            "epochs": 1,
+            "half_lr": True,
+            "early_stop": True,
+            "aggregate": 1,
+        },
+        "optim": {"lr": 0.001},
+        "main_args": {"exp_dir": "exp/tmp"},
+    }
+
+
+def create_mock_dataset(
+    root_dir,
+    duration=1,
+    sample_rate=16000,
+):
+    track_names = [
+        "Actions - One Minute Smile",
+        "Actions - South Of The Water",
+        "A Classic Education - NightOwl",
+    ]
+    for track_name in track_names:
+        length = duration * sample_rate
+        waveform = np.random.randn(int(length))
+
+        track_dir = os.path.join(root_dir, "train", track_name)
+        os.makedirs(track_dir, exist_ok=True)
+        for source in ["vocals", "drums", "bass", "other", "mixture"]:
+            source_path = os.path.join(track_dir, f"{source}.wav")
+            wavfile.write(source_path, sample_rate, waveform)
+
+
+def test_create_datasets_and_loaders(mock_conf):
+    # Create a temporary directory
+    with tempfile.TemporaryDirectory() as temp_dir:
+        # Create a mock dataset with files
+        create_mock_dataset(temp_dir)
+        # Update the data root directory in the configuration
+        mock_conf["data"]["root"] = temp_dir
+
+        train_loader, train_set, val_loader, val_set = create_datasets_and_loaders(
+            mock_conf
+        )
+
+        # Assertions can be made based on the created loaders or datasets
+        assert isinstance(train_loader, torch.utils.data.DataLoader)
+        assert isinstance(val_loader, torch.utils.data.DataLoader)
+        assert len(train_set) > 0
+        assert len(val_set) > 0
+
+
+def test_create_model_and_optimizer(mock_conf):
+    model, optimizer, scheduler = create_model_and_optimizer(mock_conf)
+    assert isinstance(model, ConvTasNet)
+    assert isinstance(optimizer, torch.optim.Adam)
+    if mock_conf["training"]["half_lr"]:
+        assert isinstance(scheduler, torch.optim.lr_scheduler.ReduceLROnPlateau)
+
+
+def test_create_trainer(mock_conf):
+    callbacks = []
+    trainer = create_trainer(mock_conf, callbacks)
+    assert isinstance(trainer, pl.Trainer)
+
+
+def test_get_loss_func():
+    loss_func = get_loss_func()
+    assert isinstance(loss_func, torch.nn.L1Loss)
+
+
+def test_create_callbacks(mock_conf):
+    exp_dir = mock_conf["main_args"]["exp_dir"]
+    os.makedirs(exp_dir, exist_ok=True)
+    callbacks, checkpoint = create_callbacks(mock_conf, exp_dir)
+    assert isinstance(callbacks, list)
+    assert isinstance(checkpoint, ModelCheckpoint)
+
+
+def test_save_best_model(mock_conf):
+    with tempfile.TemporaryDirectory() as temp_dir:
+        # Mock system, checkpoint, and train_set
+        mock_system = MagicMock()
+        mock_system.model.serialize.return_value = {}
+
+        mock_checkpoint = MagicMock()
+        mock_checkpoint.best_k_models = {
+            "model_1": torch.Tensor([0.50]),
+            "model_2": torch.Tensor([0.3]),
+        }
+        mock_checkpoint.best_model_path = os.path.join(temp_dir, "best_model.ckpt")
+
+        # Create a dummy state_dict to save
+        torch.save({"state_dict": {}}, mock_checkpoint.best_model_path)
+
+        mock_train_set = MagicMock()
+        mock_train_set.get_infos.return_value = {}
+
+        # Call the function under test
+        save_best_model(mock_system, mock_checkpoint, temp_dir, mock_train_set)
+
+        # Verify that the best_k_models.json file is created
+        best_k_models_path = os.path.join(temp_dir, "best_k_models.json")
+        assert os.path.exists(best_k_models_path)
+
+        # Verify that the best_model.pth file is created
+        best_model_path = os.path.join(temp_dir, "best_model.pth")
+        assert os.path.exists(best_model_path)
+
+        # Load and check the contents of best_k_models.json
+        with open(best_k_models_path, encoding="utf-8") as f:
+            best_k_models = json.load(f)
+            assert best_k_models == {
+                "model_1": torch.Tensor([0.5]),
+                "model_2": torch.Tensor([0.3]),
+            }