Repository: huggingface/datasets
Pull request: Add AudioFolder packaged loader
will close  #3964

AudioFolder is almost identical to ImageFolder except for inferring labels is not the default behavior (`drop_labels` is set to True in config), the option of inferring them is preserved though.

The weird thing is happening with the `test_data_files_with_metadata_and_archives` when `streaming` is `True`. Here is the log from the CI:
```

../.pyenv/versions/3.6.15/lib/python3.6/site-packages/datasets/features/audio.py:237: in _decode_non_mp3_path_like
    array, sampling_rate = librosa.load(f, sr=self.sampling_rate, mono=self.mono)
../.pyenv/versions/3.6.15/lib/python3.6/site-packages/librosa/util/decorators.py:88: in inner_f
    return f(*args, **kwargs)
../.pyenv/versions/3.6.15/lib/python3.6/site-packages/librosa/core/audio.py:176: in load
    raise (exc)
../.pyenv/versions/3.6.15/lib/python3.6/site-packages/librosa/core/audio.py:155: in load
    context = sf.SoundFile(path)
../.pyenv/versions/3.6.15/lib/python3.6/site-packages/soundfile.py:629: in __init__
    self._file = self._open(file, mode_int, closefd)
../.pyenv/versions/3.6.15/lib/python3.6/site-packages/soundfile.py:1184: in _open
    "Error opening {0!r}: ".format(self.name))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

err = 72
prefix = "Error opening <zipfile.ZipExtFile name='audio_file.wav' mode='r' compress_type=deflate>: "

    def _error_check(err, prefix=""):
        """Pretty-print a numerical error code if there is an error."""
        if err != 0:
            err_str = _snd.sf_error_number(err)
>           raise RuntimeError(prefix + _ffi.string(err_str).decode('utf-8', 'replace'))
E           RuntimeError: Error opening <zipfile.ZipExtFile name='audio_file.wav' mode='r' compress_type=deflate>: Error in WAV file. No 'data' chunk marker.
```
I hadn't been able to reproduce this locally until I created the same test environment (I mean with `pip install .[tests]`) with python3.6. The same env but with python3.8 passes the test! I didn't manage to figure out what's wrong, I also tried simply to replace the test wav file and still got the same error. Versions of `soundfile`, `librosa` and `libsndfile` are identical. Might it be something with zip compression? Sounds weird but I don't have any other ideas... 
 
TODO:

- [x] align with #4622
- [x] documentation
- [x] tests for AutoFolder?

Problem statement:
Add default Audio Loader
**Is your feature request related to a problem? Please describe.**
Writing a custom loading dataset script might be a bit challenging for users.

**Describe the solution you'd like**
Add default Audio loader (analogous to ImageFolder) for small datasets with standard directory structure.

**Describe alternatives you've considered**
Create a custom loading script? that's what users doing now.

Hints from issue discussion:

Commit message: patch autofolder for streaming manually

Patch to review:
diff --git a/datasets/audiofolder/README.md b/datasets/audiofolder/README.md
new file mode 100644
index 00000000000..7ab03192318
--- /dev/null
+++ b/datasets/audiofolder/README.md
@@ -0,0 +1,4 @@
+
+### Contributions
+
+Thanks to [@polinaeterna](https://github.com/polinaeterna), [@nateraw](https://github.com/nateraw), [@lhoestq](https://github.com/lhoestq) and [@mariosasko](https://github.com/mariosasko) for adding this dataset.
diff --git a/datasets/audiofolder/dummy/0.0.0/dummy_data.zip b/datasets/audiofolder/dummy/0.0.0/dummy_data.zip
new file mode 100644
index 00000000000..b8afccba2fa
Binary files /dev/null and b/datasets/audiofolder/dummy/0.0.0/dummy_data.zip differ
diff --git a/src/datasets/packaged_modules/__init__.py b/src/datasets/packaged_modules/__init__.py
index be5d1669b31..deabca1f35a 100644
--- a/src/datasets/packaged_modules/__init__.py
+++ b/src/datasets/packaged_modules/__init__.py
@@ -3,6 +3,7 @@
 from hashlib import sha256
 from typing import List
 
+from .audiofolder import audiofolder
 from .csv import csv
 from .imagefolder import imagefolder
 from .json import json
@@ -32,6 +33,7 @@ def _hash_python_lines(lines: List[str]) -> str:
     "parquet": (parquet.__name__, _hash_python_lines(inspect.getsource(parquet).splitlines())),
     "text": (text.__name__, _hash_python_lines(inspect.getsource(text).splitlines())),
     "imagefolder": (imagefolder.__name__, _hash_python_lines(inspect.getsource(imagefolder).splitlines())),
+    "audiofolder": (audiofolder.__name__, _hash_python_lines(inspect.getsource(audiofolder).splitlines())),
 }
 
 _EXTENSION_TO_MODULE = {
@@ -42,7 +44,8 @@ def _hash_python_lines(lines: List[str]) -> str:
     "parquet": ("parquet", {}),
     "txt": ("text", {}),
 }
