tinyalsa is a small, lightweight C library designed to simplify interaction with the Advanced Linux Sound Architecture (ALSA) in the Linux kernel.
1. ALSA and TinyALSA
What is ALSA?
ALSA (Advanced Linux Sound Architecture) is the standard framework in the Linux kernel for managing audio and MIDI hardware. It’s powerful and flexible, offering a low-level interface to sound devices. However, its native API can be quite complex due to the vast array of features and controls it exposes.
Why TinyALSA?
tinyalsa acts as a simplified wrapper around the core ALSA kernel interfaces. Its primary goal is to provide an easy-to-use API for basic operations like PCM (Pulse Code Modulation) playback and capture, and mixer control, without the overhead and complexity of the full ALSA user-space library (libasound).
2. Core Concepts and Topology of a Sound System in TinyALSA
tinyalsa models the audio hardware and its capabilities using a simple topological view:
- Sound Card (Card ID):
- Concept: Represents a physical audio device (e.g., integrated audio chip, USB sound card). A system can have multiple sound cards.
- TinyALSA’s View: Identified by a numerical card ID (typically starting from 0).
- Intuition: Each card ID is like a slot where an audio device is connected.
- PCM Device (Device ID, Playback/Capture):
- Concept: Where raw digital audio data streams into (capture/record) or out of (playback) the sound card. A single sound card can have multiple PCM devices (e.g., main output, HDMI output, microphone input).
- TinyALSA’s View: Identified by a device ID (starting from 0) within a specific sound card, and a direction (PCM_OUT for playback, PCM_IN for capture).
- Intuition: For card 0, device 0 might be your main speakers (PCM_OUT), and device 1 might be your microphone (PCM_IN).
- Mixer Device (Controls):
- Concept: Manages audio properties like volume, mute states, and input selection. These properties are called “controls.”
- TinyALSA’s View: Typically one mixer device per sound card, interacting with individual mixer_ctl elements (controls) by name or ID.
- Intuition: This is the dashboard of knobs and switches for a specific sound card.
- Subdevice (Optional):
- Concept: A further subdivision of a PCM device into independent streams. For most common uses, subdevice 0 is sufficient.
- TinyALSA’s View: Specified by a subdevice ID (defaulting to 0).
3. Working with PCM Devices (tinyalsa/pcm.h)
The pcm API is used for streaming raw audio data (playback or capture).
struct pcm_config
This structure defines the parameters for your audio stream:
struct pcm_config {
unsigned int channels; // Number of audio channels (e.g., 1 for mono, 2 for stereo)
unsigned int rate; // Sample rate in Hz (e.g., 44100, 48000)
unsigned int format; // PCM data format (e.g., PCM_FORMAT_S16_LE for signed 16-bit little-endian)
unsigned int period_size; // Number of frames per period (chunk of data)
unsigned int period_count;// Number of periods in the kernel buffer (multi-buffering)
unsigned int start_threshold; // Number of frames to play before starting (usually 0)
unsigned int stop_threshold; // Number of frames remaining to stop playback (usually 0)
unsigned int silence_threshold;// Frames to write as silence if underrun occurs (0 for no silence)
unsigned int silence_size; // Size of silence frame for silence_threshold
unsigned int avail_min; // Minimum frames in buffer to wake up (for polling)
};
- period_size (Frames per Period):
- Defines the smallest chunk of audio data transferred between your application and the ALSA driver at one time.
- A smaller period_size means more frequent transfers and potentially lower latency, but higher CPU overhead due to more interrupts.
- period_count (Number of Periods in Buffer):
- Defines how many period_size chunks make up the total circular buffer allocated by the ALSA driver in the kernel.
- This enables multiple buffering (or ring buffering). Your application fills one period while the hardware consumes another.
- A higher period_count provides a larger cushion, making audio streams more robust against system delays (preventing underruns/overruns), but increases latency. A lower period_count reduces latency but requires tighter timing from the application.
Key PCM Functions
- pcm_open(card, device, flags, config):
- Opens a PCM device for playback or capture.
- card: Sound card ID.
- device: PCM device ID on that card.
- flags: PCM_OUT for playback, PCM_IN for capture.
- config: Pointer to struct pcm_config for stream parameters.
- Returns a struct pcm* handle.
- pcm_is_ready(pcm): Checks if the PCM device was opened successfully and is ready for I/O.
- pcm_get_error(pcm): Returns a human-readable error string if pcm_is_ready returns false.
- pcm_writei(pcm, data, frames): Writes interleaved audio data to a playback PCM device.
- pcm: PCM handle.
- data: Pointer to your audio buffer.
- frames: Number of frames to write. A frame is one sample for each channel (e.g., 2 samples for stereo).
- pcm_readi(pcm, data, frames): Reads interleaved audio data from a capture PCM device.
- Parameters similar to pcm_writei.
- pcm_close(pcm): Closes the PCM device and releases resources.
PCM Playback Example
This example demonstrates playing a simple sine wave (generated on the fly) using tinyalsa.
#include <tinyalsa/pcm.h>
#include <stdio.h>
#include <math.h>
#include <stdlib.h> // For malloc, free
#define SAMPLE_RATE 44100
#define CHANNELS 2
#define DURATION_SEC 3
#define FREQUENCY 440 // A4 note
#define VOLUME 0.5
int main() {
struct pcm *pcm_handle;
struct pcm_config config;
int card = 0;
int device = 0;
short *buffer;
unsigned int num_samples = SAMPLE_RATE * DURATION_SEC * CHANNELS; // Total samples
unsigned int buffer_frames = SAMPLE_RATE / 10; // 100ms buffer, adjust period_size
unsigned int buffer_size_bytes;
// Calculate period_size and period_count based on desired total buffer duration
// For simplicity, let’s target a 10ms period, and 10 periods for a 100ms total buffer
config.period_size = SAMPLE_RATE / 100; // Frames for 10ms
config.period_count = 10; // 10 periods in the buffer
config.channels = CHANNELS;
config.rate = SAMPLE_RATE;
config.format = PCM_FORMAT_S16_LE; // Signed 16-bit, Little Endian
config.start_threshold = 0;
config.stop_threshold = 0;
config.silence_threshold = 0;
config.silence_size = 0;
config.avail_min = 0; // Don’t block for avail_min
// Calculate buffer size in bytes for one period
buffer_size_bytes = pcm_frames_to_bytes(pcm_handle, config.period_size);
buffer = (short *)malloc(buffer_size_bytes);
if (!buffer) {
fprintf(stderr, “Failed to allocate buffer\n”);
return 1;
}
// Open the PCM device for playback
pcm_handle = pcm_open(card, device, PCM_OUT, &config);
if (!pcm_handle || !pcm_is_ready(pcm_handle)) {
fprintf(stderr, “Unable to open PCM device: %s\n”, pcm_get_error(pcm_handle));
free(buffer);
return 1;
}
printf(“Playing %uHz sine wave for %u seconds…\n”, FREQUENCY, DURATION_SEC);
for (unsigned int i = 0; i < (SAMPLE_RATE * DURATION_SEC) / config.period_size; ++i) {
// Generate sine wave data for one period
for (unsigned int j = 0; j < config.period_size; ++j) {
double time = (double)(i * config.period_size + j) / SAMPLE_RATE;
short sample = (short)(VOLUME * 32767.0 * sin(2.0 * M_PI * FREQUENCY * time));
// Write interleaved stereo samples
buffer[j * CHANNELS] = sample; // Left channel
buffer[j * CHANNELS + 1] = sample; // Right channel
}
// Write the generated period to the PCM device
if (pcm_writei(pcm_handle, buffer, config.period_size) < 0) {
fprintf(stderr, “Error writing to PCM device: %s\n”, pcm_get_error(pcm_handle));
break;
}
}
printf(“Playback finished.\n”);
// Close the PCM device and free buffer
pcm_close(pcm_handle);
free(buffer);
return 0;
}
4. Working with Mixer Controls (tinyalsa/mixer.h)
The mixer API is used for controlling hardware-specific audio properties like volume, mute, input source selection, etc.
struct mixer
Represents the control panel for a specific sound card.
- mixer_open(card): Opens the mixer device for the specified card ID.
- mixer_get_num_ctls(mixer): Returns the total number of controls on this mixer.
- mixer_get_card_name(mixer): Returns the name of the sound card associated with this mixer.
- mixer_close(mixer): Closes the mixer device.
struct mixer_ctl
Represents an individual control (knob or switch) on the mixer.
- Attributes (accessed via functions):
- mixer_ctl_get_name(ctl): The human-readable name (e.g., “Master Playback Volume”, “Headphone Mute”). These names are driver-dependent and can vary.
- mixer_ctl_get_id(ctl): A numerical ID for the control.
- mixer_ctl_get_type(ctl): The type of control, crucial for interaction:
- MIXER_CTL_TYPE_BOOL: Boolean (on/off, mute/unmute).
- MIXER_CTL_TYPE_INT: Integer (volume levels, gain).
- MIXER_CTL_TYPE_ENUM: Enumerated list (input source selection).
- MIXER_CTL_TYPE_BYTE: Raw byte data.
- mixer_ctl_get_num_values(ctl): Number of values for the control (e.g., 1 for mono, 2 for stereo volume).
- mixer_ctl_get_range_min(ctl) / mixer_ctl_get_range_max(ctl): Min/Max values for integer controls.
- mixer_ctl_get_num_enums(ctl) / mixer_ctl_get_enum_string(ctl, index): For enumerated controls, retrieve the number of options and their string names.
- Key Control Functions:
- mixer_get_ctl_by_name(mixer, name): Retrieves a control by its name. This is the most common way to find a control.
- mixer_get_ctl_by_id(mixer, id): Retrieves a control by its numerical ID.
- mixer_ctl_set_value(ctl, index, value): Sets the value of a boolean or integer control. index is for multi-value controls (e.g., left/right channels).
- mixer_ctl_get_value(ctl, index): Gets the current value of a boolean or integer control.
- mixer_ctl_set_enum_by_string(ctl, string): Sets an enumerated control by its option name.
- mixer_ctl_set_enum_by_index(ctl, index): Sets an enumerated control by its option index.
- mixer_ctl_get_enum_by_index(ctl): Gets the current selected index for an enumerated control.
Mixer Control Example
This example demonstrates how to find and manipulate “Master Playback Volume” and “Master Playback Switch” (mute) controls.
#include <tinyalsa/mixer.h>
#include <stdio.h>
#include <string.h> // For strcmp
int main() {
struct mixer *mixer_handle;
struct mixer_ctl *master_vol_ctl = NULL;
struct mixer_ctl *master_mute_ctl = NULL;
unsigned int card_id = 0; // Default sound card
// 1. Open the mixer
mixer_handle = mixer_open(card_id);
if (!mixer_handle) {
fprintf(stderr, “Error: Could not open mixer for card %u\n”, card_id);
return 1;
}
printf(“Mixer for card ‘%s’ opened. Found %u controls.\n”,
mixer_get_card_name(mixer_handle), mixer_get_num_ctls(mixer_handle));
// 2. Iterate through controls to find “Master Playback Volume” and “Master Playback Switch”
// Control names can vary, so it’s often good to iterate or try common names.
unsigned int num_ctls = mixer_get_num_ctls(mixer_handle);
for (unsigned int i = 0; i < num_ctls; ++i) {
struct mixer_ctl *ctl = mixer_get_ctl_by_id(mixer_handle, i);
if (ctl) {
const char *ctl_name = mixer_ctl_get_name(ctl);
if (strcmp(ctl_name, “Master Playback Volume”) == 0 || strcmp(ctl_name, “Master”) == 0) {
master_vol_ctl = ctl;
} else if (strcmp(ctl_name, “Master Playback Switch”) == 0 || strcmp(ctl_name, “Master Mute”) == 0) {
master_mute_ctl = ctl;
}
}
}
// 3. Set Master Playback Volume to 50%
if (master_vol_ctl && mixer_ctl_get_type(master_vol_ctl) == MIXER_CTL_TYPE_INT) {
long min_vol = mixer_ctl_get_range_min(master_vol_ctl);
long max_vol = mixer_ctl_get_range_max(master_vol_ctl);
long target_vol = (long)(0.50 * (max_vol – min_vol) + min_vol); // 50% of the range
printf(“Setting Master Playback Volume to 50%% (raw value %ld)…\n”, target_vol);
unsigned int num_values = mixer_ctl_get_num_values(master_vol_ctl);
for (unsigned int i = 0; i < num_values; ++i) {
mixer_ctl_set_value(master_vol_ctl, i, target_vol);
}
printf(“Current Master Playback Volume: %ld (first channel)\n”, mixer_ctl_get_value(master_vol_ctl, 0));
} else {
fprintf(stderr, “Warning: Master Playback Volume control not found or not integer type.\n”);
}
// 4. Mute and then Unmute Master output
if (master_mute_ctl && mixer_ctl_get_type(master_mute_ctl) == MIXER_CTL_TYPE_BOOL) {
printf(“Muting Master Playback…\n”);
// Set to Muted (value 1)
mixer_ctl_set_value(master_mute_ctl, 0, 1);
printf(“Master Playback Muted: %ld\n”, mixer_ctl_get_value(master_mute_ctl, 0));
// Sleep briefly to observe the mute effect (not actual delay, just for demo concept)
// You’d use proper sleep functions in real code, or listen for events.
printf(“Waiting 2 seconds…\n”);
// sleep(2); // In a real app, use unistd.h sleep or similar
printf(“Unmuting Master Playback…\n”);
// Set to Unmuted (value 0)
mixer_ctl_set_value(master_mute_ctl, 0, 0);
printf(“Master Playback Muted: %ld\n”, mixer_ctl_get_value(master_mute_ctl, 0));
} else {
fprintf(stderr, “Warning: Master Playback Switch control not found or not boolean type.\n”);
}
// 5. Close the mixer
mixer_close(mixer_handle);
printf(“Mixer closed.\n”);
return 0;
}
Leave a comment