---
title: "Is my audio upsampled?"
description: "One call per file and one field to read: bandLimit.upsampled says yes, no or cannot-say, with the reason in plain words. What to do with each answer, how often each came out in our tests, and what it cannot see."
cluster: General
level: Practical
updatedAt: 2026-11-10
lang: en
source: https://audiolab.tools/docs/start/is-my-audio-upsampled
---

## Who this is for

You build or buy speech for a dataset that should be wideband, and you want to know which files only look that way. The sample rate in a header says how a file is stored, not what is in it: a telephone call stored at 16 or 48 kHz still stops near 4 kHz.

This page gives you one call, one field to read, and what to do with each answer. You do not need to know how the engine decides: the limits are in the answer itself.

## 1. Call it

Add `bandLimit` to a request to the signal analysis. For one local file with curl:

```bash
curl -s "https://audiolab.tools/v1/signallab?bandLimit=true" \
  -H "Authorization: Bearer $AUDIOLAB_KEY" \
  -F file=@recording.wav
```

For a file at a public URL, send JSON instead:

```bash
curl -s https://audiolab.tools/v1/signallab \
  -H "Authorization: Bearer $AUDIOLAB_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/recording.wav", "bandLimit": true}'
```

With the Python client (`pip install audiolabtools`), one file, then a folder 20 files at a time into a CSV:

```python
import csv
from pathlib import Path

from audiolabtools import AudioLab

lab = AudioLab()  # reads AUDIOLAB_KEY

band = lab.signal("recording.wav", bandLimit=True)["bandLimit"]
print(band["upsampled"], band["reason"])

files = sorted(str(p) for p in Path("recordings").glob("*.wav"))
with open("upsampled.csv", "w", newline="", encoding="utf-8") as f:
    out = csv.writer(f)
    out.writerow(["file", "upsampled", "ratio", "reason"])
    for i in range(0, len(files), 20):
        chunk = files[i : i + 20]
        answer = lab.batch(chunk, endpoint="signallab", bandLimit=True)
        for name, r in zip(chunk, answer["results"]):
            b = (r.get("data") or {}).get("bandLimit") or {}
            out.writerow([name, b.get("upsampled", "error"), b.get("ratio"),
                          b.get("reason", r.get("error"))])
```

In an AI assistant with the AudioLab MCP server (version 0.5.1 or later), `index_signal` takes `bandLimit: true`, and so does `analyze_batch` with `endpoint: "signallab/index"`, for 20 files at a time. Ask it, for example: "Which of these recordings were upsampled?"

Every file is one call and is uploaded to the API, hosted in the EU; only numbers come back. For a large folder, [the dataset audit](/docs/start/audit-a-conversation-dataset) has a full script that paces itself, can sample, and picks up where it stopped.

## 2. Read one field

Read `bandLimit.upsampled`. It is `yes`, `no` or `cannot-say`. Next to it, `bandLimit.reason` says the same in plain words, with the bandwidth and the sample rate: copy it into your report as it is. This is the answer for a recording we brought down to 8 kHz and back up to 16 kHz:

```json
{
  "bandLimit": {
    "upsampled": "yes",
    "status": "validated",
    "reason": "The band stops at 4.3 kHz in a 16 kHz file: the audio was probably at 8 kHz or less, as in a telephone call, before it was stored at this rate. In study UP-1, 0.1 to 1.8% of untouched recordings read the same way.",
    "basis": [
      {
        "check": "telephone-origin (UP-1)",
        "status": "validated",
        "result": "fires"
      },
      {
        "check": "band-limit ratio (UP-2)",
        "status": "proxy",
        "result": "band-limited",
        "ratio": 0.537
      }
    ],
    "bandwidthHz": 4296.9,
    "sampleRate": 16000,
    "ratio": 0.537,
    "verdict": "band-limited",
    "reasonCode": "ratio-below-L"
  }
}
```

`status` is `validated` only for a `yes`. `basis` lists each condition the answer rests on, with its own status, so you can see why. You do not need it to act.

## 3. What to do with each answer

