Skip to content

Commit

Permalink
Added audio.files module for identifying audio files
Browse files Browse the repository at this point in the history
  • Loading branch information
mbsantiago committed Jul 27, 2023
1 parent c790475 commit 63ec4eb
Show file tree
Hide file tree
Showing 14 changed files with 823 additions and 85 deletions.
104 changes: 104 additions & 0 deletions docs/user_guide/0_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
"""
# Acoustic Objects
Here we showcase the different objects that are defined
within `soundevent`.
"""

# %%
# ## The data module
#
# All you need to do is import the `data` module from `soundevent`.

from soundevent import data

# %%
# ## Geometries
#
# Here we showcase the different geometries that are defined
# within `soundevent`.
#
# !!! warning
# All the geometry coordinates should be provided in seconds and Hz.

# %%
# ### TimeStamp
# A `TimeStamp` is defined by a point in time relative to the start of the
# recording.

time_stamp = data.TimeStamp(
coordinates=0.1,
)
time_stamp

# %%
# ### TimeInterval
# A `TimeInterval` consists of two points in time to mark the start and end of
# an interval.

time_interval = data.TimeInterval(
coordinates=[0.1, 0.2],
)
time_interval

# %%
# ### Point
# A `Point` is a point in time and frequency.

point = data.Point(
coordinates=[0.1, 2000],
)
point

# %%
# ### BoundingBox

box = data.BoundingBox(
coordinates=[0.1, 2000, 0.2, 3000],
)
box

# %%
# A `LineString` is a sequence of points that are connected by a line.

line = data.LineString(
coordinates=[[0.1, 2000], [0.2, 4000]],
)
line

# %%
# A `Polygon`

polygon = data.Polygon(
coordinates=[
[
[0.1, 2000],
[0.2, 3000],
[0.3, 2000],
[0.2, 1000],
[0.1, 2000],
],
[
[0.15, 2000],
[0.25, 2000],
[0.2, 1500],
[0.15, 2000],
],
],
)
polygon


# %%
# ## Geometry functions
#
# You can buffer geometries in time and frequency.

from soundevent import geometry

buffer = geometry.buffer_geometry(
polygon,
time_buffer=0.01,
freq_buffer=100,
)
buffer
2 changes: 1 addition & 1 deletion docs/user_guide/example_dataset.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"info":{"uuid":"c1cb90ed-5486-4996-a7dd-b2d4a5de8251","name":"test_dataset","description":"A test dataset.","date_created":"2023-07-16T22:45:47.333462"},"tags":[{"id":0,"key":"species","value":"Myotis myotis"},{"id":1,"key":"sex","value":"female"},{"id":2,"key":"behaviour","value":"foraging"},{"id":3,"key":"species","value":"Eptesicus serotinus"},{"id":4,"key":"sex","value":"male"},{"id":5,"key":"behaviour","value":"social calls"}],"recordings":[{"id":0,"uuid":"c6e43cbb-1a55-467c-9071-0774b0198969","path":"recording1.wav","duration":10.0,"channels":1,"samplerate":441000,"time_expansion":10.0,"hash":"1234567890abcdef","date":"2021-01-01","time":"21:34:56","latitude":12.345,"longitude":34.567,"tags":[0,1,2],"features":{"SNR":10.0,"ACI":0.5},"notes":[{"uuid":"8ed5b892-86cf-4f39-bb3a-c97b9f5c0f6b","message":"This is a note.","created_by":"John Doe","is_issue":false,"created_at":"2021-01-01T12:34:56"}]},{"id":1,"uuid":"43cbf2e1-87ae-4ff8-af9e-7038e0300e39","path":"recording2.wav","duration":8.0,"channels":1,"samplerate":441000,"time_expansion":10.0,"hash":"234567890abcdef1","date":"2021-01-02","time":"19:34:56","latitude":13.345,"longitude":32.567,"tags":[3,4,5],"features":{"SNR":7.0,"ACI":0.3},"notes":[{"uuid":"429c455c-eb45-4f5e-99e6-35d06119463e","message":"Unsure about the species.","created_by":"John Doe","is_issue":false,"created_at":"2021-01-01T12:34:56"}]}]}
{"info":{"uuid":"c1cb90ed-5486-4996-a7dd-b2d4a5de8251","name":"test_dataset","description":"A test dataset.","date_created":"2023-07-17T22:31:32.819677"},"tags":[{"id":0,"key":"species","value":"Myotis myotis"},{"id":1,"key":"sex","value":"female"},{"id":2,"key":"behaviour","value":"foraging"},{"id":3,"key":"species","value":"Eptesicus serotinus"},{"id":4,"key":"sex","value":"male"},{"id":5,"key":"behaviour","value":"social calls"}],"recordings":[{"id":0,"uuid":"c6e43cbb-1a55-467c-9071-0774b0198969","path":"recording1.wav","duration":10.0,"channels":1,"samplerate":441000,"time_expansion":10.0,"hash":"1234567890abcdef","date":"2021-01-01","time":"21:34:56","latitude":12.345,"longitude":34.567,"tags":[0,1,2],"features":{"SNR":10.0,"ACI":0.5},"notes":[{"uuid":"8ed5b892-86cf-4f39-bb3a-c97b9f5c0f6b","message":"This is a note.","created_by":"John Doe","is_issue":false,"created_at":"2021-01-01T12:34:56"}]},{"id":1,"uuid":"43cbf2e1-87ae-4ff8-af9e-7038e0300e39","path":"recording2.wav","duration":8.0,"channels":1,"samplerate":441000,"time_expansion":10.0,"hash":"234567890abcdef1","date":"2021-01-02","time":"19:34:56","latitude":13.345,"longitude":32.567,"tags":[3,4,5],"features":{"SNR":7.0,"ACI":0.3},"notes":[{"uuid":"429c455c-eb45-4f5e-99e6-35d06119463e","message":"Unsure about the species.","created_by":"John Doe","is_issue":false,"created_at":"2021-01-01T12:34:56"}]}]}
2 changes: 1 addition & 1 deletion docs/user_guide/nips4b_plus_sample.json

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion src/soundevent/audio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
"""Soundevent functions for handling audio files and arrays."""

