Bibliotecas de Áudio em Zig — Processamento e Reprodução Sonora

Bibliotecas de Áudio em Zig — Processamento e Reprodução Sonora

O processamento de áudio é uma das áreas que mais se beneficia das características do Zig: performance previsível sem garbage collector, controle fino de memória e operações de baixo nível eficientes. O ecossistema oferece bibliotecas para reprodução, gravação, processamento DSP (Digital Signal Processing) e síntese de áudio.

Por Que Zig para Áudio

Aplicações de áudio em tempo real exigem latência baixa e previsível. Qualquer pausa causada por garbage collection pode gerar artefatos audíveis (clicks, pops). O Zig elimina esse problema com gerenciamento manual de memória e performance determinística. Além disso, a interoperabilidade C permite usar bibliotecas maduras de áudio sem overhead.

mach-sysaudio — Áudio Multiplataforma

O mach-sysaudio, parte do ecossistema Mach Engine, oferece uma API de áudio de baixo nível multiplataforma:

const sysaudio = @import("mach-sysaudio");
const std = @import("std");

pub fn main() !void {
    // Inicializar contexto de áudio
    var ctx = try sysaudio.Context.init(std.heap.page_allocator, .{});
    defer ctx.deinit();

    // Listar dispositivos
    const devices = try ctx.getDevices();
    for (devices) |device| {
        std.debug.print("Dispositivo: {s} ({} canais, {} Hz)\n", .{
            device.name,
            device.channels,
            device.sample_rate,
        });
    }

    // Abrir dispositivo padrão de reprodução
    var player = try ctx.openDevice(.playback, null, .{
        .sample_rate = 44100,
        .channels = 2,
        .format = .f32,
        .callback = audioCallback,
    });
    defer player.close();

    try player.start();
    // Manter rodando...
    std.time.sleep(5 * std.time.ns_per_s);
    try player.stop();
}

// Callback de áudio - chamado pelo thread de áudio
fn audioCallback(buffer: []f32, num_frames: usize) void {
    const frequencia: f32 = 440.0; // Lá (A4)
    const sample_rate: f32 = 44100.0;

    for (0..num_frames) |i| {
        const t = @as(f32, @floatFromInt(frame_counter + i)) / sample_rate;
        const sample = @sin(2.0 * std.math.pi * frequencia * t) * 0.3;

        // Estéreo: mesmo sample nos dois canais
        buffer[i * 2] = sample;
        buffer[i * 2 + 1] = sample;
    }
    frame_counter += num_frames;
}

var frame_counter: usize = 0;

Backends do Sistema

PulseAudio/ALSA (Linux)

const c = @cImport({
    @cInclude("alsa/asoundlib.h");
});

pub fn reproduzirAlsa(dados: []const i16) !void {
    var handle: ?*c.snd_pcm_t = null;

    // Abrir dispositivo
    if (c.snd_pcm_open(&handle, "default", c.SND_PCM_STREAM_PLAYBACK, 0) < 0) {
        return error.AlsaOpenFailed;
    }
    defer _ = c.snd_pcm_close(handle);

    // Configurar parâmetros
    _ = c.snd_pcm_set_params(
        handle,
        c.SND_PCM_FORMAT_S16_LE,
        c.SND_PCM_ACCESS_RW_INTERLEAVED,
        2,     // canais
        44100, // sample rate
        1,     // soft resample
        500000, // latência em us (500ms)
    );

    // Escrever samples
    const frames = dados.len / 2; // 2 canais
    _ = c.snd_pcm_writei(handle, dados.ptr, frames);
}

CoreAudio (macOS)

const c = @cImport({
    @cInclude("AudioToolbox/AudioToolbox.h");
});

// Integração com CoreAudio via C interop
fn configurarCoreAudio() !void {
    var output_unit: c.AudioComponentInstance = undefined;
    var desc = c.AudioComponentDescription{
        .componentType = c.kAudioUnitType_Output,
        .componentSubType = c.kAudioUnitSubType_DefaultOutput,
        .componentManufacturer = c.kAudioUnitManufacturer_Apple,
        .componentFlags = 0,
        .componentFlagsMask = 0,
    };

    const component = c.AudioComponentFindNext(null, &desc);
    _ = c.AudioComponentInstanceNew(component, &output_unit);
    _ = c.AudioUnitInitialize(output_unit);
    _ = c.AudioOutputUnitStart(output_unit);
}

Processamento DSP

Filtros Digitais

const BiquadFilter = struct {
    // Coeficientes
    b0: f32, b1: f32, b2: f32,
    a1: f32, a2: f32,

    // Estado
    x1: f32 = 0, x2: f32 = 0,
    y1: f32 = 0, y2: f32 = 0,

    pub fn lowPass(freq: f32, q: f32, sample_rate: f32) BiquadFilter {
        const w0 = 2.0 * std.math.pi * freq / sample_rate;
        const alpha = @sin(w0) / (2.0 * q);
        const cos_w0 = @cos(w0);

        const a0 = 1.0 + alpha;
        return .{
            .b0 = ((1.0 - cos_w0) / 2.0) / a0,
            .b1 = (1.0 - cos_w0) / a0,
            .b2 = ((1.0 - cos_w0) / 2.0) / a0,
            .a1 = (-2.0 * cos_w0) / a0,
            .a2 = (1.0 - alpha) / a0,
        };
    }

    pub fn processar(self: *BiquadFilter, input: f32) f32 {
        const output = self.b0 * input + self.b1 * self.x1 + self.b2 * self.x2
            - self.a1 * self.y1 - self.a2 * self.y2;

        self.x2 = self.x1;
        self.x1 = input;
        self.y2 = self.y1;
        self.y1 = output;

        return output;
    }
};

