diff --git a/docs/source/_static/sample_audio/sample_220925_223450.wav b/docs/source/_static/sample_audio/id/1829.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223450.wav rename to docs/source/_static/sample_audio/id/1829.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223500.wav b/docs/source/_static/sample_audio/id/1830.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223500.wav rename to docs/source/_static/sample_audio/id/1830.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223520.wav b/docs/source/_static/sample_audio/id/1832.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223520.wav rename to docs/source/_static/sample_audio/id/1832.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223530.wav b/docs/source/_static/sample_audio/id/1833.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223530.wav rename to docs/source/_static/sample_audio/id/1833.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223600.wav b/docs/source/_static/sample_audio/id/1834.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223600.wav rename to docs/source/_static/sample_audio/id/1834.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223610.wav b/docs/source/_static/sample_audio/id/1835.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223610.wav rename to docs/source/_static/sample_audio/id/1835.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223620.wav b/docs/source/_static/sample_audio/id/1836.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223620.wav rename to docs/source/_static/sample_audio/id/1836.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223630.wav b/docs/source/_static/sample_audio/id/1837.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223630.wav rename to docs/source/_static/sample_audio/id/1837.wav diff --git a/docs/source/_static/sample_audio/sample_220925_223640.wav b/docs/source/_static/sample_audio/id/1838.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223640.wav rename to docs/source/_static/sample_audio/id/1838.wav diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223450.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223450.wav new file mode 100644 index 00000000..61747c37 Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223450.wav differ diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223500.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223500.wav new file mode 100644 index 00000000..71c8dab2 Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223500.wav differ diff --git a/docs/source/_static/sample_audio/sample_220925_223510.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223510.wav similarity index 100% rename from docs/source/_static/sample_audio/sample_220925_223510.wav rename to docs/source/_static/sample_audio/timestamped/sample_220925_223510.wav diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223520.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223520.wav new file mode 100644 index 00000000..8eca20cb Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223520.wav differ diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223530.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223530.wav new file mode 100644 index 00000000..1c6557de Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223530.wav differ diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223600.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223600.wav new file mode 100644 index 00000000..61fe9862 Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223600.wav differ diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223610.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223610.wav new file mode 100644 index 00000000..eb6e9c4e Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223610.wav differ diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223620.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223620.wav new file mode 100644 index 00000000..c7c20915 Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223620.wav differ diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223630.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223630.wav new file mode 100644 index 00000000..730be4c4 Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223630.wav differ diff --git a/docs/source/_static/sample_audio/timestamped/sample_220925_223640.wav b/docs/source/_static/sample_audio/timestamped/sample_220925_223640.wav new file mode 100644 index 00000000..3dc9965a Binary files /dev/null and b/docs/source/_static/sample_audio/timestamped/sample_220925_223640.wav differ diff --git a/docs/source/coreapi_usage.rst b/docs/source/coreapi_usage.rst index 20f36d19..de425b50 100644 --- a/docs/source/coreapi_usage.rst +++ b/docs/source/coreapi_usage.rst @@ -246,6 +246,38 @@ This is the default behaviour, but other ways of computing the ``AudioData`` tim You don't have to worry about the shape of the original audio files: audio data will be fetched seamlessly in the corresponding file(s) whenever you need it. +Non-timestamped audio files +""""""""""""""""""""""""""" + +In case you don't know the timestamps at which your audio files were recorded (or you don't care specifying them), you can specify +a default timestamp at which the first valid audio file in the folder will be considered to start thanks to the +``first_file_begin`` parameter. + +Each next valid audio file will be considered to start immediately after the end of the previous one. + +.. code-block:: python + + from pathlib import Path + from osekit.core_api.audio_dataset import AudioDataset + from osekit.core_api.instrument import Instrument + from pandas import Timestamp, Timedelta + + folder = Path(r"...") + ads = AudioDataset.from_folder + ( + folder=folder, + strptime_format=None # Will use first_file_begin to timestamp the files + first_file_begin=Timestamp("2009-01-06 10:00:00"), + begin=Timestamp("2009-01-06 12:00:00"), # We can still specify the begin/end timestamps of the required dataset + end=Timestamp("2009-01-06 14:00:00"), + data_duration=Timedelta("10s"), + instrument=Instrument(end_to_end_db=150), + normalization="dc_reject" + ) + +In the example above, the first valid file in the folder will be considered to start at ``2009-01-06 10:00:00``. +If this first file is 1 hour-long, the next one will be considered to start at ``2009-01-06 11:00:00``, and so on. + Manipulation """""""""""" diff --git a/docs/source/example_ltas_core.ipynb b/docs/source/example_ltas_core.ipynb index a963ec87..9a7328f2 100644 --- a/docs/source/example_ltas_core.ipynb +++ b/docs/source/example_ltas_core.ipynb @@ -48,7 +48,7 @@ "source": [ "from pathlib import Path\n", "\n", - "audio_folder = Path(r\"_static/sample_audio\")\n", + "audio_folder = Path(r\"_static/sample_audio/timestamped\")\n", "\n", "from osekit.core_api.audio_dataset import AudioDataset\n", "from osekit.utils.audio_utils import Normalization\n", diff --git a/docs/source/example_ltas_public.ipynb b/docs/source/example_ltas_public.ipynb index dce99f89..c54c3da5 100644 --- a/docs/source/example_ltas_public.ipynb +++ b/docs/source/example_ltas_public.ipynb @@ -55,7 +55,7 @@ "source": [ "from pathlib import Path\n", "\n", - "audio_folder = Path(r\"_static/sample_audio\")\n", + "audio_folder = Path(r\"_static/sample_audio/timestamped\")\n", "\n", "from osekit.public_api.dataset import Dataset\n", "from osekit.core_api.instrument import Instrument\n", diff --git a/docs/source/example_multiple_spectrograms.rst b/docs/source/example_multiple_spectrograms.rst index e5919de4..2c0ae03d 100644 --- a/docs/source/example_multiple_spectrograms.rst +++ b/docs/source/example_multiple_spectrograms.rst @@ -1,5 +1,5 @@ -Computing multiple spectrograms -=============================== +Computing multiple spectrograms (timestamped files) +=================================================== .. _example_multiple_spectrograms: diff --git a/docs/source/example_multiple_spectrograms_core.ipynb b/docs/source/example_multiple_spectrograms_core.ipynb index 8e9498fc..25e28005 100644 --- a/docs/source/example_multiple_spectrograms_core.ipynb +++ b/docs/source/example_multiple_spectrograms_core.ipynb @@ -48,7 +48,7 @@ "source": [ "from pathlib import Path\n", "\n", - "audio_folder = Path(r\"_static/sample_audio\")\n", + "audio_folder = Path(r\"_static/sample_audio/timestamped\")\n", "\n", "from osekit.core_api.audio_dataset import AudioDataset\n", "from osekit.core_api.instrument import Instrument\n", diff --git a/docs/source/example_multiple_spectrograms_id.rst b/docs/source/example_multiple_spectrograms_id.rst new file mode 100644 index 00000000..9804819b --- /dev/null +++ b/docs/source/example_multiple_spectrograms_id.rst @@ -0,0 +1,20 @@ +Computing multiple spectrograms (ID files) +========================================== + +.. _example_multiple_spectrograms_id: + +In this example, we want to export spectrograms drawn from the sample audio dataset with the following requirements: + +* Start timestamps of the audio files are unknown +* One single ``8 s``-long spectrogram should be exported per audio file +* Audio data are downsampled sampled at ``24 kHz`` before spectrograms are computed +* The DC component of the audio data is rejected before spectrograms are computed +* Exported spectrogram images should be named after the audio file IDs + +The FFT used for computing the spectrograms will use a ``1024 samples``-long hamming window, with a ``128 samples``-long hop. + +.. toctree:: + :maxdepth: 1 + + example_multiple_spectrograms_id_core + example_multiple_spectrograms_id_public diff --git a/docs/source/example_multiple_spectrograms_id_core.ipynb b/docs/source/example_multiple_spectrograms_id_core.ipynb new file mode 100644 index 00000000..f843ac61 --- /dev/null +++ b/docs/source/example_multiple_spectrograms_id_core.ipynb @@ -0,0 +1,257 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "dc7ebca70b3b5da", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# Executing this cell will disable all TQDM outputs in stdout.\n", + "import os\n", + "\n", + "os.environ[\"DISABLE_TQDM\"] = \"True\"" + ] + }, + { + "cell_type": "markdown", + "id": "5d67c1697ae837a4", + "metadata": {}, + "source": [ + "# Computing multiple spectrograms with the Core API [^download]\n", + "\n", + "[^download]: This notebook can be downloaded as **{nb-download}`example_multiple_spectrograms_id_core.ipynb`**." + ] + }, + { + "cell_type": "markdown", + "id": "66731c408df9271c", + "metadata": {}, + "source": [ + "Create an **OSEkit** `AudioDataset` from the files on disk, by directly specifying the requirements in the constructor.\n", + "\n", + "Since we don't know (nor we care about) the files begin timestamps, we'll set the `striptime_format` to `None`, which will assign a default timestamp at which the first valid audio file will be considered to start. Then, each next valid audio file will be considered as starting at the end of the previous one. This default timestamp can be editted thanks to the `first_file_begin` parameter.\n", + "\n", + "An `Instrument` can be provided to the `AudioDataset` for the audio data to be converted in pressure units. This will lead the resulting spectra to be expressed in dB SPL (rather than in dB FS):\n", + "\n", + "We will only use the **folder** in which the files are located: we don't have to dig up to the file level." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f36ef512e49e57b", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "audio_folder = Path(r\"_static/sample_audio/id\")\n", + "\n", + "from osekit.core_api.audio_dataset import AudioDataset\n", + "from osekit.core_api.instrument import Instrument\n", + "from osekit.utils.audio_utils import Normalization\n", + "\n", + "audio_dataset = AudioDataset.from_folder(\n", + " folder=audio_folder,\n", + " strptime_format=None,\n", + " mode=\"files\", # Will create one audio data per file. We'll trim them later\n", + " instrument=Instrument(end_to_end_db=165.0),\n", + " sample_rate=24_000,\n", + " normalization=Normalization.DC_REJECT,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "ae4f34cb1e7707e5", + "metadata": {}, + "source": "The `AudioDataset` object contains all the to-be-exported `AudioData`:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b81ebdba07f5ccf", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"{' AUDIO DATASET ':#^60}\")\n", + "print(f\"{'Begin:':<30}{str(audio_dataset.begin):>30}\")\n", + "print(f\"{'End:':<30}{str(audio_dataset.end):>30}\")\n", + "print(f\"{'Sample rate:':<30}{str(audio_dataset.sample_rate):>30}\")\n", + "print(f\"{'Nb of audio data:':<30}{str(len(audio_dataset.data)):>30}\")" + ] + }, + { + "cell_type": "markdown", + "id": "eeafddd2f84557ac", + "metadata": {}, + "source": "We want to trim these audio data so that they all last `8 s`:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce629441ea840976", + "metadata": {}, + "outputs": [], + "source": [ + "from pandas import Timedelta\n", + "\n", + "for ad in audio_dataset.data:\n", + " ad.end = ad.begin + Timedelta(seconds=8)" + ] + }, + { + "cell_type": "markdown", + "id": "ff463689d799a823", + "metadata": {}, + "source": "Now, we instantiate a `scipy.signal.ShortTimeFFT` FFT object with the required parameters:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5c030c6cf06128", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from scipy.signal import ShortTimeFFT\n", + "from scipy.signal.windows import hamming\n", + "\n", + "sft = ShortTimeFFT(\n", + " win=hamming(1024),\n", + " hop=128,\n", + " fs=audio_dataset.sample_rate,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "946ef52ed70d0642", + "metadata": {}, + "source": [ + "Create an **OSEkit** `SpectroDataset` from the `AudioDataset` and the `ShortTimeFFT` objects.\n", + "\n", + "We'll also rename these spectro data so that the exported files are named after the original audio file names (that are the IDs)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7397ac200756cd82", + "metadata": {}, + "outputs": [], + "source": [ + "from osekit.core_api.spectro_dataset import SpectroDataset\n", + "\n", + "spectro_dataset = SpectroDataset.from_audio_dataset(\n", + " audio_dataset=audio_dataset,\n", + " fft=sft,\n", + " v_lim=(0.0, 150.0), # Boundaries of the spectrograms\n", + " colormap=\"viridis\", # Default value\n", + ")\n", + "\n", + "# RENAMING\n", + "for sd in spectro_dataset.data:\n", + " sd.name = next(iter(sd.audio_data.files)).path.stem" + ] + }, + { + "cell_type": "markdown", + "id": "b3fd3bd205bc369c", + "metadata": {}, + "source": "We can plot sample `SpectroData` object(s) if we want to glance at the output before computing all spectrograms (notice the 25% overlap as specified):" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e42dfe3608c8b59e", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axs = plt.subplots(2, 1)\n", + "\n", + "spectro_dataset.data[0].plot(ax=axs[1])\n", + "spectro_dataset.data[1].plot(ax=axs[0])\n", + "axs[0].get_xaxis().set_visible(False)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "cb64ef0bb8d69218", + "metadata": {}, + "source": "We are now ready to export the spectrograms and matrices:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c28a40fe9093dfeb", + "metadata": { + "tags": [ + "skip-execution" + ] + }, + "outputs": [], + "source": [ + "# Export all spectrograms\n", + "spectro_dataset.save_spectrogram(folder=audio_folder / \"spectrograms\")\n", + "\n", + "# Export all NPZ matrices\n", + "spectro_dataset.write(folder=audio_folder / \"matrices\")" + ] + }, + { + "cell_type": "markdown", + "id": "b6d50da7dfb1dffc", + "metadata": {}, + "source": [ + "## PSD estimates\n", + "\n", + "We can also export Power Spectral Density estimates using the welch method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4829920bbacee0b2", + "metadata": { + "tags": [ + "skip-execution" + ] + }, + "outputs": [], + "source": [ + "spectro_dataset.write_welch(folder=audio_folder / \"welch\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/example_multiple_spectrograms_id_public.ipynb b/docs/source/example_multiple_spectrograms_id_public.ipynb new file mode 100644 index 00000000..8e3e6d0f --- /dev/null +++ b/docs/source/example_multiple_spectrograms_id_public.ipynb @@ -0,0 +1,289 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "dc7ebca70b3b5da", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# Executing this cell will:\n", + "\n", + "# Disable all TQDM outputs in stdout.\n", + "import os\n", + "\n", + "os.environ[\"DISABLE_TQDM\"] = \"True\"\n", + "\n", + "# Setup the python logger for the Public API\n", + "from osekit import setup_logging\n", + "\n", + "setup_logging() # Overwrites the default logger to" + ] + }, + { + "cell_type": "markdown", + "id": "5d67c1697ae837a4", + "metadata": {}, + "source": [ + "# Computing multiple spectrograms with the Public API [^download]\n", + "\n", + "[^download]: This notebook can be downloaded as **{nb-download}`example_multiple_spectrograms_public.ipynb`**." + ] + }, + { + "cell_type": "markdown", + "id": "66731c408df9271c", + "metadata": {}, + "source": [ + "As always in the **Public API**, the first step is to **build the dataset**.\n", + "\n", + "Since we don't know (nor we care about) the files begin timestamps, we'll set the `striptime_format` to `None`, which will assign a default timestamp at which the first valid audio file will be considered to start. Then, each next valid audio file will be considered as starting at the end of the previous one. This default timestamp can be editted thanks to the `first_file_begin` parameter.\n", + "\n", + "An `Instrument` can be provided to the `Dataset` for the WAV data to be converted in pressure units. This will lead the resulting spectra to be expressed in dB SPL (rather than in dB FS)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb002105fc9632e8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "audio_folder = Path(r\"_static/sample_audio/id\")\n", + "\n", + "from osekit.public_api.dataset import Dataset\n", + "from osekit.core_api.instrument import Instrument\n", + "\n", + "dataset = Dataset(\n", + " folder=audio_folder,\n", + " strptime_format=None,\n", + " instrument=Instrument(end_to_end_db=165.0),\n", + ")\n", + "\n", + "dataset.build()" + ] + }, + { + "cell_type": "markdown", + "id": "d11d0e55baa8e44", + "metadata": {}, + "source": "The **Public API** `Dataset` is now analyzed and organized:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a29c761d4bbd5303", + "metadata": {}, + "outputs": [], + "source": [ + "print(f\"{' DATASET ':#^60}\")\n", + "print(f\"{'Begin:':<30}{str(dataset.origin_dataset.begin):>30}\")\n", + "print(f\"{'End:':<30}{str(dataset.origin_dataset.end):>30}\")\n", + "print(f\"{'Sample rate:':<30}{str(dataset.origin_dataset.sample_rate):>30}\\n\")\n", + "\n", + "print(f\"{' ORIGINAL FILES ':#^60}\")\n", + "import pandas as pd\n", + "\n", + "pd.DataFrame(\n", + " [\n", + " {\n", + " \"Name\": f.path.name,\n", + " \"Begin\": f.begin,\n", + " \"End\": f.end,\n", + " \"Sample Rate\": f.sample_rate,\n", + " }\n", + " for f in dataset.origin_files\n", + " ],\n", + ").set_index(\"Name\")" + ] + }, + { + "cell_type": "markdown", + "id": "9e669f8e6e50909d", + "metadata": {}, + "source": "Since we will run a spectral analysis, we need to define the FFT parameters:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6f26555c4e80ab3", + "metadata": {}, + "outputs": [], + "source": [ + "from scipy.signal import ShortTimeFFT\n", + "from scipy.signal.windows import hamming\n", + "\n", + "sample_rate = 24_000\n", + "\n", + "sft = ShortTimeFFT(win=hamming(1024), hop=128, fs=sample_rate)" + ] + }, + { + "cell_type": "markdown", + "id": "bdcd50704ad9f826", + "metadata": {}, + "source": "To **run analyses** in the **Public API**, use the `Analysis` class:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b4c2c3857ffcb60f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "from osekit.public_api.analysis import Analysis, AnalysisType\n", + "from osekit.utils.audio_utils import Normalization\n", + "\n", + "analysis = Analysis(\n", + " AnalysisType.SPECTROGRAM,\n", + " mode=\"files\", # We want one spectrogram per file\n", + " sample_rate=sample_rate,\n", + " normalization=Normalization.DC_REJECT,\n", + " fft=sft,\n", + " v_lim=(0.0, 150.0), # Boundaries of the spectrograms\n", + " colormap=\"viridis\", # Default value\n", + " name=\"8s_long_spectros\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "51c793d194a72485", + "metadata": {}, + "source": [ + "The **Core API** can still be used on top of the **Public API**.\n", + "\n", + "We'll access the Core API `SpectroDataset` that match this analysis to trim and rename the exported spectrograms:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7de8aed3d1c1328a", + "metadata": {}, + "outputs": [], + "source": [ + "from pandas import Timedelta\n", + "\n", + "spectro_dataset = dataset.get_analysis_spectrodataset(analysis)\n", + "\n", + "for sd in spectro_dataset.data:\n", + " sd.name = next(iter(sd.audio_data.files)).path.stem\n", + " sd.end = sd.begin + Timedelta(seconds=8)" + ] + }, + { + "cell_type": "markdown", + "id": "89a968c78117ceea", + "metadata": {}, + "source": "We can also glance at the spectrogram results with the **Core API**:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0f78e20ffe0a9e2", + "metadata": {}, + "outputs": [], + "source": [ + "import matplotlib.pyplot as plt\n", + "\n", + "fig, axs = plt.subplots(2, 1)\n", + "spectro_dataset.data[0].plot(ax=axs[1])\n", + "spectro_dataset.data[1].plot(ax=axs[0])\n", + "axs[0].get_xaxis().set_visible(False)\n", + "\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b2950dc5d76d2bbe", + "metadata": {}, + "source": "Running the analysis while specifying the filtered ``audio_dataset`` will skip the empty `AudioData` (and thus the empty `SpectroData`)." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b65cfdc720d50e6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "dataset.run_analysis(analysis=analysis, spectro_dataset=spectro_dataset)" + ] + }, + { + "cell_type": "markdown", + "id": "8271f118422f38dc", + "metadata": {}, + "source": "All the new files from the analysis are stored in a `SpectroDataset` named after `analysis.name`:" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3cb0adbb96d2251a", + "metadata": {}, + "outputs": [], + "source": [ + "pd.DataFrame(\n", + " [\n", + " {\n", + " \"Exported file\": path.name,\n", + " }\n", + " for path in (\n", + " audio_folder / \"processed\" / analysis.name / \"spectrogram\"\n", + " ).iterdir()\n", + " ],\n", + ").set_index(\"Exported file\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58b7aec2d8863a02", + "metadata": { + "tags": [ + "remove-cell" + ] + }, + "outputs": [], + "source": [ + "# Reset the dataset to get all files back to place.\n", + "\n", + "dataset.reset()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/example_multiple_spectrograms_public.ipynb b/docs/source/example_multiple_spectrograms_public.ipynb index 7e5d464f..47a3477d 100644 --- a/docs/source/example_multiple_spectrograms_public.ipynb +++ b/docs/source/example_multiple_spectrograms_public.ipynb @@ -80,24 +80,24 @@ "evalue": "[WinError 32] Le processus ne peut pas accéder au fichier car ce fichier est utilisé par un autre processus: '_static\\\\sample_audio\\\\sample_220925_223530.wav' -> '_static\\\\sample_audio\\\\data\\\\audio\\\\original\\\\sample_220925_223530.wav'", "output_type": "error", "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mPermissionError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[2]\u001b[39m\u001b[32m, line 14\u001b[39m\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mosekit\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mcore_api\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01minstrument\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m Instrument\n\u001b[32m 8\u001b[39m dataset = Dataset(\n\u001b[32m 9\u001b[39m folder=audio_folder,\n\u001b[32m 10\u001b[39m strptime_format=\u001b[33m\"\u001b[39m\u001b[33m%\u001b[39m\u001b[33my\u001b[39m\u001b[33m%\u001b[39m\u001b[33mm\u001b[39m\u001b[38;5;132;01m%d\u001b[39;00m\u001b[33m_\u001b[39m\u001b[33m%\u001b[39m\u001b[33mH\u001b[39m\u001b[33m%\u001b[39m\u001b[33mM\u001b[39m\u001b[33m%\u001b[39m\u001b[33mS\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m 11\u001b[39m instrument=Instrument(end_to_end_db=\u001b[32m150.0\u001b[39m),\n\u001b[32m 12\u001b[39m )\n\u001b[32m---> \u001b[39m\u001b[32m14\u001b[39m \u001b[43mdataset\u001b[49m\u001b[43m.\u001b[49m\u001b[43mbuild\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\public_api\\dataset.py:156\u001b[39m, in \u001b[36mDataset.build\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 144\u001b[39m \u001b[38;5;28mself\u001b[39m.logger.info(\u001b[33m\"\u001b[39m\u001b[33mOrganizing dataset folder...\u001b[39m\u001b[33m\"\u001b[39m)\n\u001b[32m 145\u001b[39m move_tree(\n\u001b[32m 146\u001b[39m source=\u001b[38;5;28mself\u001b[39m.folder,\n\u001b[32m 147\u001b[39m destination=\u001b[38;5;28mself\u001b[39m.folder / \u001b[33m\"\u001b[39m\u001b[33mother\u001b[39m\u001b[33m\"\u001b[39m,\n\u001b[32m (...)\u001b[39m\u001b[32m 154\u001b[39m | {\u001b[38;5;28mself\u001b[39m.folder / \u001b[33m\"\u001b[39m\u001b[33mlog\u001b[39m\u001b[33m\"\u001b[39m},\n\u001b[32m 155\u001b[39m )\n\u001b[32m--> \u001b[39m\u001b[32m156\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_sort_dataset\u001b[49m\u001b[43m(\u001b[49m\u001b[43mads\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 157\u001b[39m ads.write_json(ads.folder)\n\u001b[32m 158\u001b[39m \u001b[38;5;28mself\u001b[39m.write_json()\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\public_api\\dataset.py:513\u001b[39m, in \u001b[36mDataset._sort_dataset\u001b[39m\u001b[34m(self, dataset)\u001b[39m\n\u001b[32m 511\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_sort_dataset\u001b[39m(\u001b[38;5;28mself\u001b[39m, dataset: \u001b[38;5;28mtype\u001b[39m[DatasetChild]) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 512\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mtype\u001b[39m(dataset) \u001b[38;5;129;01mis\u001b[39;00m AudioDataset:\n\u001b[32m--> \u001b[39m\u001b[32m513\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_sort_audio_dataset\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdataset\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 514\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m\n\u001b[32m 515\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mtype\u001b[39m(dataset) \u001b[38;5;129;01mis\u001b[39;00m SpectroDataset | LTASDataset:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\public_api\\dataset.py:520\u001b[39m, in \u001b[36mDataset._sort_audio_dataset\u001b[39m\u001b[34m(self, dataset)\u001b[39m\n\u001b[32m 519\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_sort_audio_dataset\u001b[39m(\u001b[38;5;28mself\u001b[39m, dataset: AudioDataset) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m520\u001b[39m \u001b[43mdataset\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmove_files\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_get_audio_dataset_subpath\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdataset\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\core_api\\base_dataset.py:152\u001b[39m, in \u001b[36mBaseDataset.move_files\u001b[39m\u001b[34m(self, folder)\u001b[39m\n\u001b[32m 143\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Move the dataset files to the destination folder.\u001b[39;00m\n\u001b[32m 144\u001b[39m \n\u001b[32m 145\u001b[39m \u001b[33;03mParameters\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 149\u001b[39m \n\u001b[32m 150\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 151\u001b[39m \u001b[38;5;28;01mfor\u001b[39;00m file \u001b[38;5;129;01min\u001b[39;00m tqdm(\u001b[38;5;28mself\u001b[39m.files, disable=os.environ.get(\u001b[33m\"\u001b[39m\u001b[33mDISABLE_TQDM\u001b[39m\u001b[33m\"\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33m\"\u001b[39m)):\n\u001b[32m--> \u001b[39m\u001b[32m152\u001b[39m \u001b[43mfile\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmove\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfolder\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 153\u001b[39m \u001b[38;5;28mself\u001b[39m._folder = folder\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\core_api\\audio_file.py:128\u001b[39m, in \u001b[36mAudioFile.move\u001b[39m\u001b[34m(self, folder)\u001b[39m\n\u001b[32m 119\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Move the file to the target folder.\u001b[39;00m\n\u001b[32m 120\u001b[39m \n\u001b[32m 121\u001b[39m \u001b[33;03mParameters\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 125\u001b[39m \n\u001b[32m 126\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 127\u001b[39m afm.close()\n\u001b[32m--> \u001b[39m\u001b[32m128\u001b[39m \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmove\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfolder\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\core_api\\base_file.py:171\u001b[39m, in \u001b[36mBaseFile.move\u001b[39m\u001b[34m(self, folder)\u001b[39m\n\u001b[32m 169\u001b[39m destination_path = folder / \u001b[38;5;28mself\u001b[39m.path.name\n\u001b[32m 170\u001b[39m folder.mkdir(exist_ok=\u001b[38;5;28;01mTrue\u001b[39;00m, parents=\u001b[38;5;28;01mTrue\u001b[39;00m)\n\u001b[32m--> \u001b[39m\u001b[32m171\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrename\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdestination_path\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 172\u001b[39m \u001b[38;5;28mself\u001b[39m.path = destination_path\n", - "\u001b[36mFile \u001b[39m\u001b[32m~\\AppData\\Roaming\\uv\\python\\cpython-3.13.3-windows-x86_64-none\\Lib\\pathlib\\_local.py:767\u001b[39m, in \u001b[36mPath.rename\u001b[39m\u001b[34m(self, target)\u001b[39m\n\u001b[32m 757\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mrename\u001b[39m(\u001b[38;5;28mself\u001b[39m, target):\n\u001b[32m 758\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 759\u001b[39m \u001b[33;03m Rename this path to the target path.\u001b[39;00m\n\u001b[32m 760\u001b[39m \n\u001b[32m (...)\u001b[39m\u001b[32m 765\u001b[39m \u001b[33;03m Returns the new Path instance pointing to the target path.\u001b[39;00m\n\u001b[32m 766\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m767\u001b[39m \u001b[43mos\u001b[49m\u001b[43m.\u001b[49m\u001b[43mrename\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtarget\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 768\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.with_segments(target)\n", - "\u001b[31mPermissionError\u001b[39m: [WinError 32] Le processus ne peut pas accéder au fichier car ce fichier est utilisé par un autre processus: '_static\\\\sample_audio\\\\sample_220925_223530.wav' -> '_static\\\\sample_audio\\\\data\\\\audio\\\\original\\\\sample_220925_223530.wav'" + "\u001B[31m---------------------------------------------------------------------------\u001B[39m", + "\u001B[31mPermissionError\u001B[39m Traceback (most recent call last)", + "\u001B[36mCell\u001B[39m\u001B[36m \u001B[39m\u001B[32mIn[2]\u001B[39m\u001B[32m, line 14\u001B[39m\n\u001B[32m 6\u001B[39m \u001B[38;5;28;01mfrom\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34;01mosekit\u001B[39;00m\u001B[34;01m.\u001B[39;00m\u001B[34;01mcore_api\u001B[39;00m\u001B[34;01m.\u001B[39;00m\u001B[34;01minstrument\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[38;5;28;01mimport\u001B[39;00m Instrument\n\u001B[32m 8\u001B[39m dataset = Dataset(\n\u001B[32m 9\u001B[39m folder=audio_folder,\n\u001B[32m 10\u001B[39m strptime_format=\u001B[33m\"\u001B[39m\u001B[33m%\u001B[39m\u001B[33my\u001B[39m\u001B[33m%\u001B[39m\u001B[33mm\u001B[39m\u001B[38;5;132;01m%d\u001B[39;00m\u001B[33m_\u001B[39m\u001B[33m%\u001B[39m\u001B[33mH\u001B[39m\u001B[33m%\u001B[39m\u001B[33mM\u001B[39m\u001B[33m%\u001B[39m\u001B[33mS\u001B[39m\u001B[33m\"\u001B[39m,\n\u001B[32m 11\u001B[39m instrument=Instrument(end_to_end_db=\u001B[32m150.0\u001B[39m),\n\u001B[32m 12\u001B[39m )\n\u001B[32m---> \u001B[39m\u001B[32m14\u001B[39m \u001B[43mdataset\u001B[49m\u001B[43m.\u001B[49m\u001B[43mbuild\u001B[49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\public_api\\dataset.py:156\u001B[39m, in \u001B[36mDataset.build\u001B[39m\u001B[34m(self)\u001B[39m\n\u001B[32m 144\u001B[39m \u001B[38;5;28mself\u001B[39m.logger.info(\u001B[33m\"\u001B[39m\u001B[33mOrganizing dataset folder...\u001B[39m\u001B[33m\"\u001B[39m)\n\u001B[32m 145\u001B[39m move_tree(\n\u001B[32m 146\u001B[39m source=\u001B[38;5;28mself\u001B[39m.folder,\n\u001B[32m 147\u001B[39m destination=\u001B[38;5;28mself\u001B[39m.folder / \u001B[33m\"\u001B[39m\u001B[33mother\u001B[39m\u001B[33m\"\u001B[39m,\n\u001B[32m (...)\u001B[39m\u001B[32m 154\u001B[39m | {\u001B[38;5;28mself\u001B[39m.folder / \u001B[33m\"\u001B[39m\u001B[33mlog\u001B[39m\u001B[33m\"\u001B[39m},\n\u001B[32m 155\u001B[39m )\n\u001B[32m--> \u001B[39m\u001B[32m156\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_sort_dataset\u001B[49m\u001B[43m(\u001B[49m\u001B[43mads\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 157\u001B[39m ads.write_json(ads.folder)\n\u001B[32m 158\u001B[39m \u001B[38;5;28mself\u001B[39m.write_json()\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\public_api\\dataset.py:513\u001B[39m, in \u001B[36mDataset._sort_dataset\u001B[39m\u001B[34m(self, dataset)\u001B[39m\n\u001B[32m 511\u001B[39m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34m_sort_dataset\u001B[39m(\u001B[38;5;28mself\u001B[39m, dataset: \u001B[38;5;28mtype\u001B[39m[DatasetChild]) -> \u001B[38;5;28;01mNone\u001B[39;00m:\n\u001B[32m 512\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mtype\u001B[39m(dataset) \u001B[38;5;129;01mis\u001B[39;00m AudioDataset:\n\u001B[32m--> \u001B[39m\u001B[32m513\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_sort_audio_dataset\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdataset\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 514\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m\n\u001B[32m 515\u001B[39m \u001B[38;5;28;01mif\u001B[39;00m \u001B[38;5;28mtype\u001B[39m(dataset) \u001B[38;5;129;01mis\u001B[39;00m SpectroDataset | LTASDataset:\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\public_api\\dataset.py:520\u001B[39m, in \u001B[36mDataset._sort_audio_dataset\u001B[39m\u001B[34m(self, dataset)\u001B[39m\n\u001B[32m 519\u001B[39m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34m_sort_audio_dataset\u001B[39m(\u001B[38;5;28mself\u001B[39m, dataset: AudioDataset) -> \u001B[38;5;28;01mNone\u001B[39;00m:\n\u001B[32m--> \u001B[39m\u001B[32m520\u001B[39m \u001B[43mdataset\u001B[49m\u001B[43m.\u001B[49m\u001B[43mmove_files\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43m_get_audio_dataset_subpath\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdataset\u001B[49m\u001B[43m)\u001B[49m\u001B[43m)\u001B[49m\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\core_api\\base_dataset.py:152\u001B[39m, in \u001B[36mBaseDataset.move_files\u001B[39m\u001B[34m(self, folder)\u001B[39m\n\u001B[32m 143\u001B[39m \u001B[38;5;250m\u001B[39m\u001B[33;03m\"\"\"Move the dataset files to the destination folder.\u001B[39;00m\n\u001B[32m 144\u001B[39m \n\u001B[32m 145\u001B[39m \u001B[33;03mParameters\u001B[39;00m\n\u001B[32m (...)\u001B[39m\u001B[32m 149\u001B[39m \n\u001B[32m 150\u001B[39m \u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 151\u001B[39m \u001B[38;5;28;01mfor\u001B[39;00m file \u001B[38;5;129;01min\u001B[39;00m tqdm(\u001B[38;5;28mself\u001B[39m.files, disable=os.environ.get(\u001B[33m\"\u001B[39m\u001B[33mDISABLE_TQDM\u001B[39m\u001B[33m\"\u001B[39m, \u001B[33m\"\u001B[39m\u001B[33m\"\u001B[39m)):\n\u001B[32m--> \u001B[39m\u001B[32m152\u001B[39m \u001B[43mfile\u001B[49m\u001B[43m.\u001B[49m\u001B[43mmove\u001B[49m\u001B[43m(\u001B[49m\u001B[43mfolder\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 153\u001B[39m \u001B[38;5;28mself\u001B[39m._folder = folder\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\core_api\\audio_file.py:128\u001B[39m, in \u001B[36mAudioFile.move\u001B[39m\u001B[34m(self, folder)\u001B[39m\n\u001B[32m 119\u001B[39m \u001B[38;5;250m\u001B[39m\u001B[33;03m\"\"\"Move the file to the target folder.\u001B[39;00m\n\u001B[32m 120\u001B[39m \n\u001B[32m 121\u001B[39m \u001B[33;03mParameters\u001B[39;00m\n\u001B[32m (...)\u001B[39m\u001B[32m 125\u001B[39m \n\u001B[32m 126\u001B[39m \u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 127\u001B[39m afm.close()\n\u001B[32m--> \u001B[39m\u001B[32m128\u001B[39m \u001B[38;5;28;43msuper\u001B[39;49m\u001B[43m(\u001B[49m\u001B[43m)\u001B[49m\u001B[43m.\u001B[49m\u001B[43mmove\u001B[49m\u001B[43m(\u001B[49m\u001B[43mfolder\u001B[49m\u001B[43m)\u001B[49m\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\Documents\\GitHub\\OSEkit\\src\\osekit\\core_api\\base_file.py:171\u001B[39m, in \u001B[36mBaseFile.move\u001B[39m\u001B[34m(self, folder)\u001B[39m\n\u001B[32m 169\u001B[39m destination_path = folder / \u001B[38;5;28mself\u001B[39m.path.name\n\u001B[32m 170\u001B[39m folder.mkdir(exist_ok=\u001B[38;5;28;01mTrue\u001B[39;00m, parents=\u001B[38;5;28;01mTrue\u001B[39;00m)\n\u001B[32m--> \u001B[39m\u001B[32m171\u001B[39m \u001B[38;5;28;43mself\u001B[39;49m\u001B[43m.\u001B[49m\u001B[43mpath\u001B[49m\u001B[43m.\u001B[49m\u001B[43mrename\u001B[49m\u001B[43m(\u001B[49m\u001B[43mdestination_path\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 172\u001B[39m \u001B[38;5;28mself\u001B[39m.path = destination_path\n", + "\u001B[36mFile \u001B[39m\u001B[32m~\\AppData\\Roaming\\uv\\python\\cpython-3.13.3-windows-x86_64-none\\Lib\\pathlib\\_local.py:767\u001B[39m, in \u001B[36mPath.rename\u001B[39m\u001B[34m(self, target)\u001B[39m\n\u001B[32m 757\u001B[39m \u001B[38;5;28;01mdef\u001B[39;00m\u001B[38;5;250m \u001B[39m\u001B[34mrename\u001B[39m(\u001B[38;5;28mself\u001B[39m, target):\n\u001B[32m 758\u001B[39m \u001B[38;5;250m \u001B[39m\u001B[33;03m\"\"\"\u001B[39;00m\n\u001B[32m 759\u001B[39m \u001B[33;03m Rename this path to the target path.\u001B[39;00m\n\u001B[32m 760\u001B[39m \n\u001B[32m (...)\u001B[39m\u001B[32m 765\u001B[39m \u001B[33;03m Returns the new Path instance pointing to the target path.\u001B[39;00m\n\u001B[32m 766\u001B[39m \u001B[33;03m \"\"\"\u001B[39;00m\n\u001B[32m--> \u001B[39m\u001B[32m767\u001B[39m \u001B[43mos\u001B[49m\u001B[43m.\u001B[49m\u001B[43mrename\u001B[49m\u001B[43m(\u001B[49m\u001B[38;5;28;43mself\u001B[39;49m\u001B[43m,\u001B[49m\u001B[43m \u001B[49m\u001B[43mtarget\u001B[49m\u001B[43m)\u001B[49m\n\u001B[32m 768\u001B[39m \u001B[38;5;28;01mreturn\u001B[39;00m \u001B[38;5;28mself\u001B[39m.with_segments(target)\n", + "\u001B[31mPermissionError\u001B[39m: [WinError 32] Le processus ne peut pas accéder au fichier car ce fichier est utilisé par un autre processus: '_static\\\\sample_audio\\\\sample_220925_223530.wav' -> '_static\\\\sample_audio\\\\data\\\\audio\\\\original\\\\sample_220925_223530.wav'" ] } ], "source": [ "from pathlib import Path\n", "\n", - "audio_folder = Path(r\"_static/sample_audio\")\n", + "audio_folder = Path(r\"_static/sample_audio/timestamped\")\n", "\n", "from osekit.public_api.dataset import Dataset\n", "from osekit.core_api.instrument import Instrument\n", diff --git a/docs/source/example_reshaping_multiple_files_core.ipynb b/docs/source/example_reshaping_multiple_files_core.ipynb index 45d4b6f8..5d43c013 100644 --- a/docs/source/example_reshaping_multiple_files_core.ipynb +++ b/docs/source/example_reshaping_multiple_files_core.ipynb @@ -46,7 +46,7 @@ "source": [ "from pathlib import Path\n", "\n", - "audio_folder = Path(r\"_static/sample_audio\")\n", + "audio_folder = Path(r\"_static/sample_audio/timestamped\")\n", "\n", "from osekit.core_api.audio_dataset import AudioDataset\n", "from osekit.utils.audio_utils import Normalization\n", diff --git a/docs/source/example_reshaping_multiple_files_public.ipynb b/docs/source/example_reshaping_multiple_files_public.ipynb index 56cf04f4..caf14afd 100644 --- a/docs/source/example_reshaping_multiple_files_public.ipynb +++ b/docs/source/example_reshaping_multiple_files_public.ipynb @@ -51,7 +51,7 @@ "source": [ "from pathlib import Path\n", "\n", - "audio_folder = Path(r\"_static/sample_audio\")\n", + "audio_folder = Path(r\"_static/sample_audio/timestamped\")\n", "\n", "from osekit.public_api.dataset import Dataset\n", "\n", diff --git a/docs/source/example_reshaping_one_file.ipynb b/docs/source/example_reshaping_one_file.ipynb index 37dbd833..537f320b 100644 --- a/docs/source/example_reshaping_one_file.ipynb +++ b/docs/source/example_reshaping_one_file.ipynb @@ -35,7 +35,7 @@ "from osekit.core_api.audio_file import AudioFile\n", "\n", "audio_file = AudioFile(\n", - " path=Path(r\"_static/sample_audio/sample_220925_223450.wav\"),\n", + " path=Path(r\"_static/sample_audio/timestamped/sample_220925_223450.wav\"),\n", " strptime_format=\"%y%m%d_%H%M%S\",\n", ")" ] @@ -157,7 +157,9 @@ }, "outputs": [], "source": [ - "audio_data.write(Path(r\"../docs/source/_static/sample_audio/exported_files/\"))" + "audio_data.write(\n", + " Path(r\"../docs/source/_static/sample_audio/timestamped/exported_files/\")\n", + ")" ] } ], diff --git a/docs/source/example_spectrogram.ipynb b/docs/source/example_spectrogram.ipynb index 436d89fb..3d89e0c6 100644 --- a/docs/source/example_spectrogram.ipynb +++ b/docs/source/example_spectrogram.ipynb @@ -37,7 +37,7 @@ "from pathlib import Path\n", "from osekit.core_api.audio_file import AudioFile\n", "\n", - "audio_folder = Path(r\"_static/sample_audio\")\n", + "audio_folder = Path(r\"_static/sample_audio/timestamped\")\n", "audio_files = [\n", " AudioFile(\n", " path=p,\n", diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 6afb6607..350fd627 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -8,9 +8,15 @@ This section gathers **OSEkit** jupyter notebooks that complete typical tasks. Example tasks will be completed with both the :ref:`Public ` and :ref:`Core API ` (see the :ref:`usage ` section for more info about the differences between the two APIs). -The examples use a small set of audio files that can be found in the **OSEkit** repository, under ``docs/source/_static/sample_audio``. -This sample dataset is made of 10 ``10 s``-long audio files sampled at ``48 kHz``. The 5 first and 5 last audio files are consecutive -(there is no recording gap between them), but both groups of 5 consecutive files are spaced by a ``30 s``-long recording gap. +The examples use two small sets of audio files that can be found in the **OSEkit** repository, under ``docs/source/_static/sample_audio``. + +Both sample datasets are made of 10 ``10 s``-long audio files sampled at ``48 kHz``. + +In the ``docs/source/_static/sample_audio/timestamped`` folder, the start timestamp of the audio files is written +in the file names. The 5 first and 5 last audio files are consecutive (there is no recording gap between them), +but both groups of 5 consecutive files are spaced by a ``30 s``-long recording gap. + +In the ``docs/source/_static/sample_audio/timestamped`` folder, files are just named after a unique ID. =========== @@ -39,6 +45,12 @@ This sample dataset is made of 10 ``10 s``-long audio files sampled at ``48 kHz` =========== +.. topic:: :doc:`Compute/plot multiple spectrograms from non-timestamped audio files ` + + Same example as the previous one, but with files that are not timestamped (example: file names are IDs). + +=========== + .. topic:: :doc:`Compute/plot a LTAS ` Compute, plot and export a **L**\ ong-\ **T**\ erm **A**\ verage **S**\ pectrum (**LTAS**). @@ -50,4 +62,5 @@ This sample dataset is made of 10 ``10 s``-long audio files sampled at ``48 kHz` example_reshaping_multiple_files example_spectrogram example_multiple_spectrograms - example_ltas \ No newline at end of file + example_multiple_spectrograms_id + example_ltas diff --git a/docs/source/publicapi_usage.rst b/docs/source/publicapi_usage.rst index 71979cfd..39b96efc 100644 --- a/docs/source/publicapi_usage.rst +++ b/docs/source/publicapi_usage.rst @@ -43,6 +43,21 @@ The complete list of extra parameters is provided in the :class:`osekit.public_a strptime_format="%y%m%d%H%M%S" # Must match the strptime format of your audio files ) +If you don't know (or don't care to parse) the start timestamps of your audio files, you can specify the ``first_file_begin`` parameter. +Then, the first valid audio file found in the folder will be considered as starting at this timestamp, +and each next valid audio file will be considered as starting directly after the end of the previous one: + +.. code-block:: python + + from osekit.public_api.dataset import Dataset + from pathlib import Path + + dataset = Dataset( + folder=Path(r"...\dataset_folder"), + strptime_format=None # Must match the strptime format of your audio files, + first_file_begin=Timestamp("2020-01-01 00:00:00") # Will mark the start of your audio files + ) + Once this is done, the ``Dataset`` can be built using the :meth:`osekit.public_api.dataset.Dataset.build` method. .. code-block:: python diff --git a/src/osekit/core_api/audio_data.py b/src/osekit/core_api/audio_data.py index 9fe1ac87..1dfa098e 100644 --- a/src/osekit/core_api/audio_data.py +++ b/src/osekit/core_api/audio_data.py @@ -13,9 +13,6 @@ import soundfile as sf from pandas import Timedelta, Timestamp -from osekit.config import ( - TIMESTAMP_FORMATS_EXPORTED_FILES, -) from osekit.core_api.audio_file import AudioFile from osekit.core_api.audio_item import AudioItem from osekit.core_api.base_data import BaseData @@ -78,10 +75,19 @@ def nb_channels(self) -> int: ) @property - def shape(self) -> tuple[int, ...] | int: - """Shape of the audio data.""" - data_length = round(self.sample_rate * self.duration.total_seconds()) - return data_length if self.nb_channels <= 1 else (data_length, self.nb_channels) + def shape(self) -> tuple[int, int]: + """Shape of the audio data. + + First element is the number of data point in each channel, + second element is the number of channels. + + """ + return self.length, self.nb_channels + + @property + def length(self) -> int: + """Number of data points in each channel.""" + return round(self.sample_rate * self.duration.total_seconds()) @property def normalization(self) -> Normalization: @@ -110,7 +116,7 @@ def normalization_values(self, value: dict | None) -> None: ) def get_normalization_values(self) -> dict: - values = self.get_raw_value() + values = np.array(self.get_raw_value()) return { "mean": values.mean(), "peak": values.max(), @@ -243,7 +249,7 @@ def link(self, folder: Path) -> None: """ file = AudioFile( path=folder / f"{self}.wav", - strptime_format=TIMESTAMP_FORMATS_EXPORTED_FILES, + begin=self.begin, ) self.items = AudioData.from_files([file]).items @@ -253,6 +259,7 @@ def _get_item_value(self, item: AudioItem) -> np.ndarray: if item.is_empty: return item_data.repeat( round(item.duration.total_seconds() * self.sample_rate), + axis=0, ) if item.sample_rate != self.sample_rate: return resample(item_data, item.sample_rate, self.sample_rate) @@ -329,7 +336,7 @@ def split_frames( """ if start_frame < 0: raise ValueError("Start_frame must be greater than or equal to 0.") - if stop_frame < -1 or stop_frame > self.shape: + if stop_frame < -1 or stop_frame > self.length: raise ValueError("Stop_frame must be lower than the length of the data.") start_timestamp = self.begin + Timedelta( diff --git a/src/osekit/core_api/audio_dataset.py b/src/osekit/core_api/audio_dataset.py index 1e9b92ba..398f38ed 100644 --- a/src/osekit/core_api/audio_dataset.py +++ b/src/osekit/core_api/audio_dataset.py @@ -170,7 +170,7 @@ def from_dict(cls, dictionary: dict) -> AudioDataset: def from_folder( # noqa: PLR0913 cls, folder: Path, - strptime_format: str, + strptime_format: str | None, begin: Timestamp | None = None, end: Timestamp | None = None, timezone: str | pytz.timezone | None = None, @@ -189,8 +189,12 @@ def from_folder( # noqa: PLR0913 ---------- folder: Path The folder containing the audio files. - strptime_format: str - The strptime format of the timestamps in the audio file names. + strptime_format: str | None + The strptime format used in the filenames. + It should use valid strftime codes (https://strftime.org/). + If None, the first audio file of the folder will start + at first_file_begin, and each following file will start + at the end of the previous one. begin: Timestamp | None The begin of the audio dataset. Defaulted to the begin of the first file. @@ -238,7 +242,10 @@ def from_folder( # noqa: PLR0913 """ kwargs.update( - {"file_class": AudioFile, "supported_file_extensions": [".wav", ".flac"]}, + { + "file_class": AudioFile, + "supported_file_extensions": [".wav", ".flac", ".mp3"], + }, ) base_dataset = BaseDataset.from_folder( folder=folder, diff --git a/src/osekit/core_api/audio_file.py b/src/osekit/core_api/audio_file.py index 2490c272..77508b11 100644 --- a/src/osekit/core_api/audio_file.py +++ b/src/osekit/core_api/audio_file.py @@ -2,11 +2,11 @@ from __future__ import annotations -from pathlib import Path from typing import TYPE_CHECKING if TYPE_CHECKING: from os import PathLike + from pathlib import Path import numpy as np import pytz @@ -85,7 +85,13 @@ def read(self, start: Timestamp, stop: Timestamp) -> np.ndarray: """ start_sample, stop_sample = self.frames_indexes(start, stop) - return afm.read(self.path, start=start_sample, stop=stop_sample) + data = afm.read(self.path, start=start_sample, stop=stop_sample) + if len(data.shape) == 1: + return data.reshape( + data.shape[0], + 1, + ) # 2D array to match the format of multichannel audio + return data def frames_indexes(self, start: Timestamp, stop: Timestamp) -> tuple[int, int]: """Return the indexes of the frames between the start and stop timestamps. diff --git a/src/osekit/core_api/audio_item.py b/src/osekit/core_api/audio_item.py index 57d8bcbf..170c28fd 100644 --- a/src/osekit/core_api/audio_item.py +++ b/src/osekit/core_api/audio_item.py @@ -4,12 +4,13 @@ from typing import TYPE_CHECKING +import numpy as np + from osekit.core_api.audio_file import AudioFile from osekit.core_api.base_file import BaseFile from osekit.core_api.base_item import BaseItem if TYPE_CHECKING: - import numpy as np from pandas import Timestamp @@ -46,19 +47,23 @@ def sample_rate(self) -> float: @property def nb_channels(self) -> int: """Number of channels in the associated AudioFile.""" - return 0 if self.is_empty else self.file.channels + return 1 if self.is_empty else self.file.channels @property - def shape(self) -> int: + def shape(self) -> tuple[int, int]: """Number of points in the audio item data.""" + if self.is_empty: + return 0, self.nb_channels start_sample, end_sample = self.file.frames_indexes(self.begin, self.end) - return end_sample - start_sample + return end_sample - start_sample, self.nb_channels def get_value(self) -> np.ndarray: """Get the values from the File between the begin and stop timestamps. If the Item is empty, return a single 0. """ + if self.is_empty: + return np.zeros((1, self.nb_channels)) return super().get_value() @classmethod diff --git a/src/osekit/core_api/base_data.py b/src/osekit/core_api/base_data.py index 4cde981d..c1067c9b 100644 --- a/src/osekit/core_api/base_data.py +++ b/src/osekit/core_api/base_data.py @@ -109,16 +109,16 @@ def begin(self, value: Timestamp) -> None: @property def end(self) -> Timestamp: + """Return the end timestamp of the data.""" + return max(item.end for item in self.items) + + @end.setter + def end(self, value: Timestamp) -> None: """Trim the end timestamp of the data. End can only be set to an anterior date from the original end. """ - return max(item.end for item in self.items) - - @end.setter - def end(self, value: Timestamp) -> None: - """Return true if every item of this data object is empty.""" self.items = [item for item in self.items if item.begin < value] for item in self.items: item.end = min(item.end, value) diff --git a/src/osekit/core_api/base_dataset.py b/src/osekit/core_api/base_dataset.py index 098fd468..222f6e62 100644 --- a/src/osekit/core_api/base_dataset.py +++ b/src/osekit/core_api/base_dataset.py @@ -431,7 +431,7 @@ def _get_base_data_from_files_timedelta_file( def from_folder( # noqa: PLR0913 cls, folder: Path, - strptime_format: str, + strptime_format: str | None, file_class: type[TFile] = BaseFile, supported_file_extensions: list[str] | None = None, begin: Timestamp | None = None, @@ -440,6 +440,7 @@ def from_folder( # noqa: PLR0913 mode: Literal["files", "timedelta_total", "timedelta_file"] = "timedelta_total", overlap: float = 0.0, data_duration: Timedelta | None = None, + first_file_begin: Timestamp | None = None, name: str | None = None, ) -> BaseDataset: """Return a BaseDataset from a folder containing the base files. @@ -448,8 +449,12 @@ def from_folder( # noqa: PLR0913 ---------- folder: Path The folder containing the files. - strptime_format: str - The strptime format of the timestamps in the file names. + strptime_format: str | None + The strptime format used in the filenames. + It should use valid strftime codes (https://strftime.org/). + If None, the first audio file of the folder will start + at first_file_begin, and each following file will start + at the end of the previous one. file_class: type[Tfile] Derived type of BaseFile used to instantiate the dataset. supported_file_extensions: list[str] @@ -482,6 +487,9 @@ def from_folder( # noqa: PLR0913 If mode is set to "files", this parameter has no effect. If provided, data will be evenly distributed between begin and end. Else, one object will cover the whole time period. + first_file_begin: Timestamp | None + Timestamp of the first audio file being processed. + Will be ignored if striptime_format is specified. name: str|None Name of the dataset. @@ -495,17 +503,23 @@ def from_folder( # noqa: PLR0913 supported_file_extensions = [] valid_files = [] rejected_files = [] + first_file_begin = first_file_begin or Timestamp("2020-01-01 00:00:00") for file in tqdm( - folder.iterdir(), + sorted(folder.iterdir()), disable=os.getenv("DISABLE_TQDM", "False").lower() in ("true", "1", "t"), ): - if file.suffix.lower() not in supported_file_extensions: - continue - try: - f = file_class(file, strptime_format=strptime_format, timezone=timezone) - valid_files.append(f) - except (ValueError, LibsndfileError): - rejected_files.append(file) + is_file_ok = _parse_file( + file=file, + file_class=file_class, + supported_file_extensions=supported_file_extensions, + strptime_format=strptime_format, + timezone=timezone, + begin_timestamp=first_file_begin, + valid_files=valid_files, + rejected_files=rejected_files, + ) + if is_file_ok: + first_file_begin += valid_files[-1].duration if rejected_files: rejected_files = "\n\t".join(f.name for f in rejected_files) @@ -525,3 +539,28 @@ def from_folder( # noqa: PLR0913 data_duration=data_duration, name=name, ) + + +def _parse_file( + file: Path, + file_class: type, + supported_file_extensions: list[str], + strptime_format: str, + timezone: str | pytz.timezone | None, + begin_timestamp: Timestamp, + valid_files: list[BaseFile], + rejected_files: list[Path], +) -> bool: + if file.suffix.lower() not in supported_file_extensions: + return False + try: + if strptime_format is None: + f = file_class(file, begin=begin_timestamp, timezone=timezone) + else: + f = file_class(file, strptime_format=strptime_format, timezone=timezone) + valid_files.append(f) + except (ValueError, LibsndfileError): + rejected_files.append(file) + return False + else: + return True diff --git a/src/osekit/core_api/spectro_data.py b/src/osekit/core_api/spectro_data.py index d716821a..be7be5eb 100644 --- a/src/osekit/core_api/spectro_data.py +++ b/src/osekit/core_api/spectro_data.py @@ -7,6 +7,7 @@ from __future__ import annotations import gc +import itertools from typing import TYPE_CHECKING, Literal import matplotlib.pyplot as plt @@ -99,7 +100,7 @@ def get_default_ax() -> plt.Axes: _, ax = plt.subplots( nrows=1, ncols=1, - figsize=(1.3 * 1800 / 100, 1.3 * 512 / 100), + figsize=(1813 / 100, 512 / 100), dpi=100, ) @@ -121,6 +122,32 @@ def get_default_ax() -> plt.Axes: ) return ax + @BaseData.end.setter + def end(self, end: Timestamp | None) -> None: + """Trim the end timestamp of the data. + + End can only be set to an anterior date from the original end. + + """ + if end >= self.end: + return + if self.audio_data: + self.audio_data.end = end + BaseData.end.fset(self, end) + + @BaseData.begin.setter + def begin(self, begin: Timestamp | None) -> None: + """Trim the begin timestamp of the data. + + Begin can only be set to a posterior date from the original begin. + + """ + if begin <= self.begin: + return + if self.audio_data: + self.audio_data.begin = begin + BaseData.begin.fset(self, begin) + @property def shape(self) -> tuple[int, ...]: """Shape of the Spectro data.""" @@ -215,7 +242,10 @@ def get_value(self) -> np.ndarray: raise ValueError("SpectroData should have either items or audio_data.") sx = self.fft.stft( - self.audio_data.get_value_calibrated(), + x=self.audio_data.get_value_calibrated()[ + :, + 0, + ], # Only consider the 1st channel padding="zeros", ) @@ -264,7 +294,10 @@ def get_welch( nfft = self.fft.mfft _, sx = welch( - self.audio_data.get_value_calibrated(), + self.audio_data.get_value_calibrated()[ + :, + 0, + ], # Only considers the 1rst channel fs=self.audio_data.sample_rate, window=window, nperseg=nperseg, @@ -523,7 +556,7 @@ def split(self, nb_subdata: int = 2) -> list[SpectroData]: """ split_frames = list( - np.linspace(0, self.audio_data.shape, nb_subdata + 1, dtype=int), + np.linspace(0, self.audio_data.length, nb_subdata + 1, dtype=int), ) split_frames = [ self.fft.nearest_k_p(frame) if idx < (len(split_frames) - 1) else frame @@ -532,7 +565,7 @@ def split(self, nb_subdata: int = 2) -> list[SpectroData]: ad_split = [ self.audio_data.split_frames(start_frame=a, stop_frame=b) - for a, b in zip(split_frames, split_frames[1:], strict=False) + for a, b in itertools.pairwise(split_frames) ] return [ SpectroData.from_audio_data( diff --git a/src/osekit/public_api/dataset.py b/src/osekit/public_api/dataset.py index c28da28a..e284350d 100644 --- a/src/osekit/public_api/dataset.py +++ b/src/osekit/public_api/dataset.py @@ -13,6 +13,8 @@ from pathlib import Path from typing import TYPE_CHECKING, TypeVar +from pandas import Timestamp + from osekit import config from osekit.config import DPDEFAULT, resample_quality_settings from osekit.core_api import audio_file_manager as afm @@ -46,13 +48,14 @@ class Dataset: def __init__( # noqa: PLR0913 self, folder: Path, - strptime_format: str, + strptime_format: str | None, gps_coordinates: str | list | tuple = (0, 0), depth: float = 0.0, timezone: str | None = None, datasets: dict | None = None, job_builder: JobBuilder | None = None, instrument: Instrument | None = None, + first_file_begin: Timestamp | None = None, ) -> None: """Initialize a Dataset. @@ -60,9 +63,12 @@ def __init__( # noqa: PLR0913 ---------- folder: Path Path to the folder containing the original audio files. - strptime_format: str + strptime_format: str | None The strptime format used in the filenames. It should use valid strftime codes (https://strftime.org/). + If None, the first audio file of the folder will start + at first_file_begin, and each following file will start + at the end of the previous one. gps_coordinates: str | list | tuple GPS coordinates of the location were audio files were recorded. depth: float @@ -84,6 +90,9 @@ def __init__( # noqa: PLR0913 Instrument that might be used to obtain acoustic pressure from the wav audio data. See the osekit.core_api.instrument module for more info. + first_file_begin: Timestamp | None + Timestamp of the first audio file being processed. + Will be ignored if striptime_format is specified. """ self.folder = folder @@ -94,6 +103,7 @@ def __init__( # noqa: PLR0913 self.datasets = datasets if datasets is not None else {} self.job_builder = job_builder self.instrument = instrument + self.first_file_begin = first_file_begin @property def origin_files(self) -> list[AudioFile] | None: @@ -129,6 +139,7 @@ def build(self) -> None: ads = AudioDataset.from_folder( self.folder, strptime_format=self.strptime_format, + first_file_begin=self.first_file_begin, mode="files", timezone=self.timezone, name="original", @@ -303,6 +314,7 @@ def run_analysis( self, analysis: Analysis, audio_dataset: AudioDataset | None = None, + spectro_dataset: SpectroDataset | None = None, nb_jobs: int = 1, ) -> None: """Create a new analysis dataset from the original audio files. @@ -321,8 +333,14 @@ def run_analysis( audio_dataset: AudioDataset If provided, the analysis will be run on this AudioDataset. Else, an AudioDataset will be created from the analysis parameters. - This can be used to edit the analysis AudioDataset (adding/removing - AudioData etc.) + This can be used to edit the analysis AudioDataset (adding, removing, + renaming AudioData etc.) + spectro_dataset: SpectroDataset + If provided, the spectral analysis will be run on this SpectroDataset. + Else, a SpectroDataset will be created from the audio_dataset if provided, + or from the analysis parameters. + This can be used to edit the analysis SpectroDataset (adding, removing, + renaming SpectroData etc.) nb_jobs: int Number of jobs to run in parallel. @@ -346,9 +364,13 @@ def run_analysis( sds = None if analysis.is_spectro: - sds = self.get_analysis_spectrodataset( - analysis=analysis, - audio_dataset=ads, + sds = ( + self.get_analysis_spectrodataset( + analysis=analysis, + audio_dataset=ads, + ) + if spectro_dataset is None + else spectro_dataset ) self._add_spectro_dataset(sds=sds, analysis_name=analysis.name) diff --git a/tests/conftest.py b/tests/conftest.py index 5cf75518..522a0bd8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -196,7 +196,7 @@ def patch_audio_data(monkeypatch: pytest.MonkeyPatch) -> None: def mocked_init( self: AudioData, *args: list, - mocked_value: list[float] | None = None, + mocked_value: list[float] | np.ndarray | None = None, **kwargs: dict, ) -> None: defaults = { @@ -211,15 +211,20 @@ def mocked_init( original_init(self, *args, **kwargs) if mocked_value is not None: self.mocked_value = mocked_value + if type(mocked_value) is list or len(mocked_value.shape) == 1: + self.mocked_value = np.array(self.mocked_value).reshape( + len(mocked_value), + 1, + ) - def mocked_shape(self: AudioData) -> int: + def mocked_shape(self: AudioData) -> tuple[int, int]: if hasattr(self, "mocked_value"): - return len(self.mocked_value) + return self.mocked_value.shape return original_shape.fget(self) def mocked_get_raw_value(self: AudioData) -> np.ndarray: if hasattr(self, "mocked_value"): - return np.array(self.mocked_value) + return self.mocked_value return original_get_raw_value(self) monkeypatch.setattr(AudioData, "__init__", mocked_init) diff --git a/tests/test_audio.py b/tests/test_audio.py index b461ea58..9e700f79 100644 --- a/tests/test_audio.py +++ b/tests/test_audio.py @@ -9,7 +9,7 @@ import pandas as pd import pytest import soundfile as sf -from pandas import Timestamp +from pandas import Timedelta, Timestamp import osekit from osekit.config import ( @@ -17,6 +17,7 @@ TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, resample_quality_settings, ) +from osekit.core_api import AudioFileManager from osekit.core_api import audio_file_manager as afm from osekit.core_api.audio_data import AudioData from osekit.core_api.audio_dataset import AudioDataset @@ -32,19 +33,20 @@ def test_patch_audio_data(patch_audio_data: None) -> None: audio_data = AudioData( mocked_value=mocked_value, # Type: ignore # Unexpected argument ) - assert ( - audio_data.mocked_value == mocked_value # Type: ignore # Unresolved attribute + assert np.array_equal( + audio_data.mocked_value[:, 0], # Type: ignore # Unresolved attribute + mocked_value, ) - assert audio_data.shape == len(mocked_value) + assert audio_data.shape == (len(mocked_value), 1) assert type(audio_data.begin) is Timestamp assert type(audio_data.end) is Timestamp audio_data.normalization = Normalization.DC_REJECT - assert np.array_equal(audio_data.get_raw_value(), mocked_value) + assert np.array_equal(audio_data.get_raw_value()[:, 0], mocked_value) assert np.array_equal( - audio_data.get_value(), + audio_data.get_value()[:, 0], [v - np.mean(mocked_value, dtype=float) for v in mocked_value], ) @@ -217,7 +219,39 @@ def test_audio_file_read( expected: np.ndarray, ) -> None: files, request = audio_files - assert np.allclose(files[0].read(start, stop), expected, atol=1e-7) + assert np.allclose(files[0].read(start, stop)[:, 0], expected, atol=1e-7) + + +def test_multichannel_audio_file_read(monkeypatch: pytest.MonkeyPatch) -> None: + full_file = np.array([[1, 1, 1], [2, 2, 2], [3, 3, 3], [4, 4, 4], [5, 5, 5]]) + + def read_patch(*args: list, **kwargs: dict) -> np.ndarray: + start, stop = kwargs["start"], kwargs["stop"] + return full_file[start:stop, :] + + monkeypatch.setattr(AudioFileManager, "read", read_patch) + + def init_patch(self: AudioFile, *args: list, **kwargs: dict) -> None: + self.begin = kwargs["begin"] + self.path = kwargs["path"] + self.end = kwargs["end"] + self.sample_rate = kwargs["sample_rate"] + + monkeypatch.setattr(AudioFile, "__init__", init_patch) + + af = AudioFile( + begin=Timestamp("2005-10-18 00:00:00"), + end=Timestamp("2005-10-18 00:00:01"), + path=Path(r"foo"), + sample_rate=5, + ) + + assert np.array_equal(af.read(start=af.begin, stop=af.end), full_file) + + assert np.array_equal( + af.read(start=af.begin, stop=af.begin + Timedelta(seconds=3 / 5)), + full_file[:3, :], + ) @pytest.mark.parametrize( @@ -416,7 +450,18 @@ def test_audio_item( ) -> None: files, request = audio_files item = AudioItem(files[0], start, stop) - assert np.array_equal(item.get_value(), expected) + assert np.array_equal(item.get_value()[:, 0], expected) + assert item.shape == item.get_value().shape + + +def test_empty_audio_item() -> None: + item = AudioItem( + begin=Timestamp("1997-01-28 00:00:00"), + end=Timestamp("1997-01-28 00:00:01"), + ) + assert item.is_empty + assert item.shape == (0, 1) + assert np.array_equal(item.get_value(), np.zeros((1, 1))) @pytest.mark.parametrize( @@ -544,11 +589,11 @@ def test_audio_data( stop: pd.Timestamp | None, expected: np.ndarray, ) -> None: - files, request = audio_files + files, _ = audio_files data = AudioData.from_files(files, begin=start, end=stop) if all(item.is_empty for item in data.items): data.sample_rate = 48_000 - assert np.array_equal(data.get_value(), expected) + assert np.array_equal(data.get_value()[:, 0], expected) @pytest.mark.parametrize( @@ -592,7 +637,7 @@ def test_read_vs_soundfile( ) -> None: audio_files, _ = audio_files ad = AudioData.from_files(audio_files) - assert np.array_equal(sf.read(audio_files[0].path)[0], ad.get_value()) + assert np.array_equal(sf.read(audio_files[0].path)[0], ad.get_value()[:, 0]) @pytest.mark.parametrize( @@ -834,16 +879,25 @@ def test_normalize_audio_data( # AudioData ad = AudioData.from_files(afs, normalization=normalization) - assert np.array_equal(ad.get_value(), normalized_data) + assert np.array_equal(ad.get_value()[:, 0], normalized_data) # AudioDataset ads = AudioDataset.from_files(afs, normalization=normalization) assert ads.data[0].normalization == normalization - assert np.array_equal(ads.data[0].get_value(), normalized_data) + assert np.array_equal(ads.data[0].get_value()[:, 0], normalized_data) @pytest.mark.parametrize( - ("audio_files", "begin", "end", "mode", "duration", "expected_audio_data"), + ( + "audio_files", + "begin", + "end", + "mode", + "strptime_format", + "first_file_begin", + "duration", + "expected_audio_data", + ), [ pytest.param( { @@ -856,6 +910,8 @@ def test_normalize_audio_data( None, None, "timedelta_total", + TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + None, None, generate_sample_audio(1, 48_000), id="one_entire_file", @@ -871,6 +927,8 @@ def test_normalize_audio_data( None, None, "timedelta_total", + TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + None, pd.Timedelta(seconds=1), generate_sample_audio( nb_files=3, @@ -891,6 +949,8 @@ def test_normalize_audio_data( None, None, "timedelta_total", + TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + None, pd.Timedelta(seconds=1), [ generate_sample_audio(nb_files=1, nb_samples=96_000)[0][0:48_000], @@ -916,6 +976,8 @@ def test_normalize_audio_data( None, None, "timedelta_total", + TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + None, pd.Timedelta(seconds=1), generate_sample_audio(nb_files=2, nb_samples=48_000), id="overlapping_files", @@ -932,6 +994,8 @@ def test_normalize_audio_data( None, None, "files", + TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + None, None, generate_sample_audio( nb_files=3, @@ -952,6 +1016,8 @@ def test_normalize_audio_data( None, None, "files", + TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + None, None, [ generate_sample_audio(nb_files=1, nb_samples=96_000)[0][0:48_000], @@ -971,6 +1037,8 @@ def test_normalize_audio_data( None, None, "files", + TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + None, None, [ generate_sample_audio(nb_files=1, nb_samples=48_000)[0][0:24_000], @@ -979,6 +1047,50 @@ def test_normalize_audio_data( ], id="files_mode_with_overlap", ), + pytest.param( + { + "duration": 1, + "sample_rate": 48_000, + "nb_files": 3, + "inter_file_duration": 0, + "date_begin": pd.Timestamp("2024-01-01 12:00:00"), + "series_type": "increase", + }, + None, + None, + "files", + None, + None, + None, + generate_sample_audio( + nb_files=3, + nb_samples=48_000, + series_type="increase", + ), + id="files_mode_with_default_timestamps", + ), + pytest.param( + { + "duration": 1, + "sample_rate": 48_000, + "nb_files": 3, + "inter_file_duration": 0, + "date_begin": pd.Timestamp("2024-01-01 12:00:00"), + "series_type": "increase", + }, + None, + None, + "files", + None, + Timestamp("2020-01-01 00:00:00"), + None, + generate_sample_audio( + nb_files=3, + nb_samples=48_000, + series_type="increase", + ), + id="files_mode_with_given_timestamps", + ), ], indirect=["audio_files"], ) @@ -988,19 +1100,22 @@ def test_audio_dataset_from_folder( begin: pd.Timestamp | None, end: pd.Timestamp | None, mode: Literal["files", "timedelta_total", "timedelta_file"], + strptime_format: str, + first_file_begin: Timestamp | None, duration: pd.Timedelta | None, expected_audio_data: list[np.ndarray], ) -> None: dataset = AudioDataset.from_folder( tmp_path, - strptime_format=TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, + strptime_format=strptime_format, begin=begin, end=end, mode=mode, + first_file_begin=first_file_begin, data_duration=duration, ) for idx, data in enumerate(dataset.data): - vs = data.get_value() + vs = data.get_value()[:, 0] assert np.array_equal(expected_audio_data[idx], vs) @@ -1119,7 +1234,7 @@ def test_audio_dataset_from_files( data_duration=duration, ) assert all( - np.array_equal(data.get_value(), expected) + np.array_equal(data.get_value()[:, 0], expected) for (data, expected) in zip(dataset.data, expected_audio_data, strict=False) ) @@ -1343,7 +1458,7 @@ def test_audio_dataset_from_folder_errors_warnings( strptime_format=TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED, ) assert all( - np.array_equal(data.get_value(), expected) + np.array_equal(data.get_value()[:, 0], expected) for (data, expected) in zip( dataset.data, expected_audio_data, @@ -1456,7 +1571,10 @@ def test_write_files( dataset.write(output_path, subtype=subtype, link=link) for data in dataset.data: assert f"{data}.wav" in [f.name for f in output_path.glob("*.wav")] - assert np.allclose(data.get_value(), sf.read(output_path / f"{data}.wav")[0]) + assert np.allclose( + data.get_value()[:, 0], + sf.read(output_path / f"{data}.wav")[0], + ) if link: assert str(next(iter(data.files)).path) == str(output_path / f"{data}.wav") @@ -1575,10 +1693,10 @@ def test_split_data( if normalization: dataset.normalization = normalization for data in dataset.data: - subdata_shape = data.shape // nb_subdata + subdata_shape = data.length // nb_subdata for subdata, data_range in zip( data.split(nb_subdata), - range(0, data.shape, subdata_shape), + range(0, data.length, subdata_shape), strict=False, ): assert np.array_equal( @@ -1588,10 +1706,10 @@ def test_split_data( assert subdata.instrument == data.instrument assert subdata.normalization == data.normalization - subsubdata_shape = subdata.shape // nb_subdata + subsubdata_shape = subdata.length // nb_subdata for subsubdata, subdata_range in zip( subdata.split(nb_subdata), - range(0, subdata.shape, subsubdata_shape), + range(0, subdata.length, subsubdata_shape), strict=False, ): assert np.array_equal( @@ -1720,7 +1838,7 @@ def test_split_data_frames( ad = dataset.data[0].split_frames(start_frame, stop_frame) assert ad.begin == expected_begin - assert np.array_equal(ad.get_value(), expected_data) + assert np.array_equal(ad.get_value()[:, 0], expected_data) @pytest.mark.parametrize( diff --git a/tests/test_audio_file_manager.py b/tests/test_audio_file_manager.py index f1032163..f60786fd 100644 --- a/tests/test_audio_file_manager.py +++ b/tests/test_audio_file_manager.py @@ -1,14 +1,17 @@ from __future__ import annotations from pathlib import Path +from typing import TYPE_CHECKING import numpy as np import pytest -from osekit.core_api.audio_file import AudioFile from osekit.core_api.audio_file_manager import AudioFileManager from osekit.utils.audio_utils import generate_sample_audio +if TYPE_CHECKING: + from osekit.core_api.audio_file import AudioFile + @pytest.mark.parametrize( ("audio_files", "frames", "expected"), diff --git a/tests/test_core_api_base.py b/tests/test_core_api_base.py index c7c6d3de..ea17dfe5 100644 --- a/tests/test_core_api_base.py +++ b/tests/test_core_api_base.py @@ -3,6 +3,7 @@ from pathlib import Path from typing import Literal +import numpy as np import pytest from pandas import Timedelta, Timestamp @@ -357,6 +358,381 @@ def test_base_dataset_from_files_overlap_errors(overlap: float, mode: str) -> No ) +@pytest.mark.parametrize( + ( + "strptime_format", + "supported_file_extensions", + "begin", + "end", + "timezone", + "mode", + "overlap", + "data_duration", + "first_file_begin", + "name", + "files", + "expected_data_events", + ), + [ + pytest.param( + "%y%m%d%H%M%S", + [".wav"], + None, + None, + None, + "files", + 0.0, + None, + None, + None, + [Path(r"231201000000.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"231201000000.wav")], + ), + ], + id="one_file_default", + ), + pytest.param( + None, + [".wav"], + None, + None, + None, + "files", + 0.0, + None, + Timestamp("2023-12-01 00:00:00"), + None, + [Path(r"bbjuni.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"bbjuni.wav")], + ), + ], + id="one_file_no_strptime", + ), + pytest.param( + None, + [".wav"], + None, + None, + None, + "files", + 0.0, + None, + Timestamp("2023-12-01 00:00:00"), + None, + [Path(r"cool.wav"), Path(r"fun.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"cool.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01"), + end=Timestamp("2023-12-01 00:00:02"), + ), + [Path(r"fun.wav")], + ), + ], + id="multiple_files_no_strptime_should_be_consecutive", + ), + pytest.param( + None, + [".wav", ".mp3"], + None, + None, + None, + "files", + 0.0, + None, + Timestamp("2023-12-01 00:00:00"), + None, + [Path(r"cool.wav"), Path(r"fun.mp3"), Path("boring.flac")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"cool.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01"), + end=Timestamp("2023-12-01 00:00:02"), + ), + [Path(r"fun.mp3")], + ), + ], + id="only_specified_formats_are_kept", + ), + pytest.param( + None, + [".wav"], + None, + None, + None, + "timedelta_total", + 0.0, + Timedelta(seconds=0.5), + Timestamp("2023-12-01 00:00:00"), + None, + [Path(r"cool.wav"), Path(r"fun.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00"), + end=Timestamp("2023-12-01 00:00:00.5"), + ), + [Path(r"cool.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:00.5"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"cool.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01"), + end=Timestamp("2023-12-01 00:00:01.5"), + ), + [Path(r"fun.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01.5"), + end=Timestamp("2023-12-01 00:00:02"), + ), + [Path(r"fun.wav")], + ), + ], + id="timedelta_total", + ), + pytest.param( + None, + [".wav"], + None, + None, + None, + "timedelta_total", + 0.5, + Timedelta(seconds=1), + Timestamp("2023-12-01 00:00:00"), + None, + [Path(r"cool.wav"), Path(r"fun.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"cool.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:00.5"), + end=Timestamp("2023-12-01 00:00:01.5"), + ), + [Path(r"cool.wav"), Path(r"fun.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01"), + end=Timestamp("2023-12-01 00:00:02"), + ), + [Path(r"fun.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01.5"), + end=Timestamp("2023-12-01 00:00:02.5"), + ), + [Path(r"fun.wav")], + ), + ], + id="timedelta_total_with_overlap", + ), + pytest.param( + None, + [".wav"], + Timestamp("2023-12-01 00:00:00.5"), + Timestamp("2023-12-01 00:00:01.5"), + None, + "timedelta_total", + 0.0, + Timedelta(seconds=0.5), + Timestamp("2023-12-01 00:00:00"), + None, + [Path(r"cool.wav"), Path(r"fun.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00.5"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"cool.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01"), + end=Timestamp("2023-12-01 00:00:01.5"), + ), + [Path(r"fun.wav")], + ), + ], + id="timedelta_total_between_timestamps", + ), + pytest.param( + "%y%m%d%H%M%S", + [".wav"], + Timestamp("2023-12-01 00:00:00.5"), + Timestamp("2023-12-01 00:00:01.5"), + None, + "timedelta_total", + 0.0, + Timedelta(seconds=0.5), + None, + None, + [Path(r"231201000000.wav"), Path(r"231201000001.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00.5"), + end=Timestamp("2023-12-01 00:00:01"), + ), + [Path(r"231201000000.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01"), + end=Timestamp("2023-12-01 00:00:01.5"), + ), + [Path(r"231201000001.wav")], + ), + ], + id="striptime_format", + ), + pytest.param( + "%y%m%d%H%M%S", + [".wav"], + Timestamp("2023-12-01 00:00:00.5+01:00"), + Timestamp("2023-12-01 00:00:01.5+01:00"), + "Europe/Warsaw", + "timedelta_total", + 0.0, + Timedelta(seconds=0.5), + None, + None, + [Path(r"231201000000.wav"), Path(r"231201000001.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 00:00:00.5+01:00"), + end=Timestamp("2023-12-01 00:00:01+01:00"), + ), + [Path(r"231201000000.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 00:00:01+01:00"), + end=Timestamp("2023-12-01 00:00:01.5+01:00"), + ), + [Path(r"231201000001.wav")], + ), + ], + id="timezone_location", + ), + pytest.param( + "%y%m%d%H%M%S%z", + [".wav"], + Timestamp("2023-12-01 01:00:00.5+01:00"), + Timestamp("2023-12-01 01:00:01.5+01:00"), + "Europe/Warsaw", + "timedelta_total", + 0.0, + Timedelta(seconds=0.5), + None, + None, + [Path(r"231201000000+0000.wav"), Path(r"231201000001+0000.wav")], + [ + ( + Event( + begin=Timestamp("2023-12-01 01:00:00.5+01:00"), + end=Timestamp("2023-12-01 01:00:01+01:00"), + ), + [Path(r"231201000000+0000.wav")], + ), + ( + Event( + begin=Timestamp("2023-12-01 01:00:01+01:00"), + end=Timestamp("2023-12-01 01:00:01.5+01:00"), + ), + [Path(r"231201000001+0000.wav")], + ), + ], + id="timezone_conversion", + ), + ], +) +def test_base_dataset_from_folder( + monkeypatch: pytest.monkeypatch, + strptime_format: str | None, + supported_file_extensions: list[str], + begin: Timestamp | None, + end: Timestamp | None, + timezone: str | None, + mode: Literal["files", "timedelta_total", "timedelta_file"], + overlap: float, + data_duration: Timedelta | None, + first_file_begin: Timestamp | None, + name: str | None, + files: list[Path], + expected_data_events: list[tuple[Event, list[Path]]], +) -> None: + monkeypatch.setattr(Path, "iterdir", lambda x: files) + + bds = BaseDataset.from_folder( + folder=Path("foo"), + strptime_format=strptime_format, + supported_file_extensions=supported_file_extensions, + begin=begin, + end=end, + timezone=timezone, + mode=mode, + overlap=overlap, + data_duration=data_duration, + first_file_begin=first_file_begin, + name=name, + ) + + assert bds.name == name if name else str(bds.begin) + + for expected, data in zip( + sorted(expected_data_events, key=lambda e: e[0].begin), + sorted(bds.data, key=lambda e: e.begin), + strict=True, + ): + assert data.begin == expected[0].begin + assert data.end == expected[0].end + assert np.array_equal(sorted(f.path for f in data.files), sorted(expected[1])) + + @pytest.mark.parametrize( "destination_folder", [ diff --git a/tests/test_spectro.py b/tests/test_spectro.py index eee25150..90e71f8b 100644 --- a/tests/test_spectro.py +++ b/tests/test_spectro.py @@ -75,7 +75,7 @@ def test_spectrogram_shape( spectro_dataset = SpectroDataset.from_audio_dataset(dataset, sft) for audio, spectro in zip(dataset.data, spectro_dataset.data, strict=False): assert spectro.shape == spectro.get_value().shape - assert spectro.shape == (sft.f.shape[0], sft.p_num(audio.shape)) + assert spectro.shape == (sft.f.shape[0], sft.p_num(audio.length)) @pytest.mark.parametrize( @@ -165,7 +165,7 @@ def test_spectro_parameters_in_npz_files( assert sf.hop == sft.hop assert sf.mfft == sft.mfft assert sf.sample_rate == sft.fs - nb_time_bins = sft.t(ad.shape).shape[0] + nb_time_bins = sft.t(ad.length).shape[0] assert np.array_equal( sf.time, np.arange(nb_time_bins) * ad.duration.total_seconds() / nb_time_bins, @@ -1161,3 +1161,62 @@ def mock_empty_method(*args: list[...], **kwargs: dict) -> None: ltas_ds.save_spectrogram(tmp_path) ltas_ds.save_all(tmp_path, tmp_path) + + +def test_spectro_multichannel_audio_file( + monkeypatch: pytest.MonkeyPatch, + patch_audio_data: pytest.MonkeyPatch, +) -> None: + ad = AudioData(mocked_value=np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])) + + sft = ShortTimeFFT(win=hamming(512), hop=128, fs=48_000) + + sd = SpectroData.from_audio_data(ad, sft) + + treated_audio = [] + + def patch_stft(*args: list, **kwargs: dict) -> None: + treated_audio.append(kwargs["x"]) + + monkeypatch.setattr(ShortTimeFFT, "stft", patch_stft) + + sd.get_value() + + assert np.array_equal( + treated_audio[0], + [1, 5, 9], + ) # Only first channel is accounted for. + + +def test_spectro_begin_and_end(monkeypatch: pytest.MonkeyPatch) -> None: + def mocked_ad_init( + self: AudioData | SpectroData, + *args: list, + **kwargs: dict, + ) -> None: + begin: Timestamp = kwargs["begin"] + end: Timestamp = kwargs["end"] + + self.items = [Event(begin=begin, end=end)] + self.begin = kwargs["begin"] + self.end = kwargs["end"] + + monkeypatch.setattr(AudioData, "__init__", mocked_ad_init) + monkeypatch.setattr(SpectroData, "__init__", mocked_ad_init) + + ad = AudioData( + begin=Timestamp("2020-01-01 00:00:00"), + end=Timestamp("2020-01-01 00:01:00"), + ) + sd = SpectroData( + begin=Timestamp("2020-01-01 00:00:00"), + end=Timestamp("2020-01-01 00:01:00"), + ) + + sd.audio_data = ad + + sd.begin = Timestamp("2020-01-01 00:00:15") + sd.end = Timestamp("2020-01-01 00:00:30") + + assert ad.begin == sd.begin + assert ad.end == sd.end