Vision got JPEG; audio got WAV and MP3. Smell, until now, got a spreadsheet somebody emailed you with no column explanation and three different units in the same column. The .osmell format is an attempt to give smell what sound already has: a single-file container that carries the data and everything needed to interpret it.

The design philosophy is deliberately unglamorous: .osmell is a boring zip. No binary blob, no proprietary codec, no parser you have to reverse-engineer. A .osmell file opens with any unzip tool. Inside are four files, and each one is human-readable.

The Container

recording.osmell
├── manifest.json     # who, what, how — the self-description
├── data.csv          # per-channel raw readings, time-aligned
├── baseline.csv      # the clean-air reference the ratios are built on
└── events.json       # optional: onset, exposure, recovery annotations

A zip, not a folder, buys three things: one file to move, one checksum to verify, and a hard boundary against a client silently editing "just the timestamps." The spec (v1.1.0) is versioned, so an old reader fails loudly on a new file instead of misreading it.

The Manifest — Self-Describing Data

The manifest answers the questions a stranger would ask before they can use your data. The required fields are the small set without which the traces are uninterpretable:

{
  "sensor": {
    "type": "mq-135",
    "adcBits": 12,
    "adcMax": 4095,
    "samplingRateHz": 10,
    "channels": ["NO2", "C2H5OH", "VOC", "CO", "Alcohol", "LPG"]
  },
  "session": {
    "role": "exposure",
    "label": "banana",
    "groupId": "fruit-2026-08"
  },
  "baseline": {
    "source": "explicit",
    "r0Samples": 15
  }
}

Each field exists because a consumer needs it: adcBits/adcMax to reconstruct the physical voltage scale; samplingRateHz to interpret the timeline (with the data file's median gap as the verified rate); channels so the columns mean something; session.role and label so a researcher can split recordings correctly (training on a recording boundary, never a window boundary); baseline.source so the reader knows whether R_0 was a real clean-air measurement or an inference.

The manifest also carries the calibration contract when one exists — per-channel (a, b) constants with their reference substance, reference ppm, date, and method — so a calibrated recording can be round-tripped and re-inverted by any compliant client.

The Data — Raw, Time-Aligned

The traces are deliberately stored as raw values: whatever the ADC produced, one row per timestamp, one column per channel. Storing raw has one non-negotiable virtue: it preserves the possibility of re-normalization. The framework's own guidance is that z-score beat R_s/R_0 for encoder input while paradigm features beat statistical features cross-device — and the only way any reader can test a claim like that is to have the raw data. A .osmell file that stored only pre-normalized features would be a photograph of someone else's opinion about your recording.

The baseline file plays the same role for R_0: the actual clean-air samples, stored next to the data, so R_0 can be recomputed with a different window, a different aggregator, or a different policy — instead of trusting a number that was decided at capture time.

The Events — Structure the Reader Can Trust

events.json is optional but recommended, and it is where the session protocol becomes data: the moment an exposure began, when it ended, when recovery completed. These timestamps let a downstream pipeline segment the raw trace into baseline / onset / response / recovery regions without guessing — and they make the recording comparable to others that followed the same protocol. This is the difference between "a file with a smell in it" and "a recording that belongs to a dataset."

Normalization: The Menu, Kept Open

Because the container keeps raw + baseline + manifest separate, normalization is a client-side choice, applied after reading:

  • (R − R₀)/R₀ — simple, interpretable, common.
  • R_s/R₀ — the ratio that cancels V_cc and R_L (see the normalization theorem essay).
  • z-score — per-recording standardization; the measured winner for encoder input.

The format does not pick one for you. It preserves the raw + baseline structure so that any client can pick its own — and so that the choice is auditable in any re-analysis. That is the definition of interoperability used in this project: not "one pipeline," but "a container that keeps every pipeline auditable."

Quality: Seven Factors, Scored Not Assumed

A recording is only as good as its capture discipline, so the container pairs every recording with a quality score computed from seven factors:

FactorWhat it checksWeight
Continuityno irregular gaps in the timeline0.15
Dynamic rangethe signal spans a meaningful ADC range0.10
Saturation-freethe trace never pinned at a rail0.10
Baseline stabilityR₀ window is flat, not drifting0.20
Signal strengththe response is above the noise floor0.20
Recovery completenessthe trace returns toward baseline0.15
Duration adequacythe window covers the full event0.00*

The zero-weight on the last factor is deliberate: duration is logged but not scored, because the auto-R₀ policy caps the overall score at 50 when it has to infer a baseline — punishing the "we didn't record a clean baseline" case structurally rather than rhetorically. A low quality score is not a suggestion; it is a warning that the recording should not be trusted for the features it flags.

Loose CSV Ingress: Meeting Reality

Not everyone will adopt the container tomorrow, so the pipeline accepts loose CSVs and upgrades them: it infers the sampling rate from the median gap, builds an auto-manifest, and defaults to r0Samples = 15 with baseline.source = "auto". The upgrade path is explicit about what was inferred and what was measured — a loose CSV becomes a valid .osmell file whose manifest states that R_0 was not recorded properly.

Why the Boring Details Win

Every choice in this format — raw over normalized, explicit over inferred, scored over assumed — is a decision to make the container carry the discipline instead of the conversation. A .osmell file you receive in 2027 should be as interpretable as one you recorded today, by a program that never met the recorder. That is the same bar JPEG and WAV met decades ago, and it is the bar digital olfaction has been missing.

Sources & Further Reading

  • OpenSmell master reference, §2 (SDK quick start), §3 (the full .osmell specification, v1.1.0), §7.1–7.4 (web import and quality).
  • opensmell/opensmell/io.py and osmograph-web/lib/osmell/io.ts — the mirroring Python/TypeScript implementations.
  • opensmell/opensmell/mox/quality.py — the seven-factor scorer.