-_EXTENSION_TO_MODULE.update({ext[1:]: ("imagefolder", {}) for ext in imagefolder.ImageFolder.IMAGE_EXTENSIONS})
-_EXTENSION_TO_MODULE.update({ext[1:].upper(): ("imagefolder", {}) for ext in imagefolder.ImageFolder.IMAGE_EXTENSIONS})
-
-_MODULE_SUPPORTS_METADATA = {"imagefolder"}
+_EXTENSION_TO_MODULE.update({ext[1:]: ("imagefolder", {}) for ext in imagefolder.ImageFolder.EXTENSIONS})
+_EXTENSION_TO_MODULE.update({ext[1:].upper(): ("imagefolder", {}) for ext in imagefolder.ImageFolder.EXTENSIONS})
+_EXTENSION_TO_MODULE.update({ext[1:]: ("audiofolder", {}) for ext in audiofolder.AudioFolder.EXTENSIONS})
+_EXTENSION_TO_MODULE.update({ext[1:].upper(): ("audiofolder", {}) for ext in audiofolder.AudioFolder.EXTENSIONS})
+_MODULE_SUPPORTS_METADATA = {"imagefolder", "audiofolder"}
diff --git a/src/datasets/packaged_modules/audiofolder/__init__.py b/src/datasets/packaged_modules/audiofolder/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/datasets/packaged_modules/audiofolder/audiofolder.py b/src/datasets/packaged_modules/audiofolder/audiofolder.py
new file mode 100644
index 00000000000..c5248caf732
--- /dev/null
+++ b/src/datasets/packaged_modules/audiofolder/audiofolder.py
@@ -0,0 +1,80 @@
+from dataclasses import dataclass
+from typing import ClassVar, List
+
+import datasets
+
+from ..base import autofolder
+
+
+logger = datasets.utils.logging.get_logger(__name__)
+
+
+@dataclass
+class AudioFolderConfig(autofolder.AutoFolderConfig):
+    """Builder Config for AudioFolder."""
+
+    base_feature: ClassVar = datasets.Audio()
+    drop_labels: bool = True  # usually we don't need labels as classification is not the main audio task
+    drop_metadata: bool = False
+
+
+class AudioFolder(autofolder.AutoFolder):
+    BUILDER_CONFIG_CLASS = AudioFolderConfig
+    EXTENSIONS: List[str] = []  # definition at the bottom of the script
+
+    def _info(self):
+        return datasets.DatasetInfo(features=self.config.features)
+
+    def _split_generators(self, dl_manager):
+        # _prepare_split_generators() sets self.info.features,
+        # infers labels, finds metadata files if needed and returns splits
+        return self._prepare_split_generators(dl_manager)
+
+    def _generate_examples(self, files, metadata_files, split_name):
+        generator = self._prepare_generate_examples(files, metadata_files, split_name)
+        for _, example in generator:
+            yield _, example
+
+
+# Obtained with:
+# ```
+# import soundfile as sf
+#
+# AUDIO_EXTENSIONS = [f".{format.lower()}" for format in sf.available_formats().keys()]
+#
+# # .mp3 is currently decoded via `torchaudio`, .opus decoding is supported if version of `libsndfile` >= 1.0.30:
+# AUDIO_EXTENSIONS.extend([".mp3", ".opus"])
+# ```
+# We intentionally do not run this code on launch because:
+# (1) Soundfile is an optional dependency, so importing it in global namespace is not allowed
+# (2) To ensure the list of supported extensions is deterministic
+AUDIO_EXTENSIONS = [
+    ".aiff",
+    ".au",
+    ".avr",
+    ".caf",
+    ".flac",
+    ".htk",
+    ".svx",
+    ".mat4",
+    ".mat5",
+    ".mpc2k",
+    ".ogg",
+    ".paf",
+    ".pvf",
+    ".raw",
+    ".rf64",
+    ".sd2",
+    ".sds",
+    ".ircam",
+    ".voc",
+    ".w64",
+    ".wav",
+    ".nist",
+    ".wavex",
+    ".wve",
+    ".xi",
+    ".mp3",
+    ".opus",
+]
+AudioFolder.EXTENSIONS = AUDIO_EXTENSIONS
diff --git a/src/datasets/packaged_modules/base/__init__.py b/src/datasets/packaged_modules/base/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/src/datasets/packaged_modules/base/autofolder.py b/src/datasets/packaged_modules/base/autofolder.py
new file mode 100644
index 00000000000..5279ff3b6d8
--- /dev/null
+++ b/src/datasets/packaged_modules/base/autofolder.py
@@ -0,0 +1,376 @@
+import collections
+import itertools
+import os
+from dataclasses import dataclass
+from typing import ClassVar, List, Optional, Tuple
+
+import pyarrow.compute as pc
+import pyarrow.json as paj
+
+import datasets
+
+
+logger = datasets.utils.logging.get_logger(__name__)
+
+
+if datasets.config.PYARROW_VERSION.major >= 7:
+
+    def pa_table_to_pylist(table):
+        return table.to_pylist()
+
+else:
+
+    def pa_table_to_pylist(table):
+        keys = table.column_names
+        values = table.to_pydict().values()
+        return [{k: v for k, v in zip(keys, row_values)} for row_values in zip(*values)]
+
+
+def count_path_segments(path):
+    cnt = 0
+    while True:
+        parts = os.path.split(path)
+        if parts[0] == path:
+            break
+        elif parts[1] == path:
+            break
+        else:
+            path = parts[0]
+            cnt += 1
+    return cnt
+
+
+@dataclass
+class AutoFolderConfig(datasets.BuilderConfig):
+    """BuilderConfig for AutoFolder."""
+
+    base_feature: ClassVar = None  # i.e. datasets.Image(), datasets.Audio(), we don't have a type for Feature ("FieldType") # TODO: what type?
+    base_feature_name: str = ""
+    label_column: str = "label"
+    features: Optional[datasets.Features] = None
+    drop_labels: bool = False
+    drop_metadata: bool = False
+
+    def __post_init__(self):
+        if not self.base_feature_name:
+            # automatically infer feature name from feature if it's not provided
+            self.base_feature_name = self.base_feature._type.lower()
+
+
+class AutoFolder(datasets.GeneratorBasedBuilder):
+    BUILDER_CONFIG_CLASS = AutoFolderConfig
+    EXTENSIONS: List[str] = []
+    SKIP_CHECKSUM_COMPUTATION_BY_DEFAULT = True
+    METADATA_FILENAME: str = "metadata.jsonl"
+
+    def _info(self):
+        return datasets.DatasetInfo(features=self.config.features)
+
+    def _prepare_split_generators(self, dl_manager):
+        if not self.config.data_files:
+            raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
+
+        # Do an early pass if:
+        # * `features` are not specified, to infer the class labels
+        # * `drop_metadata` is False, to find the metadata files
+        do_analyze = (self.config.features is None and not self.config.drop_labels) or not self.config.drop_metadata
+        if do_analyze:
+            labels = set()
+            metadata_files = collections.defaultdict(set)
+
+            def analyze(files_or_archives, downloaded_files_or_dirs, split):
+                if len(downloaded_files_or_dirs) == 0:
+                    return
+                # The files are separated from the archives at this point, so check the first sample
+                # to see if it's a file or a directory and iterate accordingly
+                if os.path.isfile(downloaded_files_or_dirs[0]):
+                    original_files, downloaded_files = files_or_archives, downloaded_files_or_dirs
+                    for original_file, downloaded_file in zip(original_files, downloaded_files):
+                        original_file, downloaded_file = str(original_file), str(downloaded_file)
+                        _, original_file_ext = os.path.splitext(original_file)
+                        if original_file_ext.lower() in self.EXTENSIONS and not self.config.drop_labels:
+                            labels.add(os.path.basename(os.path.dirname(original_file)))
+                        elif (
+                            os.path.basename(original_file) == self.METADATA_FILENAME and not self.config.drop_metadata
+                        ):
+                            metadata_files[split].add((original_file, downloaded_file))
+                        else:
+                            original_file_name = os.path.basename(original_file)
+                            logger.debug(
+                                f"The file '{original_file_name}' was ignored: it is not an {self.config.base_feature_name}, and is not {self.METADATA_FILENAME} either."
+                            )
+                else:
+                    archives, downloaded_dirs = files_or_archives, downloaded_files_or_dirs
+                    for archive, downloaded_dir in zip(archives, downloaded_dirs):
+                        archive, downloaded_dir = str(archive), str(downloaded_dir)
+                        for downloaded_dir_file in dl_manager.iter_files(downloaded_dir):
+                            _, downloaded_dir_file_ext = os.path.splitext(downloaded_dir_file)
+                            if downloaded_dir_file_ext in self.EXTENSIONS and not self.config.drop_labels:
+                                labels.add(os.path.basename(os.path.dirname(downloaded_dir_file)))
+                            elif (
+                                os.path.basename(downloaded_dir_file) == self.METADATA_FILENAME
+                                and not self.config.drop_metadata
+                            ):
+                                metadata_files[split].add((None, downloaded_dir_file))
+                            else:
+                                archive_file_name = os.path.basename(archive)
+                                original_file_name = os.path.basename(downloaded_dir_file)
+                                logger.debug(
+                                    f"The file '{original_file_name}' from the archive '{archive_file_name}' was ignored: it is not an {self.config.base_feature_name}, and is not {self.METADATA_FILENAME} either."
+                                )
+
+            if not self.config.drop_labels:
+                logger.info("Inferring labels from data files...")
+            if not self.config.drop_metadata:
+                logger.info("Analyzing metadata files...")
+
+        data_files = self.config.data_files
+        splits = []
+        for split_name, files in data_files.items():
+            if isinstance(files, str):
+                files = [files]
+            files, archives = self._split_files_and_archives(files)
+            downloaded_files = dl_manager.download(files)
+            downloaded_dirs = dl_manager.download_and_extract(archives)
+            if do_analyze:
+                analyze(files, downloaded_files, split_name)
+                analyze(archives, downloaded_dirs, split_name)
+            splits.append(
+                datasets.SplitGenerator(
+                    name=split_name,
+                    gen_kwargs={
+                        "files": [(file, downloaded_file) for file, downloaded_file in zip(files, downloaded_files)]
+                        + [(None, dl_manager.iter_files(downloaded_dir)) for downloaded_dir in downloaded_dirs],
+                        "metadata_files": metadata_files if not self.config.drop_metadata else None,
+                        "split_name": split_name,
+                    },
+                )
+            )
+
+        if not self.config.drop_metadata and metadata_files:
+            # Verify that:
+            # * all metadata files have the same set of features
+            # * the `file_name` key is one of the metadata keys and is of type string
+            features_per_metadata_file: List[Tuple[str, datasets.Features]] = []
+            for _, downloaded_metadata_file in itertools.chain.from_iterable(metadata_files.values()):
+                with open(downloaded_metadata_file, "rb") as f:
+                    pa_metadata_table = paj.read_json(f)
+                features_per_metadata_file.append(
+                    (downloaded_metadata_file, datasets.Features.from_arrow_schema(pa_metadata_table.schema))
+                )
+            for downloaded_metadata_file, metadata_features in features_per_metadata_file:
+                if metadata_features != features_per_metadata_file[0][1]:
+                    raise ValueError(
+                        f"Metadata files {downloaded_metadata_file} and {features_per_metadata_file[0][0]} have different features: {features_per_metadata_file[0]} != {metadata_features}"
+                    )
+            metadata_features = features_per_metadata_file[0][1]
+            if "file_name" not in metadata_features:
+                raise ValueError("`file_name` must be present as dictionary key in metadata files")
+            if metadata_features["file_name"] != datasets.Value("string"):
+                raise ValueError("`file_name` key must be a string")
+            del metadata_features["file_name"]
+        else:
+            metadata_features = None
+
+        # Normally, we would do this in _info, but we need to know the labels and/or metadata
+        # before building the features
+        if self.config.features is None:
+            if not self.config.drop_labels and not metadata_files:
+                self.info.features = datasets.Features(
+                    {
+                        self.config.base_feature_name: self.config.base_feature,
+                        self.config.label_column: datasets.ClassLabel(names=sorted(labels)),
+                    }
+                )
+            else:
+                self.info.features = datasets.Features({self.config.base_feature_name: self.config.base_feature})
+
+            if not self.config.drop_metadata and metadata_files:
+                # Verify that there are no duplicated keys when compared to the existing features
+                # (self.config.base_feature, optionally self.config.label_column)
+                duplicated_keys = set(self.info.features) & set(metadata_features)
+                if duplicated_keys:
+                    raise ValueError(
+                        f"Metadata feature keys {list(duplicated_keys)} are already present as the {self.config.base_feature_name} features"
+                    )
+                self.info.features.update(metadata_features)
+
+        return splits  # labels, metadata_files, metadata_features
+
+    def _split_files_and_archives(self, data_files):
+        files, archives = [], []
+        for data_file in data_files:
+            _, data_file_ext = os.path.splitext(data_file)
+            if data_file_ext.lower() in self.EXTENSIONS:
+                files.append(data_file)
+            elif os.path.basename(data_file) == self.METADATA_FILENAME:
+                files.append(data_file)
+            else:
+                archives.append(data_file)
+        return files, archives
+
+    def _prepare_generate_examples(self, files, metadata_files, split_name):
+        if not self.config.drop_metadata and metadata_files:
+            split_metadata_files = metadata_files.get(split_name, [])
+            sample_empty_metadata = {k: None for k in self.info.features if k != self.config.base_feature_name}
+
+            last_checked_dir = None
+            metadata_dir = None
+            metadata_dict = None
+            downloaded_metadata_file = None
+
+            file_idx = 0
+            for original_file, downloaded_file_or_dir in files:
+                if original_file is not None:
+                    _, original_file_ext = os.path.splitext(original_file)
+                    if original_file_ext.lower() in self.EXTENSIONS:
+                        # If the file is a file of a needed type, and we've just entered a new directory,
+                        # find the nereast metadata file (by counting path segments) for the directory
+                        current_dir = os.path.dirname(original_file)
+                        if last_checked_dir is None or last_checked_dir != current_dir:
+                            last_checked_dir = current_dir
+                            metadata_file_candidates = [
+                                (
+                                    os.path.relpath(original_file, os.path.dirname(metadata_file_candidate)),
+                                    metadata_file_candidate,
+                                    downloaded_metadata_file,
+                                )
+                                for metadata_file_candidate, downloaded_metadata_file in split_metadata_files
+                                if metadata_file_candidate
+                                is not None  # ignore metadata_files that are inside archives
+                                and not os.path.relpath(
+                                    original_file, os.path.dirname(metadata_file_candidate)
+                                ).startswith("..")
+                            ]
+                            if metadata_file_candidates:
+                                _, metadata_file, downloaded_metadata_file = min(
+                                    metadata_file_candidates, key=lambda x: count_path_segments(x[0])
+                                )
+                                with open(downloaded_metadata_file, "rb") as f:
+                                    pa_metadata_table = paj.read_json(f)
+                                pa_file_name_array = pa_metadata_table["file_name"]
+                                pa_file_name_array = pc.replace_substring(
+                                    pa_file_name_array, pattern="\\", replacement="/"
+                                )
+                                pa_metadata_table = pa_metadata_table.drop(["file_name"])
+                                metadata_dir = os.path.dirname(metadata_file)
+                                metadata_dict = {
+                                    file_name: sample_metadata
+                                    for file_name, sample_metadata in zip(
+                                        pa_file_name_array.to_pylist(), pa_table_to_pylist(pa_metadata_table)
+                                    )
+                                }
+                            else:
+                                raise ValueError(
+                                    f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_file_or_dir}."
+                                )
+                        if metadata_dir is not None and downloaded_metadata_file is not None:
+                            file_relpath = os.path.relpath(original_file, metadata_dir)
+                            file_relpath = file_relpath.replace("\\", "/")
+                            if file_relpath not in metadata_dict:
+                                raise ValueError(
+                                    f"{self.config.base_feature_name} at {file_relpath} doesn't have metadata in {downloaded_metadata_file}."
+                                )
+                            sample_metadata = metadata_dict[file_relpath]
+                        else:
+                            raise ValueError(
+                                f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_file_or_dir}."
+                            )
+                        yield file_idx, {
+                            **sample_empty_metadata,
+                            self.config.base_feature_name: downloaded_file_or_dir,
+                            **sample_metadata,
+                        }
+                        file_idx += 1
+                else:
+                    for downloaded_dir_file in downloaded_file_or_dir:
+                        _, downloaded_dir_file_ext = os.path.splitext(downloaded_dir_file)
+                        if downloaded_dir_file_ext.lower() in self.EXTENSIONS:
+                            current_dir = os.path.dirname(downloaded_dir_file)
+                            if last_checked_dir is None or last_checked_dir != current_dir:
+                                last_checked_dir = current_dir
+                                metadata_file_candidates = [
+                                    (
+                                        os.path.relpath(
+                                            downloaded_dir_file, os.path.dirname(downloaded_metadata_file)
+                                        ),
+                                        metadata_file_candidate,
+                                        downloaded_metadata_file,
+                                    )
+                                    for metadata_file_candidate, downloaded_metadata_file in split_metadata_files
+                                    if metadata_file_candidate
+                                    is None  # ignore metadata_files that are not inside archives
+                                    and not os.path.relpath(
+                                        downloaded_dir_file, os.path.dirname(downloaded_metadata_file)
+                                    ).startswith("..")
+                                ]
+                                if metadata_file_candidates:
+                                    _, metadata_file, downloaded_metadata_file = min(
+                                        metadata_file_candidates, key=lambda x: count_path_segments(x[0])
+                                    )
+                                    with open(downloaded_metadata_file, "rb") as f:
+                                        pa_metadata_table = paj.read_json(f)
+                                    pa_file_name_array = pa_metadata_table["file_name"]
+                                    pa_file_name_array = pc.replace_substring(
+                                        pa_file_name_array, pattern="\\", replacement="/"
+                                    )
+                                    pa_metadata_table = pa_metadata_table.drop(["file_name"])
+                                    metadata_dir = os.path.dirname(downloaded_metadata_file)
+                                    metadata_dict = {
+                                        file_name: sample_metadata
+                                        for file_name, sample_metadata in zip(
+                                            pa_file_name_array.to_pylist(), pa_table_to_pylist(pa_metadata_table)
+                                        )
+                                    }
+                                else:
+                                    raise ValueError(
+                                        f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_dir_file}."
+                                    )
+                            if metadata_dir is not None and downloaded_metadata_file is not None:
+                                downloaded_dir_file_relpath = os.path.relpath(downloaded_dir_file, metadata_dir)
+                                downloaded_dir_file_relpath = downloaded_dir_file_relpath.replace("\\", "/")
+                                if downloaded_dir_file_relpath not in metadata_dict:
+                                    raise ValueError(
+                                        f"{self.config.base_feature_name} at {downloaded_dir_file_relpath} doesn't have metadata in {downloaded_metadata_file}."
+                                    )
+                                sample_metadata = metadata_dict[downloaded_dir_file_relpath]
+                            else:
+                                raise ValueError(
+                                    f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_dir_file}."
+                                )
+                            yield file_idx, {
+                                **sample_empty_metadata,
+                                self.config.base_feature_name: downloaded_dir_file,
+                                **sample_metadata,
+                            }
+                            file_idx += 1
+        else:
+            file_idx = 0
+            for original_file, downloaded_file_or_dir in files:
+                if original_file is not None:
+                    _, original_file_ext = os.path.splitext(original_file)
+                    if original_file_ext.lower() in self.EXTENSIONS:
+                        if self.config.drop_labels or metadata_files:  # TODO: align this with #4622
+                            yield file_idx, {
+                                self.config.base_feature_name: downloaded_file_or_dir,
+                            }
+                        else:
+                            yield file_idx, {
+                                self.config.base_feature_name: downloaded_file_or_dir,
+                                self.config.label_column: os.path.basename(os.path.dirname(original_file)),
+                            }
+                        file_idx += 1
+                else:
+                    for downloaded_dir_file in downloaded_file_or_dir:
+                        _, downloaded_dir_file_ext = os.path.splitext(downloaded_dir_file)
+                        if downloaded_dir_file_ext.lower() in self.EXTENSIONS:
+                            if self.config.drop_labels or metadata_files:  # TODO: align this with #4622
+                                yield file_idx, {
+                                    self.config.base_feature_name: downloaded_dir_file,
+                                }
+                            else:
+                                yield file_idx, {
+                                    self.config.base_feature_name: downloaded_dir_file,
+                                    self.config.label_column: os.path.basename(os.path.dirname(downloaded_dir_file)),
+                                }
+                            file_idx += 1
diff --git a/src/datasets/packaged_modules/imagefolder/imagefolder.py b/src/datasets/packaged_modules/imagefolder/imagefolder.py
index ecb99092e92..fa24943863b 100644
--- a/src/datasets/packaged_modules/imagefolder/imagefolder.py
+++ b/src/datasets/packaged_modules/imagefolder/imagefolder.py
@@ -1,367 +1,52 @@
-import collections
-import itertools
-import os
 from dataclasses import dataclass