from .io import load_clip, load_recording
from .media_info import MediaInfo, get_media_info
from .media_info import MediaInfo, get_media_info, compute_md5_checksum
from .spectrograms import compute_spectrogram
from .files import is_audio_file

__all__ = [
"MediaInfo",
"compute_spectrogram",
"compute_md5_checksum",
"get_media_info",
"load_clip",
"load_recording",
"is_audio_file",
]
34 changes: 34 additions & 0 deletions src/soundevent/audio/files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Functions for recognizing audio files.
For now only WAV files are supported.
"""


import os
from pathlib import Path
from typing import Union

__all__ = [
"is_audio_file",
]

PathLike = Union[os.PathLike, str]


def is_audio_file(path: PathLike) -> bool:
"""Return whether the file is an audio file.
Parameters
----------
path : PathLike
Path to the file.
Returns
-------
bool
Whether the file is an audio file.
"""
path = Path(path)
if not path.is_file():
return False
return path.suffix.lower() in (".wav",)
35 changes: 33 additions & 2 deletions src/soundevent/audio/media_info.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
"""Functions for getting media information from WAV files."""
import hashlib
import os
from dataclasses import dataclass
from typing import Union

from soundevent.audio.chunks import parse_into_chunks

__all__ = [
"MediaInfo",
"get_media_info",
"compute_md5_checksum",
]


PathLike = Union[os.PathLike, str]


@dataclass
class MediaInfo:
"""Media information."""
Expand All @@ -33,7 +39,7 @@ class MediaInfo:
"""Number of channels."""


def get_media_info(path: os.PathLike) -> MediaInfo:
def get_media_info(path: PathLike) -> MediaInfo:
"""Return the media information from the WAV file.
The information extracted from the WAV file is the audio format,
Expand All @@ -43,7 +49,7 @@ def get_media_info(path: os.PathLike) -> MediaInfo:
Parameters
----------
path : os.PathLike
path : PathLike
Path to the WAV file.
Returns
Expand Down Expand Up @@ -90,3 +96,28 @@ def get_media_info(path: os.PathLike) -> MediaInfo:
samples=samples,
duration_s=duration,
)


BUFFER_SIZE = 65536


def compute_md5_checksum(path: PathLike) -> str:
"""Compute the MD5 checksum of a file.
Parameters
----------
path : PathLike
Path to the file.
Returns
-------
str
MD5 checksum of the file.
"""
md5 = hashlib.md5()
with open(path, "rb") as fp:
buffer = fp.read(BUFFER_SIZE)
while len(buffer) > 0:
md5.update(buffer)
buffer = fp.read(BUFFER_SIZE)
return md5.hexdigest()
59 changes: 58 additions & 1 deletion src/soundevent/data/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,17 @@
scientific practices.
"""

from typing import List, Optional
import os
from pathlib import Path
from typing import List, Optional, Union
from uuid import UUID, uuid4

from pydantic import BaseModel, Field

from soundevent.data.recordings import Recording

PathLike = Union[str, os.PathLike]


class Dataset(BaseModel):
"""Datasets."""
Expand All @@ -73,3 +77,56 @@ class Dataset(BaseModel):

recordings: List[Recording] = Field(default_factory=list, repr=False)
"""List of recordings associated with the dataset."""

@classmethod
def from_directory(
cls,
path: PathLike,
name: Optional[str] = None,
description: Optional[str] = None,
recursive: bool = True,
compute_hash: bool = True,
) -> "Dataset":
"""Return a dataset from the directory.
Reads the audio files in the directory and returns a dataset
containing the recordings.
Parameters
----------
path : PathLike
Path to the directory.
recursive : bool, optional
Whether to search the directory recursively, by default True
compute_hash : bool, optional
Whether to compute the hash of the audio files, by default
Returns
-------
Dataset
The dataset.
Raises
------
ValueError
If the path is not a directory.
"""
path = Path(path)

if not path.is_dir():
raise ValueError(f"Path is not a directory: {path}")

glob_pattern = "**/*.wav" if recursive else "*.wav"

recordings = [
Recording.from_file(file, compute_hash=compute_hash)
for file in path.glob(glob_pattern)
]

return cls(
name=name or path.name,
recordings=recordings,
description=description,
)
Loading

0 comments on commit 63ec4eb

Please sign in to comment.