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:
| Factor | What it checks | Weight |
|---|---|---|
| Continuity | no irregular gaps in the timeline | 0.15 |
| Dynamic range | the signal spans a meaningful ADC range | 0.10 |
| Saturation-free | the trace never pinned at a rail | 0.10 |
| Baseline stability | R₀ window is flat, not drifting | 0.20 |
| Signal strength | the response is above the noise floor | 0.20 |
| Recovery completeness | the trace returns toward baseline | 0.15 |
| Duration adequacy | the window covers the full event | 0.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
.osmellspecification, v1.1.0), §7.1–7.4 (web import and quality). opensmell/opensmell/io.pyandosmograph-web/lib/osmell/io.ts— the mirroring Python/TypeScript implementations.opensmell/opensmell/mox/quality.py— the seven-factor scorer.
