Skip to Content
Docs

Working with audio

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

View as Markdown

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

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.

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.
  • Tags, cover art, BPM, key and loop points come along wherever the new format can hold them. See 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).

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:

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:

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

Bit depth

bits=WAV, AIFFFLAC
1616-bit16-bit
24 (the default for write)24-bit24-bit
3232-bit floatnot 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:

CoverBPMKeyLoop points
WAVyesyesyesyes
AIFFyesyesyesyes
FLACyesyesyesyes
MP3yesyesyesno
AACyesyesnono
Ogg, Opusnoyesyesyes

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.

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.

On this page