-from typing import List, Optional, Tuple
-
-import pyarrow.compute as pc
-import pyarrow.json as paj
+from typing import ClassVar, List
 
 import datasets
 from datasets.tasks import ImageClassification
 
+from ..base import autofolder
 
-logger = datasets.utils.logging.get_logger(__name__)
-
-
-if datasets.config.PYARROW_VERSION.major >= 7:
-
-    def pa_table_to_pylist(table):
-        return table.to_pylist()
 
-else:
-
-    def pa_table_to_pylist(table):
-        keys = table.column_names
-        values = table.to_pydict().values()
-        return [{k: v for k, v in zip(keys, row_values)} for row_values in zip(*values)]
-
-
-def count_path_segments(path):
-    cnt = 0
-    while True:
-        parts = os.path.split(path)
-        if parts[0] == path:
-            break
-        elif parts[1] == path:
-            break
-        else:
-            path = parts[0]
-            cnt += 1
-    return cnt
+logger = datasets.utils.logging.get_logger(__name__)
 
 
 @dataclass
-class ImageFolderConfig(datasets.BuilderConfig):
+class ImageFolderConfig(autofolder.AutoFolderConfig):
     """BuilderConfig for ImageFolder."""
 
-    features: Optional[datasets.Features] = None
+    base_feature: ClassVar = datasets.Image()
     drop_labels: bool = False
     drop_metadata: bool = False
 
 