| `upsampled` | What it means | What to do |
|---|---|---|
| `yes` | The band stops under 5 kHz in a file of 16 kHz or more, for at least 2 seconds: the audio was probably at 8 kHz or less, as in a telephone call, before it was stored at this rate. Status validated (study UP-1). | Do not use it as native wideband. |
| `no` | The band reaches half the sample rate: no sign of upsampling. Status proxy (study UP-2). | Nothing to do. |
| `cannot-say` | Everything else: a band that stops early without the telephone pattern, a band between 80 and 90 percent of half the sample rate, a file under 2 seconds, a tonal spectrum, or silence. The reason says which. | Set it aside. If your dataset may only hold native wideband, leave it out or listen to it. If you need a number to filter on, `bandLimit.ratio` is the bandwidth over half the sample rate. |

Why a band that only stops early is not a `yes`: a narrow source looks the same. In study UP-2, a LibriSpeech speaker recorded at its own rate read as band-limited, and so would a microphone or a filter that cuts the top.

## 4. How often each answer came out in our tests

We read the recordings of both studies again with the decision, on 10 November 2026. The two rules were fixed before those studies ran, and how they combine into one answer was fixed before this reading, so this is a description of how the three answers fall, not a new test. The UP-1 recordings were read twice, at their own rate and decoded at 48 kHz, so the counts are readings, not files.

| Kind of recording | Readings | `yes` | `no` | `cannot-say` |
|---|---|---|---|---|
| Through an 8 kHz telephone codec and back to 16 or 48 kHz (UP-1: G.711, AMR, GSM) | 4,692 | 4,608 (98.2%) | 0 | 84 (1.8%) |
| Through a telephone codec (UP-2: AMR, G.711, GSM) | 90 | 90 (100%) | 0 | 0 |
| Upsampled from 4 kHz | 450 | 443 (98.4%) | 0 | 7 (1.6%) |
| Upsampled from 8 kHz | 450 | 436 (96.9%) | 0 | 14 (3.1%) |
| Upsampled from 11.025 kHz | 450 | 1 (0.2%) | 0 | 449 (99.8%) |
| Upsampled from 16 kHz to 44.1 or 48 kHz | 3,428 | 5 (0.1%) | 0 | 3,423 (99.9%) |
| FLEURS speech at its own 16 kHz | 1,082 | 14 (1.3%) | 1,032 (95.4%) | 36 (3.3%) |
| LibriSpeech speech at its own 16 kHz | 150 | 0 | 141 (94.0%) | 9 (6.0%) |
| Speech cut off at 6 kHz, at its own 16 kHz | 782 | 17 (2.2%) | 1 (0.1%) | 764 (97.7%) |
| Speech through a lossy codec (MP3 32 to 128 kbit/s, AAC 48, Opus 16) | 300 | 0 | 130 (43.3%) | 170 (56.7%) |

What that means for you:

- No upsampled reading got a `no`: 0 of 9,560.
- `yes` finds the telephone pattern and upsampling from 4 or 8 kHz. Upsampling from 11.025 kHz or more comes out `cannot-say`, almost never `yes`. So read `cannot-say` as "look at this", not as "probably fine".
- On speech at its own rate, a `yes` was wrong for 14 of 1,082 FLEURS readings (1.3%) and none of the 150 LibriSpeech readings. Speech cut off at 6 kHz read `yes` in 17 of 782 (2.2%).

## What it cannot see

- Real telephone lines, VoIP codecs (Opus, G.722) and Bluetooth headsets. UP-1 used telephone codecs we ran ourselves.
- Native 44.1 and 48 kHz recordings: how often they would read `cannot-say` is not measured. A 44.1 kHz source stored at 48 kHz reads `no`; the rule cannot see that step.
- An upsampler that makes up high frequencies of its own, such as a model that restores the band, reads `no`.
- It names no source rate, apart from the telephone origin.

The two studies, with their pre-registration and results, are on [the studies page](/evidence/studies). What the band limit was tested on is also on [its page](/lenses#provenance.bandLimit).
