Metadata-Version: 2.4
Name: bioacoustic-synthesis
Version: 0.1.0
Summary: Synthesises bioacoustic training data with analytically derived box and mask labels
Author: Kaspar Soltero
License-Expression: GPL-3.0-or-later
Project-URL: Repository, https://github.com/KasparSoltero/bioacoustic-synthesis
Project-URL: Issues, https://github.com/KasparSoltero/bioacoustic-synthesis/issues
Keywords: bioacoustics,ecoacoustics,data-augmentation,annotation,spectrogram
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: COPYING.txt
Requires-Dist: matplotlib>=3.7
Requires-Dist: numpy>=1.24
Requires-Dist: pillow>=10.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: scipy>=1.10
Requires-Dist: soundfile>=0.12
Requires-Dist: torch>=2.1
Requires-Dist: torchaudio>=2.1
Requires-Dist: sounddevice>=0.4
Dynamic: license-file

# bioacoustic-synthesis

Builds synthetic soundscapes by mixing isolated vocalisations into field-recorded
backgrounds at controlled signal-to-noise ratios. Because every source signal is
known before it is mixed, each placement carries an analytically derived label —
presence, time span, time-frequency box, and time-frequency mask — with no hand
annotation.

## Install

```bash
pip install bioacoustic-synthesis
```

## Quick start

```bash
bioacoustic-synthesis -c config.yaml
```

Useful flags: `-i` reviews each generated soundscape interactively, `-n` caps the
number of source files used per class, and `-s` fixes the RNG seed. Omit `-s` and
a seed is drawn at random and written into the output's `generation_config.yaml`,
so any run can be reproduced after the fact.

## Input

Three folders of audio, all pointed at from the config. Formats: `.wav`, `.flac`,
`.mp3`.

**Positives** — *isolated* vocalisations, one subfolder per class. The subfolder
name becomes the class label. Each clip should contain the target call and as
little else as possible, since everything in the clip is placed into the mix and
counted as signal when the SNR is set.

```
vocalisations/
├── petroica_australis/
│   ├── clip_001.wav
│   └── clip_002.wav
└── prosthemadera_novaeseelandiae/
    └── clip_003.wav
```

**Negatives** — isolated contaminants: rain, wind gusts, machinery, handling
noise, anything that a detector should learn to reject. These are placed like
vocalisations, at a comparable SNR, and generate no labels.

**Backgrounds** — clean ambience carrying none of the positive classes, since any
call present here becomes an unlabelled positive in the output. Each file must be
**longer than `length_seconds`** so a window can be cropped from it; shorter files
are reassigned to the negatives pool. Background diversity is the single largest
influence on how well a detector trained on the result generalises, so favour many
distinct recordings over long ones.

Optionally, a `tags.csv` in a positives or negatives folder with a `filename`
column attaches arbitrary metadata to each record.

## Configuration

Everything is driven by one YAML file. See `config.example.yaml`:

```yaml
paths:
  vocalisations: [corpora/vocalisations]
  negative: [corpora/negatives]
  noise: [corpora/backgrounds]
  output: output

synthesis:
  n_soundscapes: 1000
  length_seconds: 10
  sample_rate: 32000

  # How many events land in each soundscape, drawn uniformly in range
  positive_overlay_range: [1, 4]
  negative_overlay_range: [0, 2]

  # Consecutive plays of the same source clip, and the gap between them
  repetitions: [1, 3]
  repetitions_spacing_s: [0.5, 3.0]

  # True dB power ratios, measured where the overlay actually has energy
  snr_db_range: [0, 25]
  negative_snr_db_range: [0, 10]

  # Label definition: a pixel joins the mask at this many dB above the local
  # noise floor, and a placement is rejected if too little of its mask survives
  mask_threshold_db: 1
  minimum_mask_area_px: 200

  edge_fade_ms: 50

  # Whether band-limited backgrounds are usable, and the tolerance for calling
  # a recording full-band
  allow_bandpass: true
  bandpass_tolerance_hz: 500

  synthetic_noise:
    white: true
    pink: true
    brown: true
    probability: 0.5
    db_range: [-46.0, -30.0]

spectrogram:
  n_fft: 2048
  win_length: 2048
  hop_length: 512
  log_base: 10.0

output:
  include_audio: true
  include_spectrogram: true
  include_boxes: true
  include_masks: true
  include_presence: true
  include_simple_labels: true
  overwrite: true
  color_mode: HSV        # HSV, RGB, or L (greyscale)
  target_db: -10.0
  generate_raw_dataset: false
  ignore_classes: []

proportions:
  species: {}
  noise: {}
```

## Output

Written to `<output>/artificial_dataset/`:

| Path | Contents |
| --- | --- |
| `sound_files/` | the mixed soundscape, 16-bit PCM WAV |
| `images/` | the rendered spectrogram, 640×640 JPEG, PCEN-normalised, log-frequency |
| `labels/` | class ids present in the soundscape, space separated |
| `presence/` | 1000-bin boolean array over the window, `.npy` |
| `box_labels/` | time-frequency boxes in YOLO format |
| `unetplusplus_masks/` | paired PNGs — image, per-instance mask, per-class mask |
| `provenance.csv` | every placement: source file, species, sample range, SNR |
| `species_value_map.csv` | class id to label |
| `dataset.yaml` | class map for YOLO training |
| `generation_config.yaml` | the config used, plus the seeds and limits the run applied |
| `example/` | the first three soundscapes as audio and annotated figures |

Each placement is labelled at four levels of detail, all derived from the same
source signal:

| | | | |
| :---: | :---: | :---: | :---: |
| ![Presence](https://raw.githubusercontent.com/KasparSoltero/bioacoustic-synthesis/main/docs/images/01_presence.png) | ![Time span](https://raw.githubusercontent.com/KasparSoltero/bioacoustic-synthesis/main/docs/images/02_timespan.png) | ![Time-frequency box](https://raw.githubusercontent.com/KasparSoltero/bioacoustic-synthesis/main/docs/images/03_tf_box.png) | ![Time-frequency mask](https://raw.githubusercontent.com/KasparSoltero/bioacoustic-synthesis/main/docs/images/04_tf_mask.png) |
| Presence | Time span | Time-frequency box | Time-frequency mask |

The mask is the primary label. It marks every spectrogram pixel where the
isolated source sits at least `mask_threshold_db` above the noise floor measured
at that placement, so it follows the call's actual structure rather than a
rectangle around it. The box is the mask's extent, and the time span and presence
labels are progressive reductions of the same measurement. A placement whose mask
is smaller than `minimum_mask_area_px` is discarded and redrawn, which keeps the
faintest events out of the labels rather than labelling something invisible.

Setting `generate_raw_dataset: true` produces a second, parallel dataset from
unisolated source clips, for comparing what the isolation contributes.

## Licence

GNU GPLv3