-class ImageFolder(datasets.GeneratorBasedBuilder):
+class ImageFolder(autofolder.AutoFolder):
     BUILDER_CONFIG_CLASS = ImageFolderConfig
-
-    IMAGE_EXTENSIONS: List[str] = []  # definition at the bottom of the script
-    SKIP_CHECKSUM_COMPUTATION_BY_DEFAULT = True
-    METADATA_FILENAME: str = "metadata.jsonl"
+    EXTENSIONS: List[str] = []  # definition at the bottom of the script
 
     def _info(self):
         return datasets.DatasetInfo(features=self.config.features)
 
     def _split_generators(self, dl_manager):
-        if not self.config.data_files:
-            raise ValueError(f"At least one data file must be specified, but got data_files={self.config.data_files}")
-
-        # Do an early pass if:
-        # * `features` are not specified, to infer the class labels
-        # * `drop_metadata` is False, to find the metadata files
-        do_analyze = (self.config.features is None and not self.config.drop_labels) or not self.config.drop_metadata
-        if do_analyze:
-            labels = set()
-            metadata_files = collections.defaultdict(set)
-
-            def analyze(files_or_archives, downloaded_files_or_dirs, split):
-                if len(downloaded_files_or_dirs) == 0:
-                    return
-                # The files are separated from the archives at this point, so check the first sample
-                # to see if it's a file or a directory and iterate accordingly
-                if os.path.isfile(downloaded_files_or_dirs[0]):
-                    original_files, downloaded_files = files_or_archives, downloaded_files_or_dirs
-                    for original_file, downloaded_file in zip(original_files, downloaded_files):
-                        original_file, downloaded_file = str(original_file), str(downloaded_file)
-                        _, original_file_ext = os.path.splitext(original_file)
-                        if original_file_ext.lower() in self.IMAGE_EXTENSIONS:
-                            labels.add(os.path.basename(os.path.dirname(original_file)))
-                        elif os.path.basename(original_file) == self.METADATA_FILENAME:
-                            metadata_files[split].add((original_file, downloaded_file))
-                        else:
-                            original_file_name = os.path.basename(original_file)
-                            logger.debug(
-                                f"The file '{original_file_name}' was ignored: it is not an image, and is not {self.METADATA_FILENAME} either."
-                            )
-                else:
-                    archives, downloaded_dirs = files_or_archives, downloaded_files_or_dirs
-                    for archive, downloaded_dir in zip(archives, downloaded_dirs):
-                        archive, downloaded_dir = str(archive), str(downloaded_dir)
-                        for downloaded_dir_file in dl_manager.iter_files(downloaded_dir):
-                            _, downloaded_dir_file_ext = os.path.splitext(downloaded_dir_file)
-                            if downloaded_dir_file_ext in self.IMAGE_EXTENSIONS:
-                                labels.add(os.path.basename(os.path.dirname(downloaded_dir_file)))
-                            elif os.path.basename(downloaded_dir_file) == self.METADATA_FILENAME:
-                                metadata_files[split].add((None, downloaded_dir_file))
-                            else:
-                                archive_file_name = os.path.basename(archive)
-                                original_file_name = os.path.basename(downloaded_dir_file)
-                                logger.debug(
-                                    f"The file '{original_file_name}' from the archive '{archive_file_name}' was ignored: it is not an image, and is not {self.METADATA_FILENAME} either."
-                                )
-
-            if not self.config.drop_labels:
-                logger.info("Inferring labels from data files...")
-            if not self.config.drop_metadata:
-                logger.info("Analyzing metadata files...")
-
-        data_files = self.config.data_files
-        splits = []
-        for split_name, files in data_files.items():
-            if isinstance(files, str):
-                files = [files]
-            files, archives = self._split_files_and_archives(files)
-            downloaded_files = dl_manager.download(files)
-            downloaded_dirs = dl_manager.download_and_extract(archives)
-            if do_analyze:
-                analyze(files, downloaded_files, split_name)
-                analyze(archives, downloaded_dirs, split_name)
-            splits.append(
-                datasets.SplitGenerator(
-                    name=split_name,
-                    gen_kwargs={
-                        "files": [(file, downloaded_file) for file, downloaded_file in zip(files, downloaded_files)]
-                        + [(None, dl_manager.iter_files(downloaded_dir)) for downloaded_dir in downloaded_dirs],
-                        "metadata_files": metadata_files if not self.config.drop_metadata else None,
-                        "split_name": split_name,
-                    },
-                )
+        # _prepare_split_generators() sets self.info.features,
+        # infers labels, finds metadata files if needed and returns splits
+        splits = self._prepare_split_generators(dl_manager)
+
+        # we need to set a task_template manually
+        # to check if metadata files were found, see if they are in the first split kwargs
+        # (metadata files passed to _generate_examples() are the same for each split)
+        if not self.config.features and not self.config.drop_labels and not splits[0].gen_kwargs["metadata_files"]:
+            task_template = ImageClassification(
+                image_column=self.config.base_feature_name, label_column=self.config.label_column
             )
-
-        if not self.config.drop_metadata and metadata_files:
-            # Verify that:
-            # * all metadata files have the same set of features
-            # * the `file_name` key is one of the metadata keys and is of type string
-            features_per_metadata_file: List[Tuple[str, datasets.Features]] = []
-            for _, downloaded_metadata_file in itertools.chain.from_iterable(metadata_files.values()):
-                with open(downloaded_metadata_file, "rb") as f:
-                    pa_metadata_table = paj.read_json(f)
-                features_per_metadata_file.append(
-                    (downloaded_metadata_file, datasets.Features.from_arrow_schema(pa_metadata_table.schema))
-                )
-            for downloaded_metadata_file, metadata_features in features_per_metadata_file:
-                if metadata_features != features_per_metadata_file[0][1]:
-                    raise ValueError(
-                        f"Metadata files {downloaded_metadata_file} and {features_per_metadata_file[0][0]} have different features: {features_per_metadata_file[0]} != {metadata_features}"
-                    )
-            metadata_features = features_per_metadata_file[0][1]
-            if "file_name" not in metadata_features:
-                raise ValueError("`file_name` must be present as dictionary key in metadata files")
-            if metadata_features["file_name"] != datasets.Value("string"):
-                raise ValueError("`file_name` key must be a string")
-            del metadata_features["file_name"]
-        else:
-            metadata_features = None
-
-        # Normally, we would do this in _info, but we need to know the labels and/or metadata
-        # before building the features
-        if self.config.features is None:
-            if not self.config.drop_labels and not metadata_files:
-                self.info.features = datasets.Features(
-                    {"image": datasets.Image(), "label": datasets.ClassLabel(names=sorted(labels))}
-                )
-                task_template = ImageClassification(image_column="image", label_column="label")
-                task_template = task_template.align_with_features(self.info.features)
-                self.info.task_templates = [task_template]
-            else:
-                self.info.features = datasets.Features({"image": datasets.Image()})
-
-            if not self.config.drop_metadata and metadata_files:
-                # Verify that there are no duplicated keys when compared to the existing features ("image", optionally "label")
-                duplicated_keys = set(self.info.features) & set(metadata_features)
-                if duplicated_keys:
-                    raise ValueError(
-                        f"Metadata feature keys {list(duplicated_keys)} are already present as the image features"
-                    )
-                self.info.features.update(metadata_features)
+            task_template = task_template.align_with_features(self.info.features)
+            self.info.task_templates = [task_template]
 
         return splits
 
-    def _split_files_and_archives(self, data_files):
-        files, archives = [], []
-        for data_file in data_files:
-            _, data_file_ext = os.path.splitext(data_file)
-            if data_file_ext.lower() in self.IMAGE_EXTENSIONS:
-                files.append(data_file)
-            elif os.path.basename(data_file) == self.METADATA_FILENAME:
-                files.append(data_file)
-            else:
-                archives.append(data_file)
-        return files, archives
-
     def _generate_examples(self, files, metadata_files, split_name):
-        if not self.config.drop_metadata and metadata_files:
-            split_metadata_files = metadata_files.get(split_name, [])
-            image_empty_metadata = {k: None for k in self.info.features if k != "image"}
-
-            last_checked_dir = None
-            metadata_dir = None
-            metadata_dict = None
-            downloaded_metadata_file = None
-
-            file_idx = 0
-            for original_file, downloaded_file_or_dir in files:
-                if original_file is not None:
-                    _, original_file_ext = os.path.splitext(original_file)
-                    if original_file_ext.lower() in self.IMAGE_EXTENSIONS:
-                        # If the file is an image, and we've just entered a new directory,
-                        # find the nereast metadata file (by counting path segments) for the directory
-                        current_dir = os.path.dirname(original_file)
-                        if last_checked_dir is None or last_checked_dir != current_dir:
-                            last_checked_dir = current_dir
-                            metadata_file_candidates = [
-                                (
-                                    os.path.relpath(original_file, os.path.dirname(metadata_file_candidate)),
-                                    metadata_file_candidate,
-                                    downloaded_metadata_file,
-                                )
-                                for metadata_file_candidate, downloaded_metadata_file in split_metadata_files
-                                if metadata_file_candidate
-                                is not None  # ignore metadata_files that are inside archives
-                                and not os.path.relpath(
-                                    original_file, os.path.dirname(metadata_file_candidate)
-                                ).startswith("..")
-                            ]
-                            if metadata_file_candidates:
-                                _, metadata_file, downloaded_metadata_file = min(
-                                    metadata_file_candidates, key=lambda x: count_path_segments(x[0])
-                                )
-                                with open(downloaded_metadata_file, "rb") as f:
-                                    pa_metadata_table = paj.read_json(f)
-                                pa_file_name_array = pa_metadata_table["file_name"]
-                                pa_file_name_array = pc.replace_substring(
-                                    pa_file_name_array, pattern="\\", replacement="/"
-                                )
-                                pa_metadata_table = pa_metadata_table.drop(["file_name"])
-                                metadata_dir = os.path.dirname(metadata_file)
-                                metadata_dict = {
-                                    file_name: image_metadata
-                                    for file_name, image_metadata in zip(
-                                        pa_file_name_array.to_pylist(), pa_table_to_pylist(pa_metadata_table)
-                                    )
-                                }
-                            else:
-                                raise ValueError(
-                                    f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_file_or_dir}."
-                                )
-                        if metadata_dir is not None and downloaded_metadata_file is not None:
-                            file_relpath = os.path.relpath(original_file, metadata_dir)
-                            file_relpath = file_relpath.replace("\\", "/")
-                            if file_relpath not in metadata_dict:
-                                raise ValueError(
-                                    f"Image at {file_relpath} doesn't have metadata in {downloaded_metadata_file}."
-                                )
-                            image_metadata = metadata_dict[file_relpath]
-                        else:
-                            raise ValueError(
-                                f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_file_or_dir}."
-                            )
-                        yield file_idx, {
-                            **image_empty_metadata,
-                            "image": downloaded_file_or_dir,
-                            **image_metadata,
-                        }
-                        file_idx += 1
-                else:
-                    for downloaded_dir_file in downloaded_file_or_dir:
-                        _, downloaded_dir_file_ext = os.path.splitext(downloaded_dir_file)
-                        if downloaded_dir_file_ext.lower() in self.IMAGE_EXTENSIONS:
-                            current_dir = os.path.dirname(downloaded_dir_file)
-                            if last_checked_dir is None or last_checked_dir != current_dir:
-                                last_checked_dir = current_dir
-                                metadata_file_candidates = [
-                                    (
-                                        os.path.relpath(
-                                            downloaded_dir_file, os.path.dirname(downloaded_metadata_file)
-                                        ),
-                                        metadata_file_candidate,
-                                        downloaded_metadata_file,
-                                    )
-                                    for metadata_file_candidate, downloaded_metadata_file in split_metadata_files
-                                    if metadata_file_candidate
-                                    is None  # ignore metadata_files that are not inside archives
-                                    and not os.path.relpath(
-                                        downloaded_dir_file, os.path.dirname(downloaded_metadata_file)
-                                    ).startswith("..")
-                                ]
-                                if metadata_file_candidates:
-                                    _, metadata_file, downloaded_metadata_file = min(
-                                        metadata_file_candidates, key=lambda x: count_path_segments(x[0])
-                                    )
-                                    with open(downloaded_metadata_file, "rb") as f:
-                                        pa_metadata_table = paj.read_json(f)
-                                    pa_file_name_array = pa_metadata_table["file_name"]
-                                    pa_file_name_array = pc.replace_substring(
-                                        pa_file_name_array, pattern="\\", replacement="/"
-                                    )
-                                    pa_metadata_table = pa_metadata_table.drop(["file_name"])
-                                    metadata_dir = os.path.dirname(downloaded_metadata_file)
-                                    metadata_dict = {
-                                        file_name: image_metadata
-                                        for file_name, image_metadata in zip(
-                                            pa_file_name_array.to_pylist(), pa_table_to_pylist(pa_metadata_table)
-                                        )
-                                    }
-                                else:
-                                    raise ValueError(
-                                        f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_dir_file}."
-                                    )
-                            if metadata_dir is not None and downloaded_metadata_file is not None:
-                                downloaded_dir_file_relpath = os.path.relpath(downloaded_dir_file, metadata_dir)
-                                downloaded_dir_file_relpath = downloaded_dir_file_relpath.replace("\\", "/")
-                                if downloaded_dir_file_relpath not in metadata_dict:
-                                    raise ValueError(
-                                        f"Image at {downloaded_dir_file_relpath} doesn't have metadata in {downloaded_metadata_file}."
-                                    )
-                                image_metadata = metadata_dict[downloaded_dir_file_relpath]
-                            else:
-                                raise ValueError(
-                                    f"One or several metadata.jsonl were found, but not in the same directory or in a parent directory of {downloaded_dir_file}."
-                                )
-                            yield file_idx, {
-                                **image_empty_metadata,
-                                "image": downloaded_dir_file,
-                                **image_metadata,
-                            }
-                            file_idx += 1
-        else:
-            file_idx = 0
-            for original_file, downloaded_file_or_dir in files:
-                if original_file is not None:
-                    _, original_file_ext = os.path.splitext(original_file)
-                    if original_file_ext.lower() in self.IMAGE_EXTENSIONS:
-                        if self.config.drop_labels or metadata_files:
-                            yield file_idx, {
-                                "image": downloaded_file_or_dir,
-                            }
-                        else:
-                            yield file_idx, {
-                                "image": downloaded_file_or_dir,
-                                "label": os.path.basename(os.path.dirname(original_file)),
-                            }
-                        file_idx += 1
-                else:
-                    for downloaded_dir_file in downloaded_file_or_dir:
-                        _, downloaded_dir_file_ext = os.path.splitext(downloaded_dir_file)
-                        if downloaded_dir_file_ext.lower() in self.IMAGE_EXTENSIONS:
-                            if self.config.drop_labels or metadata_files:
-                                yield file_idx, {
-                                    "image": downloaded_dir_file,
-                                }
-                            else:
-                                yield file_idx, {
-                                    "image": downloaded_dir_file,
-                                    "label": os.path.basename(os.path.dirname(downloaded_dir_file)),
-                                }
-                            file_idx += 1
+        generator = self._prepare_generate_examples(files, metadata_files, split_name)
+        for _, example in generator:
+            yield _, example
 
 
 # Obtained with:
