---
title: Load images and audio for inference
description: "Decode image files into [H, W, C] tensors and audio into resampled waveforms, then make them model-ready with ops."
---

Vision and audio models consume tensors, and `ClikaRT::io` gets you there from the files themselves: `load_image` decodes the common encoded formats (JPEG, PNG, GIF, BMP, TGA, PSD, HDR, PNM, PIC) into an `[H, W, C]` tensor, and `load_audio` decodes WAV, FLAC, MP3 and OGG (Vorbis) into a `Float32` waveform, resampling and remixing to the layout you name. Both also take an in-memory byte span, so the same calls serve an upload handler as well as a file path. A file outside those sets is refused by name (`Status::Unsupported`, code `NOT_IMPLEMENTED`): `image decode: photo.webp: WebP is not supported; this build decodes JPEG, PNG, ...`; a damaged file, or a file of the other family, is refused as `InvalidArgument` with the file and the container it carries named, so a batch with one bad input says which one. The write direction ships too: `encode_audio` and `save_audio` turn a waveform back into a WAV, and `save_image` writes PNG by default or JPEG, BMP and TGA when the path's extension asks for them ([Write audio back](#write-audio-back)).

The runs below use a small RGB PNG (`input.png`, 8x6, a color gradient) and a half-second stereo WAV (`clip.wav`, 8 kHz, a 440 Hz tone on the left channel); any image or audio file of yours works the same.

## Decode an image and make it model-ready

`peek_image` reads only the header, the cheap way to validate and route before decoding. `load_image` decodes to `[H, W, C]` with 8-bit pixels; `requested_channels` forces a channel count (3 collapses an alpha channel or expands grayscale, so one call normalizes mixed inputs). The rest of the preprocessing every vision model wants (float, scale, `CHW`, batch dimension) is three ops.

<LangTabs>

<LangTab value="cpp">

```cpp title="image_to_input.cpp"
int main() {
    const std::string input = input_png();

    const io::ImageInfo info = io::peek_image(input);
    std::printf("header: %lldx%lld, %lld channel(s)\n", static_cast<long long>(info.width),
                static_cast<long long>(info.height), static_cast<long long>(info.channels));

    const Tensor img = io::load_image(input, /*requested_channels=*/3);
    std::printf("decoded: %s\n", img.to_string().c_str());

    // Model-ready: float in [0,1], channels first, leading batch dim.
    const Tensor x = ops::unsqueeze(
        ops::permute(img.to(DataType::Float32) * (1.0 / 255.0), {2, 0, 1}), 0);
    std::printf("input:   %s\n", x.to_string().c_str());
    std::printf("mean pixel value: %.4f\n", ops::mean(x).item<float>());
    return 0;
}
```

</LangTab>

<LangTab value="python">

The image and audio decoders are part of the C++ API today; the C++ tab
shows both. From python, a decoded image or waveform enters as a numpy
array through `crt.tensor(array)`; the processor module then carries the
resize/normalize and feature steps.

</LangTab>

</LangTabs>

```text
header: 8x6, 3 channel(s)
decoded: Tensor(shape=[6, 8, 3], dtype=UInt8, device=CPU, numel=144, data=[0, 0, 128, 32, 0, 128, ...])
input:   Tensor(shape=[1, 3, 6, 8], dtype=Float32, device=CPU, numel=144, data=[0, 0.1255, 0.251, 0.3765, 0.502, 0.6275, ...])
mean pixel value: 0.4444
```

The decoded tensor is ordinary compute-engine currency from that point on: `.to(device)` moves it, and a model's forward takes it as-is. For bytes already in memory (an HTTP upload, an asset in an archive), pass a `Span<const std::uint8_t>` instead of the path; the `http_server` example's `06_image_compute` chapter runs exactly that flow behind an upload endpoint.

## Decode audio at the model's sample rate

Audio checkpoints are trained at a fixed sample rate and channel count, and `load_audio` meets them at the file: `target_sample_rate` resamples and `target_channels` remixes during the decode, so the tensor that comes out is already the model's input layout. The result is an `AudioData`: `samples` (`Float32`, `[frames]` mono or `[frames, channels]`) plus the effective `AudioInfo`.

<LangTabs>

<LangTab value="cpp">

```cpp title="audio_to_input.cpp"
int main() {
    const std::string clip = clip_wav();

    // Whatever the file's native layout, ask for 16 kHz mono.
    const io::AudioData audio = io::load_audio(clip, /*target_sample_rate=*/16000,
                                               /*target_channels=*/1);
    std::printf("decoded: %d Hz, %d channel(s), %lld frames (%.2f s)\n",
                audio.info.sample_rate, audio.info.channels,
                static_cast<long long>(audio.info.frames),
                static_cast<double>(audio.info.frames) / audio.info.sample_rate);
    std::printf("samples: %s\n", audio.samples.to_string().c_str());

    // Loudness check: RMS over the waveform.
    const Tensor rms = ops::sqrt(ops::mean(audio.samples * audio.samples));
    std::printf("rms = %.4f\n", rms.item<float>());
    return 0;
}
```

