# Working with audio

> Read and write files in any common format, get at the samples, and keep your own files, with nothing to install.

The SDK carries its own ffmpeg, so these helpers work the same on every computer Mixel runs on, and your app never runs programs itself.

## Look at a file

```python
info = ox.audio.probe(path)
info.rate, info.channels, info.duration, info.codec   # 44100, 2, 183.4, "pcm_s16le"
```

## Change the format

`ox.audio.convert(path, format)` writes a new file and returns its `Path`. The formats are `wav`, `aiff`, `flac`, `mp3`, `aac`, `ogg` and `opus`.

```python title="app.py"
import ox1audio as ox


@ox.action("Make FLAC copies")
def flac(files: ox.Files("audio")):
    made = []
    for i, path in enumerate(files):
        ox.progress(i / len(files), path.name)
        out = ox.audio.convert(path, "flac", on_progress=lambda f: ox.progress((i + f) / len(files), path.name))
        made.append(ox.sound(out, title=path.stem))
    return made
```

- WAV, AIFF and FLAC keep the source's bit depth. Pass `bits=16` or `24` to choose, or `bits=32` for float (WAV and AIFF). See [Bit depth](#bit-depth).
- Tags, cover art, BPM, key and loop points come along wherever the new format can hold them. See [What a copy keeps](#what-a-copy-keeps).
- The new file goes in a fresh temporary folder, or in `folder=` when you give one: `folder=ox.beside(path)` puts it next to the original. It never replaces a file that's already there: `song.wav` becomes `song 2.wav`.
- `on_progress` gets the fraction of this one file, so a long conversion still moves the bar and still stops when the person presses Stop.

## Work with the samples

`ox.audio.read(path)` gives `(samples, rate)`: a float32 numpy array shaped `(frames, channels)`, from -1 to 1. `ox.audio.write(samples, rate, format)` writes it back and returns the `Path`. Add numpy to `pyproject.toml` (`mixel check --fix` does it when you import it).

```python title="app.py"
import numpy as np

import ox1audio as ox


@ox.action("Reverse")
def reverse(file: ox.Files("audio", multiple=False), fade: ox.Slider(0, 200, 5, label="Fade (ms)") = 10):
    samples, rate = ox.audio.read(file)
    backwards = samples[::-1].copy()
    n = min(int(rate * fade / 1000), len(backwards) // 2)
    if n:
        ramp = np.linspace(0, 1, n, dtype=np.float32)[:, None]
        backwards[:n] *= ramp
        backwards[-n:] *= ramp[::-1]
    out = ox.audio.write(backwards, rate, "wav", name=f"{file.stem} reversed")
    return ox.sound(out, title=f"{file.stem} reversed")
```

WAV, AIFF and FLAC are written at 24-bit unless you pass `bits=`. A one-dimensional array is written as mono.

## Levels

Samples run from -1 to 1, which is 0 dBFS. Gain, EQ, sums and separated stems often go over, and an integer or lossy file can't hold what's over: it would clip. So `ox.audio.write` refuses, and its error says by how much:

```text
ValueError: ox.audio.write: the samples peak at +5.1 dBFS, over full scale, so 24-bit WAV would clip them.
fix:  lower the gain by 5.2 dB (samples * 0.549), or write "wav" with bits=32 (float keeps overs)
```

Lower the gain yourself, by the same amount on every channel and every stem that belongs together, or write `bits=32`: 32-bit float WAV or AIFF keeps overs exactly, and any DAW reads it. Samples with NaN or infinite values are refused too. A level of exactly 1.0 is fine.

Samples in the thousands are integers, not floats: `scipy.io.wavfile` and `soundfile` with `dtype="int16"` give 32767 for full scale. The error says so and names the number to divide by. `ox.audio.read` always gives floats.

**Converting a float file that goes over.** A 32-bit float WAV can hold samples over 0 dBFS, and a copy that isn't float can't. So `convert` turns the whole file down once, by one gain, until its true peak is −1 dBTP, and tells you. For WAV, AIFF and FLAC that happens only when samples go over 0 dBFS; for MP3, AAC, Ogg and Opus, when the true peak does, because their decoders overshoot between samples. It never limits or compresses. The returned path carries `.lowered_db` and a `.note` you can show the person:

```python
out = ox.audio.convert(path, "mp3")
if out.note:
    print(out.note)   # Lowered 7 dB so it doesn't clip.
```

## Bit depth

| `bits=` | WAV, AIFF | FLAC |
| --- | --- | --- |
| `16` | 16-bit | 16-bit |
| `24` (the default for `write`) | 24-bit | 24-bit |
| `32` | 32-bit float | not possible: use WAV or AIFF |

Going to fewer bits adds TPDF dither, the standard way to keep quiet audio (fades, reverb tails) clean instead of distorted. It happens only when the depth goes down: samples that already fit the depth are written exactly, so reading a 16-bit file and writing it back at 16-bit gives the same samples. The dither is the same every run, so tests can compare outputs.

## What a copy keeps

`convert` keeps the title, artist and other tags, and these, where the format can hold them:

| | Cover | BPM | Key | Loop points |
| --- | --- | --- | --- | --- |
| WAV | yes | yes | yes | yes |
| AIFF | yes | yes | yes | yes |
| FLAC | yes | yes | yes | yes |
| MP3 | yes | yes | yes | no |
| AAC | yes | yes | no | no |
| Ogg, Opus | no | yes | yes | yes |

Every channel is kept too, in any layout (5.1, 7.1, hexagonal and the rest). MP3 and Opus hold two, so they get a stereo mix. FLAC, AAC and Ogg hold up to eight: a file with more is refused, with a message that says to choose WAV or AIFF.

## Scratch files

`ox.temp()` gives an empty folder that's removed when the app closes. `convert` and `write` already use one when you don't pass `folder=`. Files you return with `ox.sound` are copied into the Library first, so they outlive it.

## The app's own files

`ox.data` is a private folder for the app, kept between runs: presets, a downloaded model, a cache. Paths are relative, and they can't leave the folder. It needs the `data` permission.

```python title="app.py"
import json

import ox1audio as ox


@ox.action("Save a preset")
def save(name: ox.Text(placeholder="Warm vocal") = "Default", gain: ox.Slider(-12, 12, 0.5, label="Gain (dB)") = 0.0):
    ox.data.write(f"presets/{name}.json", json.dumps({"gain": gain}))
    return f"Saved {name}. You have {len(ox.data.list('presets'))} presets."
```

`ox.data.read(path)` returns bytes, `read_text` a string, `list(folder)` the names inside, and `remove(path)` deletes one. A page has the same calls as `ox.data.*`.

`mixel run` and `ox.test` don't give an app a data folder yet, so try code that uses `ox.data` with `mixel dev`.