@@ -376,7 +61,7 @@ def _generate_examples(self, files, metadata_files, split_name):
 # We intentionally do not run this code on launch because:
 # (1) Pillow is an optional dependency, so importing Pillow in global namespace is not allowed
 # (2) To ensure the list of supported extensions is deterministic
-ImageFolder.IMAGE_EXTENSIONS = [
+IMAGE_EXTENSIONS = [
     ".blp",
     ".bmp",
     ".dib",
@@ -441,3 +126,4 @@ def _generate_examples(self, files, metadata_files, split_name):
     ".xbm",
     ".xpm",
 ]
+ImageFolder.EXTENSIONS = IMAGE_EXTENSIONS
diff --git a/src/datasets/streaming.py b/src/datasets/streaming.py
index cd25f0153f5..95726268018 100644
--- a/src/datasets/streaming.py
+++ b/src/datasets/streaming.py
@@ -112,3 +112,9 @@ def extend_dataset_builder_for_streaming(builder: "DatasetBuilder"):
                 internal_import_name = imports[1]
                 internal_module_name = ".".join(builder.__module__.split(".")[:-1] + [internal_import_name])
                 extend_module_for_streaming(internal_module_name, use_auth_token=builder.use_auth_token)
+
+    # we need to patch autofolder.py manually because it's a parent of Image and Audio folder and it
+    # contains generation of examples (which uses `os` functions)
+    autofolder_module = [cls.__module__ for cls in type(builder).__mro__ if cls.__name__ == "AutoFolder"]
+    if autofolder_module:
+        extend_module_for_streaming(autofolder_module[0])
diff --git a/tests/conftest.py b/tests/conftest.py
index cfb82e507f5..4a268d78cfc 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -474,6 +474,11 @@ def image_file():
     return os.path.join(os.path.dirname(__file__), "features", "data", "test_image_rgb.jpg")
 
 
+@pytest.fixture
+def audio_file():
+    return os.path.join(os.path.dirname(__file__), "features", "data", "test_audio_44100.wav")
+
+
 @pytest.fixture(scope="session")
 def zip_image_path(image_file, tmp_path_factory):
     import zipfile
diff --git a/tests/packaged_modules/test_audiofolder.py b/tests/packaged_modules/test_audiofolder.py
new file mode 100644
index 00000000000..1cdc0ea217d
--- /dev/null
+++ b/tests/packaged_modules/test_audiofolder.py
@@ -0,0 +1,345 @@
+import shutil
+import textwrap
+
+import librosa
+import numpy as np
+import pytest
+import soundfile as sf
+
+from datasets import Audio, Features, Value
+from datasets.data_files import DataFilesDict, get_data_patterns_locally
+from datasets.packaged_modules.audiofolder.audiofolder import AudioFolder
+from datasets.streaming import extend_module_for_streaming
+
+from ..utils import require_sndfile
+
+
+@pytest.fixture
+def cache_dir(tmp_path):
+    return str(tmp_path / "audiofolder_cache_dir")
+
+
+@pytest.fixture
+def audio_file_with_metadata(tmp_path, audio_file):
+    audio_filename = tmp_path / "audio_file.wav"
+    shutil.copyfile(audio_file, audio_filename)
+    audio_metadata_filename = tmp_path / "metadata.jsonl"
+    audio_metadata = textwrap.dedent(
+        """\
+        {"file_name": "audio_file.wav", "text": "Audio transcription"}
+        """
+    )
+    with open(audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+    return str(audio_filename), str(audio_metadata_filename)
+
+
+@pytest.fixture
+def audio_files_with_metadata_that_misses_one_audio(tmp_path, audio_file):
+    audio_filename = tmp_path / "audio_file.wav"
+    shutil.copyfile(audio_file, audio_filename)
+    audio_filename2 = tmp_path / "audio_file2.wav"
+    shutil.copyfile(audio_file, audio_filename2)
+    audio_metadata_filename = tmp_path / "metadata.jsonl"
+    audio_metadata = textwrap.dedent(
+        """\
+        {"file_name": "audio_file.wav", "text": "Audio transcription"}
+        """
+    )
+    with open(audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+    return str(audio_filename), str(audio_filename2), str(audio_metadata_filename)
+
+
+@pytest.fixture
+def data_files_with_one_split_and_metadata(tmp_path, audio_file):
+    data_dir = tmp_path / "audiofolder_data_dir_with_metadata"
+    data_dir.mkdir(parents=True, exist_ok=True)
+    subdir = data_dir / "subdir"
+    subdir.mkdir(parents=True, exist_ok=True)
+
+    audio_filename = data_dir / "audio_file.wav"
+    shutil.copyfile(audio_file, audio_filename)
+    audio_filename2 = data_dir / "audio_file2.wav"
+    shutil.copyfile(audio_file, audio_filename2)
+    audio_filename3 = subdir / "audio_file3.wav"  # in subdir
+    shutil.copyfile(audio_file, audio_filename3)
+
+    audio_metadata_filename = data_dir / "metadata.jsonl"
+    audio_metadata = textwrap.dedent(
+        """\
+        {"file_name": "audio_file.wav", "text": "First audio transcription"}
+        {"file_name": "audio_file2.wav", "text": "Second audio transcription"}
+        {"file_name": "subdir/audio_file3.wav", "text": "Third audio transcription (in subdir)"}
+        """
+    )
+    with open(audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+    data_files_with_one_split_and_metadata = DataFilesDict.from_local_or_remote(
+        get_data_patterns_locally(str(data_dir)), str(data_dir)
+    )
+    assert len(data_files_with_one_split_and_metadata) == 1
+    assert len(data_files_with_one_split_and_metadata["train"]) == 4
+    return data_files_with_one_split_and_metadata
+
+
+@pytest.fixture
+def data_files_with_two_splits_and_metadata(tmp_path, audio_file):
+    data_dir = tmp_path / "audiofolder_data_dir_with_metadata"
+    data_dir.mkdir(parents=True, exist_ok=True)
+    train_dir = data_dir / "train"
+    train_dir.mkdir(parents=True, exist_ok=True)
+    test_dir = data_dir / "test"
+    test_dir.mkdir(parents=True, exist_ok=True)
+
+    audio_filename = train_dir / "audio_file.wav"  # train audio
+    shutil.copyfile(audio_file, audio_filename)
+    audio_filename2 = train_dir / "audio_file2.wav"  # train audio
+    shutil.copyfile(audio_file, audio_filename2)
+    audio_filename3 = test_dir / "audio_file3.wav"  # test audio
+    shutil.copyfile(audio_file, audio_filename3)
+
+    train_audio_metadata_filename = train_dir / "metadata.jsonl"
+    audio_metadata = textwrap.dedent(
+        """\
+        {"file_name": "audio_file.wav", "text": "First train audio transcription"}
+        {"file_name": "audio_file2.wav", "text": "Second train audio transcription"}
+        """
+    )
+    with open(train_audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+    test_audio_metadata_filename = test_dir / "metadata.jsonl"
+    audio_metadata = textwrap.dedent(
+        """\
+        {"file_name": "audio_file3.wav", "text": "Test audio transcription"}
+        """
+    )
+    with open(test_audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+    data_files_with_two_splits_and_metadata = DataFilesDict.from_local_or_remote(
+        get_data_patterns_locally(str(data_dir)), str(data_dir)
+    )
+    assert len(data_files_with_two_splits_and_metadata) == 2
+    assert len(data_files_with_two_splits_and_metadata["train"]) == 3
+    assert len(data_files_with_two_splits_and_metadata["test"]) == 2
+    return data_files_with_two_splits_and_metadata
+
+
+@pytest.fixture
+def data_files_with_zip_archives(tmp_path, audio_file):
+    data_dir = tmp_path / "audiofolder_data_dir_with_zip_archives"
+    data_dir.mkdir(parents=True, exist_ok=True)
+    archive_dir = data_dir / "archive"
+    archive_dir.mkdir(parents=True, exist_ok=True)
+    subdir = archive_dir / "subdir"
+    subdir.mkdir(parents=True, exist_ok=True)
+
+    audio_filename = archive_dir / "audio_file.wav"
+    shutil.copyfile(audio_file, audio_filename)
+    audio_filename2 = subdir / "audio_file2.wav"  # in subdir
+    # make sure they're two different audios
+    # Indeed we won't be able to compare the audio filenames, since the archive is not extracted in streaming mode
+    array, sampling_rate = librosa.load(str(audio_filename), sr=16000)  # original sampling rate is 44100
+    sf.write(str(audio_filename2), array, samplerate=16000)
+
+    audio_metadata_filename = archive_dir / "metadata.jsonl"
+    audio_metadata = textwrap.dedent(
+        """\
+        {"file_name": "audio_file.wav", "text": "First audio transcription"}
+        {"file_name": "subdir/audio_file2.wav", "text": "Second audio transcription (in subdir)"}
+        """
+    )
+
+    with open(audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+
+    shutil.make_archive(str(archive_dir), "zip", archive_dir)
+    shutil.rmtree(str(archive_dir))
+
+    data_files_with_zip_archives = DataFilesDict.from_local_or_remote(
+        get_data_patterns_locally(str(data_dir)), str(data_dir)
+    )
+
+    assert len(data_files_with_zip_archives) == 1
+    assert len(data_files_with_zip_archives["train"]) == 1
+    return data_files_with_zip_archives
+
+
+@require_sndfile
+@pytest.mark.parametrize("drop_labels", [True, False])
+def test_generate_examples_drop_labels(audio_file, drop_labels):
+    audiofolder = AudioFolder(drop_labels=drop_labels)
+    generator = audiofolder._generate_examples([(audio_file, audio_file)], None, "train")
+    if not drop_labels:
+        assert all(
+            example.keys() == {"audio", "label"} and all(val is not None for val in example.values())
+            for _, example in generator
+        )
+    else:
+        assert all(
+            example.keys() == {"audio"} and all(val is not None for val in example.values())
+            for _, example in generator
+        )
+
+
+@require_sndfile
+@pytest.mark.parametrize("drop_metadata", [True, False])
+def test_generate_examples_drop_metadata(audio_file_with_metadata, drop_metadata):
+    audio_file, audio_metadata_file = audio_file_with_metadata
+    if not drop_metadata:
+        features = Features({"audio": Audio(), "text": Value("string")})
+    else:
+        features = Features({"audio": Audio()})
+    audiofolder = AudioFolder(drop_metadata=drop_metadata, features=features)
+    generator = audiofolder._generate_examples(
+        [(audio_file, audio_file)], {"train": [(audio_metadata_file, audio_metadata_file)]}, "train"
+    )
+    if not drop_metadata:
+        assert all(
+            example.keys() == {"audio", "text"} and all(val is not None for val in example.values())
+            for _, example in generator
+        )
+    else:
+        assert all(
+            example.keys() == {"audio"} and all(val is not None for val in example.values())
+            for _, example in generator
+        )
+
+
+@require_sndfile
+@pytest.mark.parametrize("drop_metadata", [True, False])
+def test_generate_examples_with_metadata_in_wrong_location(audio_file, audio_file_with_metadata, drop_metadata):
+    _, audio_metadata_file = audio_file_with_metadata
+    if not drop_metadata:
+        features = Features({"audio": Audio(), "text": Value("string")})
+    else:
+        features = Features({"audio": Audio()})
+    audiofolder = AudioFolder(drop_metadata=drop_metadata, features=features)
+    generator = audiofolder._generate_examples(
+        [(audio_file, audio_file)], {"train": [(audio_metadata_file, audio_metadata_file)]}, "train"
+    )
+    if not drop_metadata:
+        with pytest.raises(ValueError):
+            list(generator)
+    else:
+        assert all(
+            example.keys() == {"audio"} and all(val is not None for val in example.values())
+            for _, example in generator
+        )
+
+
+@require_sndfile
+@pytest.mark.parametrize("drop_metadata", [True, False])
+def test_generate_examples_with_metadata_that_misses_one_audio(
+    audio_files_with_metadata_that_misses_one_audio, drop_metadata
+):
+    audio_file, audio_file2, audio_metadata_file = audio_files_with_metadata_that_misses_one_audio
+    if not drop_metadata:
+        features = Features({"audio": Audio(), "text": Value("string")})
+    else:
+        features = Features({"audio": Audio()})
+    audiofolder = AudioFolder(drop_metadata=drop_metadata, features=features)
+    generator = audiofolder._generate_examples(
+        [(audio_file, audio_file), (audio_file2, audio_file2)],
+        {"train": [(audio_metadata_file, audio_metadata_file)]},
+        "train",
+    )
+    if not drop_metadata:
+        with pytest.raises(ValueError):
+            _ = list(generator)
+    else:
+        assert all(
+            example.keys() == {"audio"} and all(val is not None for val in example.values())
+            for _, example in generator
+        )
+
+
+@require_sndfile
+@pytest.mark.parametrize("streaming", [False, True])
+@pytest.mark.parametrize("n_splits", [1, 2])
+def test_data_files_with_metadata_and_splits(
+    streaming, cache_dir, n_splits, data_files_with_one_split_and_metadata, data_files_with_two_splits_and_metadata
+):
+    data_files = data_files_with_one_split_and_metadata if n_splits == 1 else data_files_with_two_splits_and_metadata
+    audiofolder = AudioFolder(data_files=data_files, cache_dir=cache_dir)
+    audiofolder.download_and_prepare()
+    datasets = audiofolder.as_streaming_dataset() if streaming else audiofolder.as_dataset()
+    for split, data_files in data_files.items():
+        expected_num_of_audios = len(data_files) - 1  # don't count the metadata file
+        assert split in datasets
+        dataset = list(datasets[split])
+        assert len(dataset) == expected_num_of_audios
+        # make sure each sample has its own audio and metadata
+        assert len(set(example["audio"]["path"] for example in dataset)) == expected_num_of_audios
+        assert len(set(example["text"] for example in dataset)) == expected_num_of_audios
+        assert all(example["text"] is not None for example in dataset)
+
+
+@require_sndfile
+@pytest.mark.parametrize("streaming", [False, True])
+def test_data_files_with_metadata_and_archives(streaming, cache_dir, data_files_with_zip_archives):
+    if streaming:
+        extend_module_for_streaming(AudioFolder.__module__)
+    audiofolder = AudioFolder(data_files=data_files_with_zip_archives, cache_dir=cache_dir)
+    audiofolder.download_and_prepare()
+    datasets = audiofolder.as_streaming_dataset() if streaming else audiofolder.as_dataset()
+    for split, data_files in data_files_with_zip_archives.items():
+        num_of_archives = len(data_files)  # the metadata file is inside the archive
+        expected_num_of_audios = 2 * num_of_archives
+        assert split in datasets
+        dataset = list(datasets[split])
+        assert len(dataset) == expected_num_of_audios
+        # make sure each sample has its own audio (all arrays are different) and metadata
+        assert (
+            sum(np.array_equal(dataset[0]["audio"]["array"], example["audio"]["array"]) for example in dataset[1:])
+            == 0
+        )
+        assert len(set(example["text"] for example in dataset)) == expected_num_of_audios
+        assert all(example["text"] is not None for example in dataset)
+
+
+@require_sndfile
+def test_data_files_with_wrong_metadata_file_name(cache_dir, tmp_path, audio_file):
+    data_dir = tmp_path / "data_dir_with_bad_metadata"
+    data_dir.mkdir(parents=True, exist_ok=True)
+    shutil.copyfile(audio_file, data_dir / "audio_file.wav")
+    audio_metadata_filename = data_dir / "bad_metadata.jsonl"  # bad file
+    audio_metadata = textwrap.dedent(
+        """\
+        {"file_name": "audio_file.wav", "text": "Audio transcription"}
+        """
+    )
+    with open(audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+
+    data_files_with_bad_metadata = DataFilesDict.from_local_or_remote(
+        get_data_patterns_locally(str(data_dir)), str(data_dir)
+    )
+    audiofolder = AudioFolder(data_files=data_files_with_bad_metadata, cache_dir=cache_dir)
+    audiofolder.download_and_prepare()
+    dataset = audiofolder.as_dataset(split="train")
+    # check that there are no metadata, since the metadata file name doesn't have the right name
+    assert "text" not in dataset.column_names
+
+
+@require_sndfile
+def test_data_files_with_wrong_audio_file_name_column_in_metadata_file(cache_dir, tmp_path, audio_file):
+    data_dir = tmp_path / "data_dir_with_bad_metadata"
+    data_dir.mkdir(parents=True, exist_ok=True)
+    shutil.copyfile(audio_file, data_dir / "audio_file.wav")
+    audio_metadata_filename = data_dir / "metadata.jsonl"
+    audio_metadata = textwrap.dedent(  # with bad column "bad_file_name" instead of "file_name"
+        """\
+        {"bad_file_name_column": "audio_file.wav", "text": "Audio transcription"}
+        """
+    )
+    with open(audio_metadata_filename, "w", encoding="utf-8") as f:
+        f.write(audio_metadata)
+
+    data_files_with_bad_metadata = DataFilesDict.from_local_or_remote(
+        get_data_patterns_locally(str(data_dir)), str(data_dir)
+    )
+    audiofolder = AudioFolder(data_files=data_files_with_bad_metadata, cache_dir=cache_dir)
+    with pytest.raises(ValueError) as exc_info:
+        audiofolder.download_and_prepare()
+    assert "`file_name` must be present" in str(exc_info.value)
diff --git a/tests/test_dataset_common.py b/tests/test_dataset_common.py
index bcb5425020f..f75aac75686 100644
--- a/tests/test_dataset_common.py
+++ b/tests/test_dataset_common.py
@@ -81,6 +81,7 @@ def get_packaged_dataset_dummy_data_files(dataset_name, path_to_dummy_data):
         "csv": ".csv",
         "parquet": ".parquet",
         "imagefolder": "/",
+        "audiofolder": "/",
     }
     return {
         "train": os.path.join(path_to_dummy_data, "train" + extensions[dataset_name]),


Code review comments:
diff_hunk: @@ -112,3 +112,9 @@ def extend_dataset_builder_for_streaming(builder: "DatasetBuilder"):
                 internal_import_name = imports[1]
                 internal_module_name = ".".join(builder.__module__.split(".")[:-1] + [internal_import_name])
                 extend_module_for_streaming(internal_module_name, use_auth_token=builder.use_auth_token)
+
+    # we need to patch autofolder.py manually because it's a parent of Image and Audio folder and it
+    # contains generation of examples (which uses `os` functions)
+    autofolder_module = [cls.__module__ for cls in type(builder).__mro__ if cls.__name__ == "AutoFolder"]
path: src/datasets/streaming.py
text: @user1:
Just in case someone subclasses a class named "AutoFolder" that is not the one in `datasets`:
```suggestion
    # contains generation of examples (which uses `os` functions)
    from .packaged_modules.base.autofolder import AutoFolder

    autofolder_module = [cls.__module__ for cls in type(builder).__mro__ if cls is AutoFolder
```