</LangTab>

<LangTab value="python">

This step is part of the C++ API today; the C++ tab shows it.

</LangTab>

</LangTabs>

```text
decoded: 16000 Hz, 1 channel(s), 8000 frames (0.50 s)
samples: Tensor(shape=[8000], dtype=Float32, device=CPU, numel=8000, data=[0.0008917, 0.02581, 0.06115, 0.09333, 0.1175, 0.1378, ...])
rms = 0.1295
```

Passing 0 for either target keeps the file's native rate or channel count, and `peek_audio`'s `AudioInfo` answers "what would this decode produce" without decoding. From here the waveform feeds a feature front end (`ops::` has the FFT-adjacent reductions and windowed views a spectrogram needs) or goes straight into a model that consumes raw samples.

## Write audio back

The write direction is two calls: `encode_audio(samples, sample_rate)` returns the PCM16 WAV bytes of a waveform, and `save_audio(samples, sample_rate, path)` writes the same bytes to a file. A `Float16`, `BFloat16`, `Float32` or `Float64` waveform converts to 16-bit PCM; `Int16` is written as is. `[frames]` is mono and `[frames, channels]` is interleaved multi-channel, the same layouts the decoder produces, so a model's synthesized speech goes to disk with one call and an HTTP handler returns the encoded bytes without touching a file. The path's extension is the format request: `.wav` (or no extension) writes; a name asking for a container the writer does not produce (`reply.mp3`, `reply.ogg`) is refused by name with `Status::Unsupported` and nothing is written, so WAV bytes never land under a foreign suffix.

<LangTabs>

<LangTab value="cpp">

```cpp title="audio_write.cpp"
#include <cstdio>
#include <vector>

#include <ClikaRT/clika_rt.h>

using ClikaRT::DataType;
using ClikaRT::Tensor;
namespace io = ClikaRT::io;
namespace ops = ClikaRT::ops;

int main() {
    // Half a second of a 440 Hz tone at 16 kHz: t = frame / rate,
    // s = 0.5 * sin(2 pi f t), a Float32 waveform in [-1, 1].
    const int rate = 16000;
    const Tensor t = ops::arange(0.0, 8000.0, 1.0, DataType::Float32) * (1.0 / rate);
    const Tensor tone = ops::sin(t * (2.0 * 3.14159265358979 * 440.0)) * 0.5;

    // encode_audio returns the PCM16 WAV bytes; save_audio writes the same
    // bytes to a file.
    const std::vector<unsigned char> wav_bytes = io::encode_audio(tone, rate);
    std::printf("encoded: %zu bytes of RIFF/WAVE\n", wav_bytes.size());

    io::save_audio(tone, rate, "tone.wav");

    // Round trip: decode what was written and check the layout survived.
    const io::AudioData back = io::load_audio("tone.wav", rate, /*target_channels=*/1);
    std::printf("reloaded: %d Hz, %d channel(s), %lld frames\n",
                back.info.sample_rate, back.info.channels,
                static_cast<long long>(back.info.frames));
    const Tensor rms = ops::sqrt(ops::mean(back.samples * back.samples));
    std::printf("rms = %.4f\n", rms.item<float>());
    return 0;
}
```

</LangTab>

<LangTab value="python">

```python title="audio_write.py"
import math

import clika_runtime as crt

# Half a second of a 440 Hz tone at 16 kHz, a Float32 waveform in [-1, 1].
rate = 16000
t = crt.arange(0.0, 8000.0, 1.0) * (1.0 / rate)
tone = crt.sin(t * (2.0 * math.pi * 440.0)) * 0.5

# encode_audio returns the PCM16 WAV bytes; save_audio writes them to a file.
wav_bytes = crt.io.encode_audio(tone, rate)
print(f"encoded: {len(wav_bytes)} bytes of RIFF/WAVE")

crt.io.save_audio(tone, rate, "tone.wav")

# Round trip: decode what was written and check the layout survived.
# load_audio returns (samples, sample_rate, channels, frames).
samples, sample_rate, channels, frames = crt.io.load_audio("tone.wav", rate, 1)
print(f"reloaded: {sample_rate} Hz, {channels} channel(s), {frames} frames")
rms = crt.sqrt((samples * samples).mean())
print(f"rms = {rms.item():.4f}")
```

</LangTab>

</LangTabs>

```text
encoded: 16044 bytes of RIFF/WAVE
reloaded: 16000 Hz, 1 channel(s), 8000 frames
rms = 0.3535
```

The byte count is the 8000 frames as 16-bit PCM plus the 44-byte RIFF header, and the round-trip RMS is the tone's own (0.5 amplitude over root two).

Decoding gives you pixels and samples; the model-specific half (resize, normalize, log-mel) is [the processors guide](preprocess-with-processors.mdx). Weights travel the same road in [the GGUF guide](load-quantized-weights.mdx); the bundle's `io` example walks NumPy and safetensors round-trips in its earlier chapters.