FFT (Fast Fourier Transform)

fn fft(dados: []f32, n: usize) void {
    if (n <= 1) return;

    // Separar pares e ímpares
    var pares = allocator.alloc(f32, n / 2);
    var impares = allocator.alloc(f32, n / 2);

    for (0..n / 2) |i| {
        pares[i] = dados[2 * i];
        impares[i] = dados[2 * i + 1];
    }

    fft(pares, n / 2);
    fft(impares, n / 2);

    for (0..n / 2) |k| {
        const t = std.math.complex.exp(
            std.math.complex.init(0, -2.0 * std.math.pi * @as(f32, @floatFromInt(k)) / @as(f32, @floatFromInt(n)))
        );
        _ = t;
        // Butterfly operation
    }
}

Síntese de Áudio

const Oscilador = struct {
    fase: f32 = 0,
    frequencia: f32,
    sample_rate: f32,
    forma_onda: FormaOnda,

    const FormaOnda = enum { senoidal, quadrada, dente_serra, triangular };

    pub fn proximo(self: *Oscilador) f32 {
        const sample = switch (self.forma_onda) {
            .senoidal => @sin(2.0 * std.math.pi * self.fase),
            .quadrada => if (self.fase < 0.5) @as(f32, 1.0) else @as(f32, -1.0),
            .dente_serra => 2.0 * self.fase - 1.0,
            .triangular => 4.0 * @abs(self.fase - 0.5) - 1.0,
        };

        self.fase += self.frequencia / self.sample_rate;
        if (self.fase >= 1.0) self.fase -= 1.0;

        return sample;
    }
};

// Envelope ADSR
const Envelope = struct {
    attack: f32,  // segundos
    decay: f32,
    sustain: f32, // nível (0-1)
    release: f32,
    estado: enum { idle, attack, decay, sustain, release } = .idle,
    nivel: f32 = 0,
    tempo: f32 = 0,
    sample_rate: f32,

    pub fn noteOn(self: *Envelope) void {
        self.estado = .attack;
        self.tempo = 0;
    }

    pub fn noteOff(self: *Envelope) void {
        self.estado = .release;
        self.tempo = 0;
    }

    pub fn proximo(self: *Envelope) f32 {
        const dt = 1.0 / self.sample_rate;
        self.tempo += dt;

        switch (self.estado) {
            .attack => {
                self.nivel = self.tempo / self.attack;
                if (self.nivel >= 1.0) {
                    self.nivel = 1.0;
                    self.estado = .decay;
                    self.tempo = 0;
                }
            },
            .decay => {
                self.nivel = 1.0 - (1.0 - self.sustain) * (self.tempo / self.decay);
                if (self.tempo >= self.decay) {
                    self.estado = .sustain;
                }
            },
            .sustain => self.nivel = self.sustain,
            .release => {
                self.nivel = self.sustain * (1.0 - self.tempo / self.release);
                if (self.nivel <= 0) {
                    self.nivel = 0;
                    self.estado = .idle;
                }
            },
            .idle => self.nivel = 0,
        }

        return self.nivel;
    }
};

Formatos de Áudio

WAV

const WavHeader = packed struct {
    riff: [4]u8,         // "RIFF"
    file_size: u32,
    wave: [4]u8,         // "WAVE"
    fmt_chunk: [4]u8,    // "fmt "
    fmt_size: u32,
    audio_format: u16,   // 1 = PCM
    num_channels: u16,
    sample_rate: u32,
    byte_rate: u32,
    block_align: u16,
    bits_per_sample: u16,
    data_chunk: [4]u8,   // "data"
    data_size: u32,
};

pub fn lerWav(caminho: []const u8) !struct { header: WavHeader, dados: []const u8 } {
    const arquivo = try std.fs.cwd().openFile(caminho, .{});
    defer arquivo.close();

    var header: WavHeader = undefined;
    _ = try arquivo.read(std.mem.asBytes(&header));

    const dados = try allocator.alloc(u8, header.data_size);
    _ = try arquivo.read(dados);

    return .{ .header = header, .dados = dados };
}

Boas Práticas

  1. Evite alocação no thread de áudio: Pré-aloque todos os buffers
  2. Use ring buffers: Para comunicação entre threads de áudio e UI
  3. Processe em blocos: Opere em buffers de 256-1024 samples
  4. Teste com diferentes sample rates: 44100, 48000, 96000 Hz
  5. Monitore latência: Use ferramentas do sistema para verificar underruns

Próximos Passos

Explore as bibliotecas gráficas para visualização de áudio, o Mach Engine para jogos com áudio, e as bibliotecas matemáticas para DSP avançado. Consulte nossos tutoriais e receitas para projetos práticos.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.