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
- Evite alocação no thread de áudio: Pré-aloque todos os buffers
- Use ring buffers: Para comunicação entre threads de áudio e UI
- Processe em blocos: Opere em buffers de 256-1024 samples
- Teste com diferentes sample rates: 44100, 48000, 96000 Hz
- 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.