Sound Onion Log in

Beta We are beta testing now, and open for business soon.

Defined formats. Your own scripts.

Every recording's facts, every detection, every review and every correction is a plain, versioned file beside the audio. Read them, script against them, write your own: everything captured in Sound Onion is yours to analyse at scale.

What goes in

WhereReads
Sound Onion, in the browserWAV: 8, 16, 24 and 32-bit integer and 32-bit float, any sample rate, any number of channels (it reads the first channel)
Acoustic AnalyzerWAV; FLAC and AIFF one file at a time

Your recordings stay in your folders, named as you name them. Sound Onion adds its files beside them and never changes yours.

What is written beside your recordings

Survey_2026/                                  a review pack
  review_pack.yaml                            the mark a pack is found by: its clips, how it was made
  DEMO_A_20260601_101500_000/                 one recording's clips
    DEMO_A_20260601_101500_000__02m10.0s-02m16.0s.wav       a clip: the recording's own samples
    metadata/                                 what a script wrote
      DEMO_A_20260601_101500_000__02m10.0s-02m16.0s.info.yaml     where the clip came from, the detector's claim
      DEMO_A_20260601_101500_000__02m10.0s-02m16.0s.pyramid.*     the spectrogram's detail levels
    edits/                                    what a person wrote
      DEMO_A_20260601_101500_000__02m10.0s-02m16.0s.AB.k3x9q2.yaml       reviewer AB's answer
      DEMO_A_20260601_101500_000__02m10.0s-02m16.0s.AB.k3x9q2-002.yaml   AB's next answer; the earlier one is kept

Versioned, so old files keep working

Every file the analyser writes, and every review, starts with its format, a number, and its schema, the kind of file. A reader brings an older format up to date as it reads, and refuses one newer than it knows rather than guess. A new optional field does not change the format; removing, renaming or changing the meaning of one does, with a step to upgrade.

KindFileHolds
detection<recording>.detection.yamleverything the analyser found in a recording, with its evidence
clipmetadata/<clip>.info.yamlwhere a clip came from, its gain, and the detector's claim
review-pack<pack>/review_pack.yamlthe mark a pack of clips is found by
learningslearnings_<stamp>.yamlevery reviewer's answers, scored, for the analyser to learn from
rules<name>.rules.yamlthe rules a run reached from its learnings

A detection, as the analyser writes it

One entry for each thing found, in time order: the box in time and frequency, the model's class and how sure it is, in numbers and in words.

format: 2
schema: detection
detections:
- t0: 1395.5            # s, in the recording's own time
  t1: 1398.5
  f0: 3183.1            # Hz
  f1: 14337.7
  label: pinger
  group: man-made       # animal, man-made, ambient or unsure
  confidence: 0.83
  sure: fairly          # very, fairly, leaning or guessing
  runner_up: rowing
  share: 0.82           # this box's part of the evidence at that moment
  energy_in_band: mostly above
  said: "Pinger, fairly sure (runner-up: rowing), at 1395.5–1398.5 s in 3.2 kHz–14.3 kHz
    (82% of the evidence at that time). …"

A review, as a reviewer saves it

format: 3
schema: review
clip: DEMO_A_20260601_101500_000__02m10.0s-02m16.0s.wav
reviewer: AB
install: k3x9q2
at: '2026-06-03T10:00:00.000Z'
rows:
  - source: animal       # animal, man-made, ambient or unsure
    what: whistle
    notes: ''
tags: []

Script it, at any scale

Because every answer is a file, a few lines of your own code reach everything captured in Sound Onion: every reviewer, every clip, every survey. This counts what each reviewer said, taking each one's latest answer:

from collections import Counter
from pathlib import Path
import yaml

latest = {}
for f in Path("Survey_2026").rglob("edits/*.yaml"):
    review = yaml.safe_load(f.read_text())
    if not isinstance(review, dict) or review.get("schema") != "review" or "rows" not in review:
        continue                                   # a folder's tags, or an answer withdrawn
    key = (review["clip"], review["reviewer"])
    if key not in latest or review["at"] > latest[key]["at"]:
        latest[key] = review                       # each reviewer's newest answer is the one that stands

said = Counter((row.get("source"), row.get("what")) for r in latest.values() for row in r["rows"])
print(said.most_common())

Publish results does the whole collection for you, scored against every detection, into one learnings file.

Bring your own

The analyser and its models

Talk to us

We are beta testing now and will open for business soon. Tell us what you record and how much of it there is: we will show you your own data in Sound Onion, and you will be among the first.

Email us

accounts@sound-onion.com

Log in

.sound-onion.com

The name we gave you when we set up your site, as in yourlab.sound-onion.com.