ALSA 서브시스템 심화
Advanced Linux Sound Architecture (ALSA)는 리눅스 커널의 핵심 오디오 서브시스템으로, 사운드 카드 드라이버, PCM 재생/녹음, MIDI, 믹서 제어, 타이머 등 오디오 인프라 전반을 제공합니다. 이 문서는 ALSA Core API, PCM 서브시스템, Control 인터페이스, ASoC (ALSA System on Chip) 프레임워크, DAPM (Dynamic Audio Power Management), DPCM (Dynamic PCM), HD Audio, USB Audio, Sound Open Firmware (SOF), Compress Offload, 전원 관리, DMA 버퍼 관리, 지연시간 최적화, 가상화 환경 오디오까지 ALSA의 모든 측면을 심층적으로 다룹니다.
- Intel HD Audio Specification: 고품질 오디오 버스 표준
- USB Audio Class (UAC) 1.0/2.0/3.0: USB 오디오 디바이스 프로토콜
- I2S/TDM/PDM: 디지털 오디오 인터페이스 프로토콜
- AC'97/I2C/SPI: 코덱 제어 버스
- Device Tree Bindings: ASoC 디바이스 설정
ALSA 아키텍처 개요
OSS에서 ALSA로
초기 리눅스는 OSS (Open Sound System)를 사운드 서브시스템으로 사용했으나, 라이선스 제약과 기능 한계로 인해 커널 2.5/2.6 시절 ALSA로 전환되었습니다. ALSA는 다음과 같은 개선사항을 제공합니다:
- 모듈화된 디자인: PCM, Control, MIDI, Timer 등 독립적 서브시스템
- 하드웨어 믹싱 지원: 다중 스트림 동시 재생/녹음
- 샘플레이트/포맷 자동 변환: 플러그인 레이어 (dmix, dsnoop, plug)
- 고급 라우팅: 복잡한 오디오 파이프라인 구성
- 임베디드 지원: ASoC (System on Chip) 프레임워크
사용자 공간 스택
현대 리눅스 오디오 스택은 커널 ALSA 위에 여러 계층으로 구성됩니다:
| 계층 | 컴포넌트 | 역할 |
|---|---|---|
| 애플리케이션 | Firefox, Chrome, VLC, OBS | 최종 오디오 소비자/생산자 |
| 세션 매니저 | PipeWire, PulseAudio, JACK | 오디오 라우팅, 믹싱, 재샘플링 |
| 라이브러리 | libasound (alsa-lib) | 커널 ALSA API 래퍼 |
| 커널 | ALSA 드라이버 | 하드웨어 제어 |
커널 구조
ALSA 커널 코드는 sound/ 디렉터리 아래 다음과 같이 구성됩니다:
| 디렉터리 | 설명 | 주요 파일 |
|---|---|---|
sound/core/ |
ALSA 코어 (PCM, Control, Timer 등) | pcm.c, control.c, init.c, device.c |
sound/soc/ |
ASoC 프레임워크 | soc-core.c, soc-dapm.c, soc-pcm.c |
sound/pci/ |
PCI 사운드 카드 드라이버 | intel8x0.c, emu10k1/, via82xx.c |
sound/pci/hda/ |
HD Audio 드라이버 | hda_intel.c, hda_codec.c, patch_realtek.c |
sound/usb/ |
USB Audio 드라이버 | card.c, pcm.c, mixer.c, quirks.c |
sound/soc/sof/ |
Sound Open Firmware | core.c, ipc.c, topology.c |
sound/soc/codecs/ |
코덱 드라이버 (I2S/I2C) | wm8731.c, rt5640.c, da7219.c |
sound/drivers/ |
가상/테스트 드라이버 | dummy.c, aloop.c, virmidi.c |
디바이스 노드
ALSA는 /dev/snd/ 아래 문자 디바이스 노드를 생성합니다:
| 노드 패턴 | 타입 | 설명 |
|---|---|---|
/dev/snd/controlCN |
Control | 카드 N의 믹서/컨트롤 인터페이스 |
/dev/snd/pcmCNDMp |
PCM Playback | 카드 N, 디바이스 M의 재생 스트림 |
/dev/snd/pcmCNDMc |
PCM Capture | 카드 N, 디바이스 M의 녹음 스트림 |
/dev/snd/hwCNDM |
HW Dep | 하드웨어 종속 인터페이스 (펌웨어 업로드 등) |
/dev/snd/midiCNDM |
Raw MIDI | MIDI 입출력 포트 |
/dev/snd/seq |
Sequencer | MIDI 시퀀서 (가상 라우팅) |
/dev/snd/timer |
Timer | 고정밀 타이머 |
예: /dev/snd/pcmC0D0p는 카드 0번, PCM 디바이스 0번의 재생(playback) 스트림을 의미합니다.
전체 아키텍처 다이어그램
/dev/snd/* ioctl → ALSA Core → 드라이버 (ASoC/HDA/USB) → 하드웨어 (Codec/DMA)
snd_card 심화
struct snd_card는 ALSA의 최상위 추상화로, 하나의 사운드 카드 인스턴스를 나타냅니다. 모든 PCM 디바이스, 컨트롤, MIDI 포트는 카드에 종속됩니다.
snd_card 구조체
/* include/sound/core.h */
struct snd_card {
int number; /* 카드 번호 (0, 1, 2...) */
char id[16]; /* /proc/asound/id에 표시될 ID */
char driver[16]; /* 드라이버 이름 */
char shortname[32]; /* 짧은 설명 */
char longname[80]; /* 긴 설명 (하드웨어 정보) */
char mixername[80]; /* 믹서 이름 */
char components[128]; /* 컴포넌트 문자열 */
struct module *module; /* 소유 모듈 */
void *private_data; /* 드라이버 전용 데이터 */
void (*private_free)(struct snd_card *card);
struct list_head devices; /* snd_device 리스트 */
struct list_head controls; /* snd_kcontrol 리스트 */
struct list_head ctl_files; /* 열린 control 파일 */
unsigned int last_numid; /* 마지막 할당된 control numid */
struct rw_semaphore controls_rwsem; /* control 잠금 */
rwlock_t ctl_files_rwlock; /* ctl_files 잠금 */
int user_ctl_count; /* 사용자 정의 control 개수 */
struct snd_info_entry *proc_root; /* /proc/asound/cardN/ */
struct snd_info_entry *proc_id; /* /proc/asound/cardN/id */
unsigned int power_state; /* PM 상태 */
struct wait_queue_head power_sleep; /* PM wait queue */
#ifdef CONFIG_PM
unsigned int power_on:1; /* 전원 켜짐 플래그 */
struct delayed_work power_work; /* 전원 관리 워크큐 */
#endif
struct device *dev; /* 디바이스 포인터 (PCI/USB/Platform) */
struct device card_dev; /* 카드 디바이스 객체 */
bool registered; /* snd_card_register() 호출 여부 */
bool sync_irq; /* 동기 IRQ 모드 */
atomic_t usage_count; /* 참조 카운트 */
wait_queue_head_t remove_sleep; /* 제거 대기 큐 */
};
카드 생명주기
ALSA 드라이버는 다음 단계로 카드를 생성/등록/해제합니다:
/* 1. 카드 생성 */
static int mydriver_probe(struct pci_dev *pci,
const struct pci_device_id *id)
{
struct snd_card *card;
struct my_chip *chip;
int err;
/* 카드 생성: idx는 모듈 파라미터, id는 /proc ID */
err = snd_card_new(&pci->dev, index[dev], id[dev],
THIS_MODULE, sizeof(*chip), &card);
if (err < 0)
return err;
chip = card->private_data;
chip->card = card;
chip->pci = pci;
/* 2. 하드웨어 초기화 */
err = my_chip_create(card, pci, &chip);
if (err < 0)
goto error;
/* 3. PCM 디바이스 생성 */
err = snd_pcm_new(card, "MyPCM", 0, 1, 1, &chip->pcm);
if (err < 0)
goto error;
snd_pcm_set_ops(chip->pcm, SNDRV_PCM_STREAM_PLAYBACK,
&my_playback_ops);
snd_pcm_set_ops(chip->pcm, SNDRV_PCM_STREAM_CAPTURE,
&my_capture_ops);
chip->pcm->private_data = chip;
strcpy(chip->pcm->name, "My PCM");
/* 4. DMA 버퍼 할당 (managed) */
snd_pcm_set_managed_buffer_all(chip->pcm,
SNDRV_DMA_TYPE_DEV,
&pci->dev,
64 * 1024, 256 * 1024);
/* 5. Control (믹서) 추가 */
err = my_create_controls(card);
if (err < 0)
goto error;
/* 6. 카드 문자열 설정 */
strcpy(card->driver, "MyDriver");
strcpy(card->shortname, "My Audio Card");
sprintf(card->longname, "%s at 0x%lx irq %d",
card->shortname, chip->port, chip->irq);
/* 7. 카드 등록 (사용자 공간에 노출) */
err = snd_card_register(card);
if (err < 0)
goto error;
pci_set_drvdata(pci, card);
return 0;
error:
snd_card_free(card); /* 에러 시 정리 */
return err;
}
/* 8. 카드 제거 */
static void mydriver_remove(struct pci_dev *pci)
{
struct snd_card *card = pci_get_drvdata(pci);
/* snd_card_free()는 자동으로 모든 리소스 해제:
* - 등록된 디바이스 제거
* - control 해제
* - proc 엔트리 제거
* - private_free() 콜백 호출
* - DMA 버퍼 해제 (managed인 경우) */
snd_card_free(card);
}
snd_card_register() 호출 전까지 디바이스는 사용자 공간에 노출되지 않습니다. 모든 컴포넌트 (PCM, Control 등)를 등록 전에 준비해야 합니다.
snd_device: 컴포넌트 시스템
ALSA는 snd_device 메커니즘으로 카드 내 컴포넌트를 관리합니다. PCM, Control, MIDI 등은 모두 snd_device로 등록되어 통합 생명주기를 따릅니다.
/* include/sound/core.h */
enum snd_device_type {
SNDRV_DEV_LOWLEVEL, /* 저수준 하드웨어 */
SNDRV_DEV_CONTROL, /* Control 인터페이스 */
SNDRV_DEV_PCM, /* PCM */
SNDRV_DEV_RAWMIDI, /* Raw MIDI */
SNDRV_DEV_TIMER, /* Timer */
SNDRV_DEV_SEQUENCER, /* Sequencer */
SNDRV_DEV_HWDEP, /* Hardware dependent */
SNDRV_DEV_COMPRESS, /* Compress offload */
};
struct snd_device_ops {
int (*dev_register)(struct snd_device *dev);
int (*dev_disconnect)(struct snd_device *dev);
int (*dev_free)(struct snd_device *dev);
};
/* 디바이스 등록 */
int snd_device_new(struct snd_card *card,
enum snd_device_type type,
void *device_data,
const struct snd_device_ops *ops);
예: 커스텀 하드웨어 초기화:
static int my_chip_dev_free(struct snd_device *device)
{
struct my_chip *chip = device->device_data;
/* 하드웨어 정리 */
if (chip->irq >= 0)
free_irq(chip->irq, chip);
iounmap(chip->iobase);
pci_release_regions(chip->pci);
kfree(chip);
return 0;
}
static const struct snd_device_ops my_chip_ops = {
.dev_free = my_chip_dev_free,
};
static int my_chip_create(struct snd_card *card,
struct pci_dev *pci,
struct my_chip **rchip)
{
struct my_chip *chip;
int err;
chip = kzalloc(sizeof(*chip), GFP_KERNEL);
if (!chip)
return -ENOMEM;
chip->card = card;
chip->pci = pci;
/* PCI 리소스 획득 */
err = pci_enable_device(pci);
if (err < 0) {
kfree(chip);
return err;
}
err = pci_request_regions(pci, "mydriver");
if (err < 0) {
pci_disable_device(pci);
kfree(chip);
return err;
}
chip->port = pci_resource_start(pci, 0);
chip->iobase = pci_ioremap_bar(pci, 0);
/* IRQ 할당 */
if (request_irq(pci->irq, my_interrupt, IRQF_SHARED,
KBUILD_MODNAME, chip)) {
dev_err(&pci->dev, "cannot grab IRQ %d\n", pci->irq);
my_chip_dev_free((struct snd_device){.device_data = chip});
return -EBUSY;
}
chip->irq = pci->irq;
/* 하드웨어 초기화 */
my_chip_init_hardware(chip);
/* snd_device로 등록 (snd_card_free() 시 자동 정리) */
err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, chip,
&my_chip_ops);
if (err < 0) {
my_chip_dev_free((struct snd_device){.device_data = chip});
return err;
}
*rchip = chip;
return 0;
}
/proc/asound/ 인터페이스
ALSA는 /proc/asound/에 디버깅/진단 정보를 노출합니다:
/proc/asound/
├── cards # 모든 카드 요약
├── devices # 전체 디바이스 리스트
├── timers # 타이머 리스트
├── pcm # PCM 디바이스 정보
├── card0/ # 카드 0번
│ ├── id # 카드 ID 문자열
│ ├── pcm0p/ # PCM 디바이스 0번 playback
│ │ ├── info # PCM 하드웨어 정보
│ │ ├── sub0/ # 서브스트림 0번
│ │ │ ├── info
│ │ │ ├── hw_params
│ │ │ ├── sw_params
│ │ │ └── status
│ ├── codec#0 # HD Audio 코덱 (HDA인 경우)
│ └── eld#0.0 # HDMI ELD (HDA HDMI)
└── card1/
드라이버는 커스텀 proc 엔트리를 추가할 수 있습니다:
/* 커스텀 /proc 파일 생성 */
static void my_proc_read(struct snd_info_entry *entry,
struct snd_info_buffer *buffer)
{
struct my_chip *chip = entry->private_data;
snd_iprintf(buffer, "Chip Status:\n");
snd_iprintf(buffer, " IRQ: %d\n", chip->irq);
snd_iprintf(buffer, " Port: 0x%lx\n", chip->port);
snd_iprintf(buffer, " Sample Rate: %d Hz\n",
chip->current_rate);
}
static void my_proc_init(struct my_chip *chip)
{
struct snd_info_entry *entry;
if (!snd_card_proc_new(chip->card, "my_status", &entry))
snd_info_set_text_ops(entry, chip, my_proc_read);
}
결과: /proc/asound/card0/my_status 파일에 상태 정보 표시.
PCM 서브시스템 심화
PCM (Pulse Code Modulation) 서브시스템은 ALSA의 핵심으로, 디지털 오디오 스트림의 재생과 녹음을 담당합니다. PCM은 복잡한 상태 머신, 하드웨어 제약 시스템, DMA 버퍼 관리를 포함합니다.
PCM 기본 개념
PCM 오디오는 다음 파라미터로 정의됩니다:
- Sample (샘플): 단일 시점의 오디오 값 (16-bit, 24-bit, 32-bit 등)
- Frame (프레임): 모든 채널의 샘플 집합. 스테레오 16-bit = 4 bytes/frame (2채널 × 2바이트)
- Sample Rate (샘플레이트): 초당 프레임 수 (44100 Hz, 48000 Hz, 96000 Hz 등)
- Period (피리어드): 인터럽트 단위. 하나의 DMA 버퍼 조각 (예: 64 frames)
- Buffer (버퍼): 전체 링 버퍼 크기 (예: 4 periods = 256 frames)
Byte Rate = Sample Rate × Channels × Bytes per Sample
예: 48000 Hz × 2 ch × 2 bytes = 192,000 bytes/sec = 192 KB/s
Time Axis:
[Frame 0][Frame 1][Frame 2][Frame 3]... (44100 frames/sec for 44.1kHz)
Stereo Frame (16-bit):
[Left Sample: 2 bytes][Right Sample: 2 bytes] = 4 bytes total
Buffer Layout (4 periods, 64 frames/period):
[Period 0: 64 frames][Period 1: 64 frames][Period 2: 64 frames][Period 3: 64 frames]
└─ IRQ after DMA └─ IRQ after DMA └─ IRQ after DMA └─ IRQ after DMA
핵심 PCM 구조체
/* sound/core/pcm.c */
struct snd_pcm {
struct snd_card *card;
int device; /* PCM 디바이스 번호 */
char id[64]; /* ID 문자열 */
char name[80]; /* 이름 */
struct snd_pcm_str streams[2]; /* PLAYBACK, CAPTURE */
struct mutex open_mutex; /* open 동기화 */
struct wait_queue_head open_wait;
void *private_data;
void (*private_free)(struct snd_pcm *pcm);
bool internal; /* 내부 전용 (loopback 등) */
bool nonatomic; /* non-atomic 컨텍스트 콜백 허용 */
};
struct snd_pcm_str {
int stream; /* SNDRV_PCM_STREAM_PLAYBACK/CAPTURE */
struct snd_pcm *pcm;
unsigned int substream_count; /* 서브스트림 개수 */
struct snd_pcm_substream *substream; /* 서브스트림 리스트 */
struct snd_info_entry *proc_root;
struct snd_kcontrol *chmap_kctl; /* 채널맵 control */
};
struct snd_pcm_substream {
struct snd_pcm *pcm;
struct snd_pcm_str *pstr;
int number; /* 서브스트림 번호 */
char name[32]; /* 서브스트림 이름 */
struct snd_pcm_runtime *runtime; /* 런타임 상태 (open 시 할당) */
struct snd_pcm_ops *ops; /* 드라이버 콜백 */
unsigned int dma_buffer_p:1; /* DMA 버퍼 사전 할당 여부 */
unsigned int no_rewinds:1; /* rewind 금지 */
struct snd_pcm_group self_group; /* 링크 그룹 (동기화) */
struct snd_pcm_group *group;
void *private_data;
struct snd_pcm_substream *next; /* 다음 서브스트림 */
};
struct snd_pcm_runtime {
/* 상태 */
snd_pcm_state_t state; /* OPEN, SETUP, PREPARED, RUNNING, XRUN... */
snd_pcm_substate_t suspended_state;
/* HW 파라미터 */
struct snd_pcm_hardware hw; /* 하드웨어 제약 */
struct snd_pcm_hw_constraints hw_constraints;
unsigned int rate; /* 샘플레이트 (Hz) */
unsigned int channels; /* 채널 수 */
snd_pcm_format_t format; /* 샘플 포맷 */
unsigned int frame_bits; /* frame 크기 (bits) */
snd_pcm_uframes_t period_size; /* period 크기 (frames) */
unsigned int periods; /* period 개수 */
snd_pcm_uframes_t buffer_size; /* 버퍼 크기 (frames) */
/* DMA 버퍼 */
struct snd_dma_buffer *dma_buffer_p;
unsigned char *dma_area; /* DMA 버퍼 가상 주소 */
dma_addr_t dma_addr; /* DMA 버퍼 물리 주소 */
size_t dma_bytes; /* DMA 버퍼 크기 (bytes) */
/* 포인터 관리 */
snd_pcm_uframes_t hw_ptr_base; /* HW 포인터 베이스 */
snd_pcm_uframes_t hw_ptr_wrap; /* HW 포인터 wrap 카운터 */
snd_pcm_uframes_t control->appl_ptr; /* 애플리케이션 포인터 */
snd_pcm_uframes_t control->avail_min; /* wake-up threshold */
/* 타이밍 */
snd_pcm_uframes_t delay; /* 지연 (frames) */
u64 hw_ptr_jiffies; /* 마지막 HW 포인터 업데이트 */
/* Wait queues */
wait_queue_head_t sleep; /* mmap I/O 대기 큐 */
wait_queue_head_t tsleep; /* 기타 대기 큐 */
/* 콜백 */
void (*transfer_ack_begin)(struct snd_pcm_substream *);
void (*transfer_ack_end)(struct snd_pcm_substream *);
/* Private data */
void *private_data;
void (*private_free)(struct snd_pcm_runtime *);
};
/* 하드웨어 기능 설명 */
struct snd_pcm_hardware {
unsigned int info; /* SNDRV_PCM_INFO_* 플래그 */
u64 formats; /* 지원 포맷 비트마스크 */
u64 subformats; /* 서브포맷 (MSBF 등) */
unsigned int rates; /* SNDRV_PCM_RATE_* 플래그 */
unsigned int rate_min; /* 최소 샘플레이트 */
unsigned int rate_max; /* 최대 샘플레이트 */
unsigned int channels_min; /* 최소 채널 */
unsigned int channels_max; /* 최대 채널 */
size_t buffer_bytes_max; /* 최대 버퍼 크기 */
size_t period_bytes_min; /* 최소 period 크기 */
size_t period_bytes_max; /* 최대 period 크기 */
unsigned int periods_min; /* 최소 period 개수 */
unsigned int periods_max; /* 최대 period 개수 */
size_t fifo_size; /* FIFO 크기 (frames, 지연 보정용) */
};
snd_pcm_ops: 드라이버 콜백
드라이버는 snd_pcm_ops 구조체를 통해 PCM 동작을 구현합니다:
/* include/sound/pcm.h */
struct snd_pcm_ops {
/* 필수 콜백 */
int (*open)(struct snd_pcm_substream *substream);
int (*close)(struct snd_pcm_substream *substream);
int (*ioctl)(struct snd_pcm_substream *substream,
unsigned int cmd, void *arg);
int (*hw_params)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params);
int (*hw_free)(struct snd_pcm_substream *substream);
int (*prepare)(struct snd_pcm_substream *substream);
int (*trigger)(struct snd_pcm_substream *substream, int cmd);
snd_pcm_uframes_t (*pointer)(struct snd_pcm_substream *substream);
/* 선택적 콜백 */
int (*sync_stop)(struct snd_pcm_substream *substream);
int (*ack)(struct snd_pcm_substream *substream); /* appl_ptr 업데이트 통지 */
int (*copy)(struct snd_pcm_substream *substream, int channel,
unsigned long pos, void *buf, unsigned long count);
int (*fill_silence)(struct snd_pcm_substream *substream, int channel,
unsigned long pos, unsigned long count);
struct page *(*page)(struct snd_pcm_substream *substream,
unsigned long offset);
int (*mmap)(struct snd_pcm_substream *substream,
struct vm_area_struct *vma);
};
콜백 설명:
open: 서브스트림 열 때 호출.runtime->hw초기화, 하드웨어 제약 설정.close: 서브스트림 닫을 때 호출. 리소스 해제.hw_params: 사용자가 포맷/샘플레이트/버퍼 크기 설정 시 호출. DMA 버퍼 할당, 하드웨어 레지스터 구성.hw_free:hw_params해제 시 호출. DMA 버퍼 해제.prepare: 스트림 시작 전 호출. 하드웨어 초기화, 포인터 리셋.trigger: START/STOP/PAUSE/RESUME 명령 시 호출. DMA 시작/중지.pointer: 현재 HW 포인터 반환 (frames 단위). 주기적으로 호출됨.ack: appl_ptr 업데이트 시 통지. DSP 펌웨어에 새 데이터 알림.copy: 커스텀 데이터 복사 (DMA가 아닌 MMIO 등). 제공 안 하면 memcpy 사용.
하드웨어 제약 시스템
ALSA는 복잡한 제약 시스템으로 지원 가능한 파라미터 조합을 제한합니다. 예: "샘플레이트가 96kHz면 채널은 최대 2개", "period 크기는 64의 배수".
/* open 콜백에서 제약 설정 */
static int my_pcm_open(struct snd_pcm_substream *substream)
{
struct my_chip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
int err;
/* 하드웨어 기능 설정 */
runtime->hw = (struct snd_pcm_hardware) {
.info = SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_MMAP_VALID |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_BLOCK_TRANSFER |
SNDRV_PCM_INFO_PAUSE,
.formats = SNDRV_PCM_FMTBIT_S16_LE |
SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
.rates = SNDRV_PCM_RATE_44100 | SNDRV_PCM_RATE_48000 |
SNDRV_PCM_RATE_96000 | SNDRV_PCM_RATE_192000,
.rate_min = 44100,
.rate_max = 192000,
.channels_min = 2,
.channels_max = 8,
.buffer_bytes_max = 256 * 1024,
.period_bytes_min = 64,
.period_bytes_max = 32 * 1024,
.periods_min = 2,
.periods_max = 32,
.fifo_size = 0,
};
/* Period 크기는 64 frames의 배수로 제한 */
err = snd_pcm_hw_constraint_step(runtime, 0,
SNDRV_PCM_HW_PARAM_PERIOD_SIZE,
64);
if (err < 0)
return err;
/* Buffer 크기는 period 크기의 정수배 */
err = snd_pcm_hw_constraint_integer(runtime,
SNDRV_PCM_HW_PARAM_PERIODS);
if (err < 0)
return err;
/* 96kHz 이상이면 채널은 최대 2개 */
err = snd_pcm_hw_rule_add(runtime, 0,
SNDRV_PCM_HW_PARAM_CHANNELS,
my_rate_channels_rule,
NULL,
SNDRV_PCM_HW_PARAM_RATE,
-1);
runtime->private_data = chip;
return 0;
}
/* 제약 룰: rate >= 96kHz → channels <= 2 */
static int my_rate_channels_rule(struct snd_pcm_hw_params *params,
struct snd_pcm_hw_rule *rule)
{
struct snd_interval *c =
hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS);
struct snd_interval *r =
hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE);
struct snd_interval ch = *c;
if (r->min >= 96000) {
ch.max = min(ch.max, 2u);
return snd_interval_refine(c, &ch);
}
return 0;
}
snd_pcm_hw_constraint_list(): 허용된 값 목록snd_pcm_hw_constraint_step(): 값은 N의 배수snd_pcm_hw_constraint_pow2(): 값은 2의 거듭제곱snd_pcm_hw_constraint_minmax(): 최소/최대 범위snd_pcm_hw_constraint_integer(): 정수 값만 허용snd_pcm_hw_rule_add(): 커스텀 룰 (여러 파라미터 간 의존성)
버퍼 관리
ALSA는 다양한 DMA 버퍼 타입을 지원합니다. 최신 커널은 managed buffer API를 권장합니다:
/* PCM 생성 시 DMA 버퍼 사전 할당 (권장) */
snd_pcm_set_managed_buffer_all(pcm,
SNDRV_DMA_TYPE_DEV, /* DMA 타입 */
&pci->dev, /* 디바이스 */
64 * 1024, /* 최소 버퍼 크기 */
256 * 1024); /* 최대 버퍼 크기 */
/* DMA 타입 종류 */
#define SNDRV_DMA_TYPE_CONTINUOUS 0 /* GFP_KERNEL 메모리 */
#define SNDRV_DMA_TYPE_DEV 1 /* dma_alloc_coherent() */
#define SNDRV_DMA_TYPE_DEV_SG 2 /* Scatter-Gather */
#define SNDRV_DMA_TYPE_VMALLOC 7 /* vmalloc() */
Managed API 사용 시 hw_params/hw_free 콜백에서 버퍼 할당/해제 불필요. ALSA 코어가 자동 처리.
PCM 상태 머신
snd_pcm_prepare()로 복구 필요.
완전한 PCM 드라이버 예제
/* 간단한 더미 PCM 드라이버 (sound/drivers/dummy.c 기반) */
static const struct snd_pcm_hardware my_pcm_hw = {
.info = SNDRV_PCM_INFO_MMAP |
SNDRV_PCM_INFO_INTERLEAVED |
SNDRV_PCM_INFO_MMAP_VALID,
.formats = SNDRV_PCM_FMTBIT_S16_LE,
.rates = SNDRV_PCM_RATE_48000,
.rate_min = 48000,
.rate_max = 48000,
.channels_min = 2,
.channels_max = 2,
.buffer_bytes_max = 64 * 1024,
.period_bytes_min = 64,
.period_bytes_max = 8 * 1024,
.periods_min = 2,
.periods_max = 32,
};
static int my_pcm_open(struct snd_pcm_substream *substream)
{
struct my_chip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
runtime->hw = my_pcm_hw;
runtime->private_data = chip;
/* 타이머 기반 period elapsed 시뮬레이션 */
hrtimer_init(&chip->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
chip->timer.function = my_hrtimer_callback;
return 0;
}
static int my_pcm_close(struct snd_pcm_substream *substream)
{
struct my_chip *chip = snd_pcm_substream_chip(substream);
hrtimer_cancel(&chip->timer);
return 0;
}
static int my_pcm_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *hw_params)
{
/* Managed buffer 사용 시 아무 작업 필요 없음 */
return 0;
}
static int my_pcm_hw_free(struct snd_pcm_substream *substream)
{
return 0;
}
static int my_pcm_prepare(struct snd_pcm_substream *substream)
{
struct my_chip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
/* 하드웨어 레지스터 설정 (샘플레이트, 포맷 등) */
chip->pcm_buffer_size = frames_to_bytes(runtime, runtime->buffer_size);
chip->pcm_period_size = frames_to_bytes(runtime, runtime->period_size);
chip->pcm_buf_pos = 0;
return 0;
}
static int my_pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
struct my_chip *chip = snd_pcm_substream_chip(substream);
switch (cmd) {
case SNDRV_PCM_TRIGGER_START:
case SNDRV_PCM_TRIGGER_RESUME:
/* DMA 시작 (또는 타이머 시작) */
chip->running = 1;
hrtimer_start(&chip->timer,
ns_to_ktime(chip->period_time_ns),
HRTIMER_MODE_REL);
break;
case SNDRV_PCM_TRIGGER_STOP:
case SNDRV_PCM_TRIGGER_SUSPEND:
/* DMA 중지 */
chip->running = 0;
hrtimer_cancel(&chip->timer);
break;
case SNDRV_PCM_TRIGGER_PAUSE_PUSH:
chip->running = 0;
hrtimer_cancel(&chip->timer);
break;
case SNDRV_PCM_TRIGGER_PAUSE_RELEASE:
chip->running = 1;
hrtimer_start(&chip->timer,
ns_to_ktime(chip->period_time_ns),
HRTIMER_MODE_REL);
break;
default:
return -EINVAL;
}
return 0;
}
static snd_pcm_uframes_t my_pcm_pointer(struct snd_pcm_substream *substream)
{
struct my_chip *chip = snd_pcm_substream_chip(substream);
struct snd_pcm_runtime *runtime = substream->runtime;
/* 현재 DMA 포인터를 frames 단위로 반환 */
return bytes_to_frames(runtime, chip->pcm_buf_pos);
}
static const struct snd_pcm_ops my_pcm_ops = {
.open = my_pcm_open,
.close = my_pcm_close,
.ioctl = snd_pcm_lib_ioctl,
.hw_params = my_pcm_hw_params,
.hw_free = my_pcm_hw_free,
.prepare = my_pcm_prepare,
.trigger = my_pcm_trigger,
.pointer = my_pcm_pointer,
};
/* Period elapsed 콜백 (타이머 또는 DMA IRQ) */
static enum hrtimer_restart my_hrtimer_callback(struct hrtimer *timer)
{
struct my_chip *chip = container_of(timer, struct my_chip, timer);
if (!chip->running)
return HRTIMER_NORESTART;
/* 포인터 진행 */
chip->pcm_buf_pos += chip->pcm_period_size;
if (chip->pcm_buf_pos >= chip->pcm_buffer_size)
chip->pcm_buf_pos = 0;
/* Period elapsed 통지 (wake up user space) */
snd_pcm_period_elapsed(chip->substream);
/* 다음 period 타이머 예약 */
hrtimer_forward_now(timer, ns_to_ktime(chip->period_time_ns));
return HRTIMER_RESTART;
}
poll()/select()를 깨워 다음 데이터 처리를 시작합니다.
Control (Mixer) 인터페이스
Control 인터페이스는 ALSA의 믹서 기능을 제공합니다. 볼륨, 뮤트, 입력 선택, EQ, 3D 효과 등 오디오 경로와 파라미터를 제어합니다.
Control 타입
| 타입 | 설명 | 예시 |
|---|---|---|
SNDRV_CTL_ELEM_TYPE_BOOLEAN |
On/Off 스위치 (0 or 1) | Mute, Loopback Enable |
SNDRV_CTL_ELEM_TYPE_INTEGER |
정수 범위 값 | Volume (0-100), Balance (-64~+63) |
SNDRV_CTL_ELEM_TYPE_INTEGER64 |
64비트 정수 | 고정밀 타이머 값 |
SNDRV_CTL_ELEM_TYPE_ENUMERATED |
선택 목록 | Input Source (Line/Mic/CD) |
SNDRV_CTL_ELEM_TYPE_BYTES |
바이트 배열 | 펌웨어 데이터, EQ 계수 |
SNDRV_CTL_ELEM_TYPE_IEC958 |
IEC958 (S/PDIF) 상태 | 디지털 출력 설정 |
Control 네이밍 규칙
ALSA는 일관된 네이밍 규칙을 권장합니다: [Source] [Direction] [Function]
| 구성요소 | 예시 | 설명 |
|---|---|---|
| Source | Master, PCM, Line, Mic, CD | 오디오 소스 |
| Direction | Playback, Capture | 재생 또는 녹음 |
| Function | Volume, Switch, Route | 제어 기능 |
표준 Control 이름 예시:
Master Playback VolumeMaster Playback Switch(Mute)PCM Playback VolumeMic Capture VolumeMic Boost Capture SwitchLine Capture SwitchCapture Source(Enumerated: Line/Mic/CD)
Control 생성 예제
/* INTEGER control: 볼륨 (0-100) */
static int my_volume_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
uinfo->count = 2; /* 스테레오 */
uinfo->value.integer.min = 0;
uinfo->value.integer.max = 100;
return 0;
}
static int my_volume_get(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct my_chip *chip = snd_kcontrol_chip(kcontrol);
ucontrol->value.integer.value[0] = chip->volume_left;
ucontrol->value.integer.value[1] = chip->volume_right;
return 0;
}
static int my_volume_put(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_value *ucontrol)
{
struct my_chip *chip = snd_kcontrol_chip(kcontrol);
int changed = 0;
if (chip->volume_left != ucontrol->value.integer.value[0]) {
chip->volume_left = ucontrol->value.integer.value[0];
changed = 1;
}
if (chip->volume_right != ucontrol->value.integer.value[1]) {
chip->volume_right = ucontrol->value.integer.value[1];
changed = 1;
}
if (changed)
my_chip_update_volume(chip); /* 하드웨어 레지스터 업데이트 */
return changed;
}
static const struct snd_kcontrol_new my_volume_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Master Playback Volume",
.info = my_volume_info,
.get = my_volume_get,
.put = my_volume_put,
};
/* BOOLEAN control: 뮤트 스위치 */
static int my_mute_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
uinfo->type = SNDRV_CTL_ELEM_TYPE_BOOLEAN;
uinfo->count = 1;
uinfo->value.integer.min = 0;
uinfo->value.integer.max = 1;
return 0;
}
static const struct snd_kcontrol_new my_mute_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Master Playback Switch",
.info = my_mute_info,
.get = my_mute_get,
.put = my_mute_put,
};
/* ENUMERATED control: 입력 소스 선택 */
static const char * const my_input_src_texts[] = {
"Line", "Mic", "CD", "Aux"
};
static int my_input_src_info(struct snd_kcontrol *kcontrol,
struct snd_ctl_elem_info *uinfo)
{
return snd_ctl_enum_info(uinfo, 1, 4, my_input_src_texts);
}
static const struct snd_kcontrol_new my_input_src_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Capture Source",
.info = my_input_src_info,
.get = my_input_src_get,
.put = my_input_src_put,
};
/* Control 등록 */
static int my_create_controls(struct snd_card *card)
{
int err;
err = snd_ctl_add(card, snd_ctl_new1(&my_volume_ctl, chip));
if (err < 0)
return err;
err = snd_ctl_add(card, snd_ctl_new1(&my_mute_ctl, chip));
if (err < 0)
return err;
err = snd_ctl_add(card, snd_ctl_new1(&my_input_src_ctl, chip));
if (err < 0)
return err;
return 0;
}
TLV (Type-Length-Value) dB Scale
ALSA는 TLV 메타데이터로 볼륨 값을 dB 단위로 표현합니다. 사용자 공간은 이를 이용해 정확한 dB 계산과 UI 표시가 가능합니다.
/* TLV dB scale: 0~100 → -48dB ~ 0dB (0.5dB step) */
static const DECLARE_TLV_DB_SCALE(my_volume_tlv, -4800, 50, 0);
/* DECLARE_TLV_DB_SCALE(name, min_dB*100, step_dB*100, mute_at_min)
* min_dB = -48dB = -4800 (센티벨 단위)
* step_dB = 0.5dB = 50
* mute_at_min = 0 (최소값에서 뮤트 안 함)
*/
static const struct snd_kcontrol_new my_volume_ctl = {
.iface = SNDRV_CTL_ELEM_IFACE_MIXER,
.name = "Master Playback Volume",
.access = SNDRV_CTL_ELEM_ACCESS_READWRITE |
SNDRV_CTL_ELEM_ACCESS_TLV_READ,
.info = my_volume_info,
.get = my_volume_get,
.put = my_volume_put,
.tlv.p = my_volume_tlv,
};
Control 변경 통지
하드웨어가 control 값을 변경하면 (예: 물리적 볼륨 노브) 드라이버는 사용자 공간에 통지해야 합니다:
/* 하드웨어 인터럽트 핸들러에서 */
static irqreturn_t my_interrupt(int irq, void *dev_id)
{
struct my_chip *chip = dev_id;
u32 status = readl(chip->regs + STATUS_REG);
if (status & VOLUME_CHANGED) {
/* 새 볼륨 값 읽기 */
chip->volume_left = readl(chip->regs + VOL_LEFT);
chip->volume_right = readl(chip->regs + VOL_RIGHT);
/* 변경 통지 (kcontrol은 snd_ctl_add() 시 저장된 포인터) */
snd_ctl_notify(chip->card, SNDRV_CTL_EVENT_MASK_VALUE,
&chip->volume_kctl->id);
}
return IRQ_HANDLED;
}
Jack Detection (자동 감지)
ALSA는 헤드폰/마이크 잭 삽입/제거 감지 API를 제공합니다:
/* include/sound/jack.h */
struct snd_jack *jack;
/* Jack 생성 */
int err = snd_jack_new(card, "Headphone", SND_JACK_HEADPHONE,
&jack, true, true);
/* Jack 상태 업데이트 (IRQ 핸들러에서) */
if (headphone_plugged)
snd_jack_report(jack, SND_JACK_HEADPHONE);
else
snd_jack_report(jack, 0);
/* Jack 타입 */
#define SND_JACK_HEADPHONE 0x0001
#define SND_JACK_MICROPHONE 0x0002
#define SND_JACK_HEADSET 0x0003 /* HP + MIC */
#define SND_JACK_LINEOUT 0x0004
#define SND_JACK_MECHANICAL 0x0008 /* 물리적 스위치 */
#define SND_JACK_VIDEOOUT 0x0010
#define SND_JACK_LINEIN 0x0020
사용자 공간은 /dev/input/event*를 통해 jack 이벤트를 수신하고 자동으로 오디오 경로를 전환합니다.
MIDI & Raw MIDI
Raw MIDI API
Raw MIDI는 하드웨어 MIDI 포트를 직접 제어합니다:
/* MIDI 디바이스 생성 */
struct snd_rawmidi *rmidi;
int err = snd_rawmidi_new(card, "MyMIDI", 0,
1, 1, /* 1 output, 1 input */
&rmidi);
rmidi->private_data = chip;
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_OUTPUT,
&my_midi_output_ops);
snd_rawmidi_set_ops(rmidi, SNDRV_RAWMIDI_STREAM_INPUT,
&my_midi_input_ops);
/* MIDI ops */
static const struct snd_rawmidi_ops my_midi_output_ops = {
.open = my_midi_output_open,
.close = my_midi_output_close,
.trigger = my_midi_output_trigger,
};
static void my_midi_output_trigger(struct snd_rawmidi_substream *substream,
int up)
{
struct my_chip *chip = substream->rmidi->private_data;
if (up) {
/* MIDI 송신 시작: FIFO에서 읽어 하드웨어로 전송 */
unsigned char byte;
while (snd_rawmidi_transmit_peek(substream, &byte, 1) == 1) {
if (!my_midi_tx_ready(chip))
break;
my_midi_write(chip, byte);
snd_rawmidi_transmit_ack(substream, 1);
}
} else {
/* MIDI 송신 중지 */
}
}
/* MIDI 수신 (IRQ에서 호출) */
static void my_midi_rx_interrupt(struct my_chip *chip)
{
unsigned char byte;
while (my_midi_rx_avail(chip)) {
byte = my_midi_read(chip);
snd_rawmidi_receive(chip->midi_input, &byte, 1);
}
}
가상 MIDI (snd-virmidi)
snd-virmidi 모듈은 가상 MIDI 포트를 생성하여 ALSA Sequencer와 Raw MIDI 간 브리지 역할을 합니다.
$ modprobe snd-virmidi
$ aconnect -l
client 14: 'Virtual Raw MIDI 0-0' [type=kernel]
0 'VirMIDI 0-0 '
ALSA Sequencer
Sequencer는 MIDI 이벤트의 고수준 라우팅과 스케줄링을 제공합니다. Client/Port 모델로 복잡한 MIDI 패치베이 구성이 가능합니다.
/* /dev/snd/seq 인터페이스
* - 다중 클라이언트/포트 지원
* - 이벤트 큐잉, 타임스탬프
* - 동적 라우팅 (aconnect로 포트 연결)
* - MIDI 파일 재생 (pmidi, aplaymidi)
*/
Timer API
ALSA Timer
ALSA Timer는 고정밀 타이밍 이벤트를 제공합니다. PCM 스트림과 동기화하거나 독립적인 타이머로 사용할 수 있습니다.
- Hardware Timer: 실제 하드웨어 타이머 (RTC, HPET, 사운드 카드 타이머)
- Software Timer: 커널 hrtimer 기반
- PCM Timer: PCM 스트림의 period elapsed와 동기화
/* 타이머 생성 */
struct snd_timer *timer;
struct snd_timer_id tid = {
.dev_class = SNDRV_TIMER_CLASS_CARD,
.dev_sclass = SNDRV_TIMER_SCLASS_NONE,
.card = card->number,
.device = 0,
.subdevice = 0,
};
int err = snd_timer_new(card, "MyTimer", &tid, &timer);
timer->private_data = chip;
timer->hw = my_timer_hw;
strcpy(timer->name, "My Hardware Timer");
/* 타이머 HW 정의 */
static struct snd_timer_hardware my_timer_hw = {
.flags = SNDRV_TIMER_HW_AUTO,
.resolution = 1000000, /* 1us (나노초 단위) */
.ticks = 1000000,
.open = my_timer_open,
.close = my_timer_close,
.start = my_timer_start,
.stop = my_timer_stop,
};
PCM Timer 통합
PCM 서브시스템은 자동으로 period elapsed 타이밍을 타이머 이벤트로 변환합니다. 사용자 공간은 /dev/snd/timer를 열고 PCM 타이머에 연결하여 정밀한 동기화를 구현할 수 있습니다.
HW Dep (Hardware Dependent)
HW Dep API
snd_hwdep는 표준 ALSA 인터페이스로 표현할 수 없는 하드웨어 종속 기능을 위한 일반 디바이스 노드입니다.
/* HW Dep 디바이스 생성 */
struct snd_hwdep *hw;
int err = snd_hwdep_new(card, "MyHwDep", 0, &hw);
hw->iface = SNDRV_HWDEP_IFACE_OPL3; /* 또는 SNDRV_HWDEP_IFACE_SB16CSP 등 */
hw->private_data = chip;
hw->ops.open = my_hwdep_open;
hw->ops.release = my_hwdep_release;
hw->ops.ioctl = my_hwdep_ioctl;
hw->ops.read = my_hwdep_read;
hw->ops.write = my_hwdep_write;
Firmware 업로드
많은 사운드 카드는 DSP 펌웨어를 hwdep를 통해 업로드합니다:
static long my_hwdep_ioctl(struct snd_hwdep *hw,
struct file *file,
unsigned int cmd,
unsigned long arg)
{
struct my_chip *chip = hw->private_data;
switch (cmd) {
case MY_IOCTL_LOAD_FIRMWARE:
return my_load_firmware(chip, (void __user *)arg);
case MY_IOCTL_GET_VERSION:
return put_user(chip->firmware_version, (int __user *)arg);
default:
return -ENOTTY;
}
}
Compress Offload
Compress Offload 개요
Compress Offload API는 압축된 오디오 스트림 (MP3, AAC, FLAC 등)을 DSP에 직접 전송하여 CPU 사용률을 낮춥니다. 모바일/임베디드 플랫폼에서 배터리 수명 연장에 효과적입니다.
/* include/sound/compress_driver.h */
struct snd_compr *compr;
int err = snd_compress_new(card, 0, SND_COMPRESS_PLAYBACK, "compress", &compr);
compr->ops = &my_compr_ops;
/* Compress ops */
struct snd_compr_ops {
int (*open)(struct snd_compr_stream *stream);
int (*free)(struct snd_compr_stream *stream);
int (*set_params)(struct snd_compr_stream *stream,
struct snd_compr_params *params);
int (*get_params)(struct snd_compr_stream *stream,
struct snd_codec *params);
int (*trigger)(struct snd_compr_stream *stream, int cmd);
int (*pointer)(struct snd_compr_stream *stream,
struct snd_compr_tstamp *tstamp);
int (*copy)(struct snd_compr_stream *stream,
char __user *buf, size_t count);
int (*get_caps)(struct snd_compr_stream *stream,
struct snd_compr_caps *caps);
int (*get_codec_caps)(struct snd_compr_stream *stream,
struct snd_compr_codec_caps *codec);
};
Compress Ops 구현
static int my_compr_set_params(struct snd_compr_stream *stream,
struct snd_compr_params *params)
{
struct snd_codec *codec = ¶ms->codec;
/* DSP에 코덱 파라미터 설정 */
switch (codec->id) {
case SND_AUDIOCODEC_MP3:
my_dsp_set_codec(CODEC_MP3, codec->bit_rate);
break;
case SND_AUDIOCODEC_AAC:
my_dsp_set_codec(CODEC_AAC, codec->bit_rate);
break;
default:
return -EINVAL;
}
return 0;
}
static int my_compr_copy(struct snd_compr_stream *stream,
char __user *buf, size_t count)
{
/* 압축 데이터를 DSP 링 버퍼로 복사 */
return my_dsp_write_compressed(stream, buf, count);
}
지원 코덱 포맷
#define SND_AUDIOCODEC_PCM 1
#define SND_AUDIOCODEC_MP3 2
#define SND_AUDIOCODEC_AMR 3
#define SND_AUDIOCODEC_AMRWB 4
#define SND_AUDIOCODEC_AMRWBPLUS 5
#define SND_AUDIOCODEC_AAC 6
#define SND_AUDIOCODEC_WMA 7
#define SND_AUDIOCODEC_REAL 8
#define SND_AUDIOCODEC_VORBIS 9
#define SND_AUDIOCODEC_FLAC 10
#define SND_AUDIOCODEC_IEC61937 11 /* S/PDIF passthrough */
#define SND_AUDIOCODEC_G723_1 12
#define SND_AUDIOCODEC_G729 13
#define SND_AUDIOCODEC_BESPOKE 14 /* 벤더 전용 */
사용자 공간은 tinycompress 라이브러리를 통해 compress 디바이스에 접근합니다.
ASoC 프레임워크 심화
ASoC (ALSA System on Chip)는 임베디드/SoC 플랫폼을 위한 고수준 오디오 프레임워크입니다. 코덱, 플랫폼 (CPU DAI + DMA), 머신 (보드 특화) 드라이버를 분리하여 재사용성을 극대화합니다.
ASoC 아키텍처
ASoC는 세 계층으로 구성됩니다:
- Codec Driver: 오디오 코덱 칩 (WM8731, RT5640 등). I2C/SPI로 제어, I2S/TDM으로 데이터 전송.
- Platform Driver: SoC의 오디오 인터페이스 (I2S, PCM, AC97). DMA 엔진 포함.
- Machine Driver: 보드별 연결 정보 (어떤 코덱이 어떤 CPU DAI에 연결되었는지, GPIO 설정 등).
Codec Driver 작성
/* sound/soc/codecs/wm8731.c 스타일 간단 예제 */
#include <sound/soc.h>
#include <sound/tlv.h>
#include <linux/regmap.h>
/* Codec 레지스터 맵 */
#define WM8731_LINVOL 0x00
#define WM8731_RINVOL 0x01
#define WM8731_LOUT1V 0x02
#define WM8731_ROUT1V 0x03
#define WM8731_APANA 0x04
#define WM8731_APDIGI 0x05
#define WM8731_PWR 0x06
#define WM8731_IFACE 0x07
#define WM8731_SRATE 0x08
#define WM8731_ACTIVE 0x09
#define WM8731_RESET 0x0f
/* Regmap 설정 */
static const struct regmap_config wm8731_regmap = {
.reg_bits = 7,
.val_bits = 9,
.max_register = WM8731_RESET,
.cache_type = REGCACHE_RBTREE,
};
/* Kcontrols */
static const DECLARE_TLV_DB_SCALE(in_tlv, -3450, 150, 0);
static const DECLARE_TLV_DB_SCALE(out_tlv, -7300, 100, 1);
static const struct snd_kcontrol_new wm8731_controls[] = {
SOC_DOUBLE_R_TLV("Capture Volume", WM8731_LINVOL, WM8731_RINVOL,
0, 31, 0, in_tlv),
SOC_DOUBLE_R_TLV("Headphone Playback Volume", WM8731_LOUT1V,
WM8731_ROUT1V, 0, 127, 0, out_tlv),
SOC_SINGLE("Mic Boost (+20dB)", WM8731_APANA, 0, 1, 0),
};
/* DAPM widgets (나중에 DAPM 섹션에서 상세 설명) */
static const struct snd_soc_dapm_widget wm8731_dapm_widgets[] = {
SND_SOC_DAPM_DAC("DAC", "Playback", WM8731_PWR, 3, 1),
SND_SOC_DAPM_ADC("ADC", "Capture", WM8731_PWR, 2, 1),
SND_SOC_DAPM_OUTPUT("LHPOUT"),
SND_SOC_DAPM_OUTPUT("RHPOUT"),
SND_SOC_DAPM_INPUT("LLINEIN"),
SND_SOC_DAPM_INPUT("RLINEIN"),
};
/* DAPM routes */
static const struct snd_soc_dapm_route wm8731_routes[] = {
{"LHPOUT", NULL, "DAC"},
{"RHPOUT", NULL, "DAC"},
{"ADC", NULL, "LLINEIN"},
{"ADC", NULL, "RLINEIN"},
};
/* DAI ops */
static int wm8731_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct snd_soc_component *component = dai->component;
unsigned int iface = 0;
/* 샘플 포맷 설정 */
switch (params_width(params)) {
case 16:
break;
case 20:
iface |= 0x04;
break;
case 24:
iface |= 0x08;
break;
case 32:
iface |= 0x0c;
break;
}
snd_soc_component_write(component, WM8731_IFACE, iface);
/* 샘플레이트 설정 (생략: 복잡한 클럭 계산) */
return 0;
}
static int wm8731_set_dai_fmt(struct snd_soc_dai *dai,
unsigned int fmt)
{
struct snd_soc_component *component = dai->component;
unsigned int iface = 0;
/* Format: I2S, Left-justified, Right-justified, DSP mode A/B */
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_I2S:
iface |= 0x02;
break;
case SND_SOC_DAIFMT_LEFT_J:
break;
case SND_SOC_DAIFMT_RIGHT_J:
iface |= 0x01;
break;
case SND_SOC_DAIFMT_DSP_A:
iface |= 0x03;
break;
default:
return -EINVAL;
}
/* Clock master/slave */
switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
case SND_SOC_DAIFMT_CBM_CFM: /* Codec is master */
iface |= 0x40;
break;
case SND_SOC_DAIFMT_CBS_CFS: /* Codec is slave */
break;
default:
return -EINVAL;
}
snd_soc_component_update_bits(component, WM8731_IFACE, 0x4f, iface);
return 0;
}
static const struct snd_soc_dai_ops wm8731_dai_ops = {
.hw_params = wm8731_hw_params,
.set_fmt = wm8731_set_dai_fmt,
};
/* Codec DAI 정의 */
static struct snd_soc_dai_driver wm8731_dai = {
.name = "wm8731-hifi",
.playback = {
.stream_name = "Playback",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_96000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
},
.capture = {
.stream_name = "Capture",
.channels_min = 1,
.channels_max = 2,
.rates = SNDRV_PCM_RATE_8000_96000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S20_3LE |
SNDRV_PCM_FMTBIT_S24_LE | SNDRV_PCM_FMTBIT_S32_LE,
},
.ops = &wm8731_dai_ops,
};
/* Component driver */
static const struct snd_soc_component_driver soc_component_dev_wm8731 = {
.controls = wm8731_controls,
.num_controls = ARRAY_SIZE(wm8731_controls),
.dapm_widgets = wm8731_dapm_widgets,
.num_dapm_widgets = ARRAY_SIZE(wm8731_dapm_widgets),
.dapm_routes = wm8731_routes,
.num_dapm_routes = ARRAY_SIZE(wm8731_routes),
};
/* I2C probe */
static int wm8731_i2c_probe(struct i2c_client *i2c)
{
struct regmap *regmap;
int ret;
regmap = devm_regmap_init_i2c(i2c, &wm8731_regmap);
if (IS_ERR(regmap))
return PTR_ERR(regmap);
ret = devm_snd_soc_register_component(&i2c->dev,
&soc_component_dev_wm8731,
&wm8731_dai, 1);
return ret;
}
static const struct i2c_device_id wm8731_i2c_id[] = {
{ "wm8731", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, wm8731_i2c_id);
static struct i2c_driver wm8731_i2c_driver = {
.driver = {
.name = "wm8731",
},
.probe_new = wm8731_i2c_probe,
.id_table = wm8731_i2c_id,
};
module_i2c_driver(wm8731_i2c_driver);
Platform Driver 작성
/* CPU DAI + DMA (간단화된 예제) */
static int my_i2s_hw_params(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params,
struct snd_soc_dai *dai)
{
struct my_i2s *i2s = snd_soc_dai_get_drvdata(dai);
unsigned int rate = params_rate(params);
unsigned int channels = params_channels(params);
unsigned int width = params_width(params);
/* I2S 컨트롤러 클럭 설정 */
unsigned int bclk = rate * channels * width;
my_i2s_set_clk(i2s, bclk);
return 0;
}
static int my_i2s_set_fmt(struct snd_soc_dai *dai, unsigned int fmt)
{
struct my_i2s *i2s = snd_soc_dai_get_drvdata(dai);
u32 ctrl = 0;
switch (fmt & SND_SOC_DAIFMT_FORMAT_MASK) {
case SND_SOC_DAIFMT_I2S:
ctrl |= I2S_MODE_I2S;
break;
case SND_SOC_DAIFMT_LEFT_J:
ctrl |= I2S_MODE_LEFT_J;
break;
default:
return -EINVAL;
}
switch (fmt & SND_SOC_DAIFMT_MASTER_MASK) {
case SND_SOC_DAIFMT_CBS_CFS: /* CPU is master */
ctrl |= I2S_MASTER;
break;
case SND_SOC_DAIFMT_CBM_CFM: /* Codec is master */
break;
default:
return -EINVAL;
}
writel(ctrl, i2s->regs + I2S_CTRL);
return 0;
}
static const struct snd_soc_dai_ops my_i2s_dai_ops = {
.hw_params = my_i2s_hw_params,
.set_fmt = my_i2s_set_fmt,
};
static struct snd_soc_dai_driver my_i2s_dai = {
.playback = {
.channels_min = 2,
.channels_max = 8,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
},
.capture = {
.channels_min = 2,
.channels_max = 8,
.rates = SNDRV_PCM_RATE_8000_192000,
.formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE |
SNDRV_PCM_FMTBIT_S32_LE,
},
.ops = &my_i2s_dai_ops,
};
/* Platform component (DMA) */
static const struct snd_soc_component_driver my_platform_component = {
.name = "my-platform",
.pcm_construct = my_pcm_new, /* DMA 버퍼 할당 */
};
static int my_i2s_probe(struct platform_device *pdev)
{
struct my_i2s *i2s;
int ret;
i2s = devm_kzalloc(&pdev->dev, sizeof(*i2s), GFP_KERNEL);
if (!i2s)
return -ENOMEM;
i2s->regs = devm_platform_ioremap_resource(pdev, 0);
platform_set_drvdata(pdev, i2s);
ret = devm_snd_soc_register_component(&pdev->dev,
&my_platform_component,
&my_i2s_dai, 1);
return ret;
}
Machine Driver 작성
/* sound/soc/fsl/imx-wm8731.c 스타일 */
static struct snd_soc_dai_link my_board_dai_link = {
.name = "WM8731",
.stream_name = "WM8731 HiFi",
.cpus = &(struct snd_soc_dai_link_component){
.dai_name = "my-i2s-dai",
},
.num_cpus = 1,
.codecs = &(struct snd_soc_dai_link_component){
.dai_name = "wm8731-hifi",
},
.num_codecs = 1,
.platforms = &(struct snd_soc_dai_link_component){
.name = "my-platform",
},
.num_platforms = 1,
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS, /* I2S, CPU is master */
};
static struct snd_soc_card my_board_card = {
.name = "MyBoard-WM8731",
.owner = THIS_MODULE,
.dai_link = &my_board_dai_link,
.num_links = 1,
};
static int my_board_probe(struct platform_device *pdev)
{
struct device_node *np = pdev->dev.of_node;
struct device_node *cpu_np, *codec_np;
/* Device Tree에서 DAI 노드 읽기 */
cpu_np = of_parse_phandle(np, "cpu-dai", 0);
codec_np = of_parse_phandle(np, "audio-codec", 0);
my_board_dai_link.cpus->of_node = cpu_np;
my_board_dai_link.codecs->of_node = codec_np;
my_board_dai_link.platforms->of_node = cpu_np;
my_board_card.dev = &pdev->dev;
return devm_snd_soc_register_card(&pdev->dev, &my_board_card);
}
static const struct of_device_id my_board_dt_ids[] = {
{ .compatible = "myvendor,myboard-audio", },
{ }
};
static struct platform_driver my_board_driver = {
.driver = {
.name = "myboard-audio",
.of_match_table = my_board_dt_ids,
},
.probe = my_board_probe,
};
module_platform_driver(my_board_driver);
DAI 포맷
| 포맷 | 설명 | 용도 |
|---|---|---|
SND_SOC_DAIFMT_I2S |
I2S (Philips) | 가장 일반적인 스테레오 포맷 |
SND_SOC_DAIFMT_LEFT_J |
Left Justified | MSB가 LRCLK 엣지에 정렬 |
SND_SOC_DAIFMT_RIGHT_J |
Right Justified | LSB가 LRCLK 엣지에 정렬 |
SND_SOC_DAIFMT_DSP_A |
DSP Mode A | 1 BCLK 지연 |
SND_SOC_DAIFMT_DSP_B |
DSP Mode B | 지연 없음 |
SND_SOC_DAIFMT_AC97 |
AC'97 | 레거시 AC'97 버스 |
SND_SOC_DAIFMT_PDM |
PDM (Pulse Density Modulation) | MEMS 마이크 |
Device Tree Binding 예제
/* arch/arm/boot/dts/myboard.dts */
i2s0: i2s@40030000 {
compatible = "myvendor,my-i2s";
reg = <0x40030000 0x1000>;
interrupts = <45>;
clocks = <&clk_i2s>;
dmas = <&dma 10>, <&dma 11>;
dma-names = "tx", "rx";
#sound-dai-cells = <0>;
};
wm8731: codec@1a {
compatible = "wlf,wm8731";
reg = <0x1a>;
#sound-dai-cells = <0>;
clocks = <&clk_mclk>;
clock-names = "mclk";
};
sound {
compatible = "myvendor,myboard-audio";
cpu-dai = <&i2s0>;
audio-codec = <&wm8731>;
};
또는 simple-audio-card 사용:
sound {
compatible = "simple-audio-card";
simple-audio-card,name = "MyBoard Audio";
simple-audio-card,format = "i2s";
simple-audio-card,mclk-fs = <256>;
simple-audio-card,cpu {
sound-dai = <&i2s0>;
};
simple-audio-card,codec {
sound-dai = <&wm8731>;
};
};
DAPM 심화
DAPM (Dynamic Audio Power Management)는 ASoC의 핵심 기능으로, 오디오 경로를 기반으로 전원 도메인을 자동으로 켜고 끄는 지능형 전원 관리 시스템입니다. Widget 그래프와 오디오 경로 추적으로 사용하지 않는 컴포넌트의 전원을 자동 차단하여 전력 소비를 최소화합니다.
DAPM Widget 타입
| Widget 타입 | 설명 | 예시 |
|---|---|---|
SND_SOC_DAPM_INPUT |
입력 핀 (외부) | Mic, Line In |
SND_SOC_DAPM_OUTPUT |
출력 핀 (외부) | Headphone, Speaker |
SND_SOC_DAPM_MIC |
마이크 바이어스 포함 | Internal Mic |
SND_SOC_DAPM_HP |
헤드폰 출력 | Headphone Jack |
SND_SOC_DAPM_SPK |
스피커 출력 | External Speaker |
SND_SOC_DAPM_LINE |
라인 입출력 | Line Out, Line In |
SND_SOC_DAPM_ADC |
Analog-to-Digital Converter | Left ADC, Right ADC |
SND_SOC_DAPM_DAC |
Digital-to-Analog Converter | Left DAC, Right DAC |
SND_SOC_DAPM_MIXER |
믹서 (여러 입력 합성) | Output Mixer |
SND_SOC_DAPM_MUX |
멀티플렉서 (하나 선택) | Input Mux (Line/Mic/CD) |
SND_SOC_DAPM_DEMUX |
디멀티플렉서 | Output Router |
SND_SOC_DAPM_PGA |
Programmable Gain Amplifier | Mic Boost, Volume Control |
SND_SOC_DAPM_SUPPLY |
전원 공급 (다른 widget 의존) | VREF, Clock, Bias |
SND_SOC_DAPM_REGULATOR_SUPPLY |
Regulator 전원 | AVDD, DVDD |
SND_SOC_DAPM_CLOCK_SUPPLY |
클럭 소스 | MCLK |
SND_SOC_DAPM_AIF_IN |
오디오 인터페이스 입력 | I2S RX |
SND_SOC_DAPM_AIF_OUT |
오디오 인터페이스 출력 | I2S TX |
SND_SOC_DAPM_PRE |
스트림 시작 전 이벤트 | Pre-charge circuit |
SND_SOC_DAPM_POST |
스트림 종료 후 이벤트 | Pop noise reduction |
SND_SOC_DAPM_SWITCH |
On/Off 스위치 | Capture Switch |
DAPM Route 정의
Route는 widget 간 오디오 경로를 정의합니다:
static const struct snd_soc_dapm_widget wm8731_widgets[] = {
/* Outputs */
SND_SOC_DAPM_OUTPUT("LHPOUT"),
SND_SOC_DAPM_OUTPUT("RHPOUT"),
SND_SOC_DAPM_OUTPUT("LOUT"),
SND_SOC_DAPM_OUTPUT("ROUT"),
/* Inputs */
SND_SOC_DAPM_INPUT("LLINEIN"),
SND_SOC_DAPM_INPUT("RLINEIN"),
SND_SOC_DAPM_INPUT("MICIN"),
/* DACs */
SND_SOC_DAPM_DAC("DAC", "Playback", WM8731_PWR, 3, 1),
/* ADCs */
SND_SOC_DAPM_ADC("ADC", "Capture", WM8731_PWR, 2, 1),
/* Mixers */
SND_SOC_DAPM_MIXER("Output Mixer", WM8731_PWR, 4, 1, NULL, 0),
/* Input Mux */
SND_SOC_DAPM_MUX("Input Mux", SND_SOC_NOPM, 0, 0, &wm8731_input_mux),
/* Mic Bias */
SND_SOC_DAPM_SUPPLY("Mic Bias", WM8731_PWR, 1, 0, NULL, 0),
};
static const struct snd_soc_dapm_route wm8731_routes[] = {
/* Playback path */
{"Output Mixer", NULL, "DAC"},
{"LHPOUT", NULL, "Output Mixer"},
{"RHPOUT", NULL, "Output Mixer"},
{"LOUT", NULL, "Output Mixer"},
{"ROUT", NULL, "Output Mixer"},
/* Capture path */
{"Input Mux", "Line", "LLINEIN"},
{"Input Mux", "Line", "RLINEIN"},
{"Input Mux", "Mic", "MICIN"},
{"ADC", NULL, "Input Mux"},
/* Mic bias */
{"MICIN", NULL, "Mic Bias"},
};
DAPM 전원 시퀀싱 이벤트
Widget은 전원 상태 변화 시 이벤트 콜백을 받을 수 있습니다:
static int my_amp_event(struct snd_soc_dapm_widget *w,
struct snd_kcontrol *kcontrol,
int event)
{
struct snd_soc_component *component = snd_soc_dapm_to_component(w->dapm);
switch (event) {
case SND_SOC_DAPM_PRE_PMU: /* Power-up 전 */
dev_dbg(component->dev, "Amp powering up\n");
/* Pre-charge 회로 활성화 */
break;
case SND_SOC_DAPM_POST_PMU: /* Power-up 후 */
/* 앰프 언뮤트 (pop noise 방지 위해 지연) */
msleep(50);
snd_soc_component_update_bits(component, AMP_CTRL, MUTE_BIT, 0);
break;
case SND_SOC_DAPM_PRE_PMD: /* Power-down 전 */
/* 앰프 뮤트 (pop noise 방지) */
snd_soc_component_update_bits(component, AMP_CTRL, MUTE_BIT, MUTE_BIT);
msleep(50);
break;
case SND_SOC_DAPM_POST_PMD: /* Power-down 후 */
dev_dbg(component->dev, "Amp powered down\n");
break;
}
return 0;
}
SND_SOC_DAPM_PGA_E("Headphone Amp", SND_SOC_NOPM, 0, 0, NULL, 0,
my_amp_event,
SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMU |
SND_SOC_DAPM_PRE_PMD | SND_SOC_DAPM_POST_PMD),
DAPM Controls (동적 경로)
DAPM mixer와 mux는 오디오 경로를 동적으로 변경할 수 있습니다:
/* Input Mux 정의 */
static const char * const input_mux_texts[] = {
"Line", "Mic", "CD", "Aux"
};
static SOC_ENUM_SINGLE_DECL(input_mux_enum, INPUT_MUX_REG, 0,
input_mux_texts);
static const struct snd_kcontrol_new input_mux_control =
SOC_DAPM_ENUM("Route", input_mux_enum);
SND_SOC_DAPM_MUX("Input Mux", SND_SOC_NOPM, 0, 0, &input_mux_control),
/* Mixer 정의 (여러 입력 합성) */
static const struct snd_kcontrol_new output_mixer_controls[] = {
SOC_DAPM_SINGLE("DAC Switch", MIXER_REG, 0, 1, 0),
SOC_DAPM_SINGLE("Line Bypass Switch", MIXER_REG, 1, 1, 0),
SOC_DAPM_SINGLE("Mic Sidetone Switch", MIXER_REG, 2, 1, 0),
};
SND_SOC_DAPM_MIXER("Output Mixer", SND_SOC_NOPM, 0, 0,
output_mixer_controls,
ARRAY_SIZE(output_mixer_controls)),
DAPM 그래프 시각화
DPCM (Dynamic PCM)
Front-End / Back-End 분리
DPCM (Dynamic PCM)은 ASoC의 고급 기능으로, 하나의 PCM 스트림을 여러 백엔드 (코덱/디지털 인터페이스)로 동적 라우팅할 수 있습니다. 주로 DSP 기반 시스템에서 사용됩니다.
- Front-End (FE) DAI: 애플리케이션이 여는 PCM 디바이스. 사용자 공간 인터페이스.
- Back-End (BE) DAI: 실제 하드웨어 (코덱, HDMI, BT, S/PDIF 등). 여러 FE가 하나의 BE를 공유하거나 동적 전환 가능.
FE/BE DAI Link 설정
/* Front-End DAI */
static struct snd_soc_dai_link fe_dai_links[] = {
{
.name = "Media Playback",
.stream_name = "Media",
.cpus = COMP_CPU("sst-media-cpu-dai"),
.codecs = COMP_DUMMY(),
.platforms = COMP_PLATFORM("sst-dsp"),
.dynamic = 1, /* FE는 dynamic = 1 */
.dpcm_playback = 1,
},
{
.name = "Voice Call",
.stream_name = "Voice",
.cpus = COMP_CPU("sst-voice-cpu-dai"),
.codecs = COMP_DUMMY(),
.platforms = COMP_PLATFORM("sst-dsp"),
.dynamic = 1,
.dpcm_playback = 1,
.dpcm_capture = 1,
},
};
/* Back-End DAIs */
static struct snd_soc_dai_link be_dai_links[] = {
{
.name = "Codec",
.id = 0,
.cpus = COMP_CPU("ssp2-port"),
.codecs = COMP_CODEC("wm8731.1-001a", "wm8731-hifi"),
.platforms = COMP_DUMMY(),
.no_pcm = 1, /* BE는 no_pcm = 1 (사용자 공간 노출 안 함) */
.dpcm_playback = 1,
.dpcm_capture = 1,
.dai_fmt = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
SND_SOC_DAIFMT_CBS_CFS,
},
{
.name = "HDMI",
.id = 1,
.cpus = COMP_CPU("hdmi-port"),
.codecs = COMP_CODEC("hdmi-audio-codec", "i2s-hifi"),
.platforms = COMP_DUMMY(),
.no_pcm = 1,
.dpcm_playback = 1,
},
};
/* DAPM routes로 FE와 BE 연결 */
static const struct snd_soc_dapm_route dpcm_routes[] = {
/* Media FE → Codec BE */
{"Codec Playback", NULL, "sst media0_out"},
{"sst media0_in", NULL, "Codec Capture"},
/* Media FE → HDMI BE */
{"HDMI Playback", NULL, "sst media0_out"},
/* Voice FE → Codec BE only */
{"Codec Playback", NULL, "sst voice_out"},
{"sst voice_in", NULL, "Codec Capture"},
};
SND_SOC_DPCM_TRIGGER_POST, SND_SOC_DPCM_TRIGGER_BESPOKE 플래그로 트리거 순서를 제어할 수 있습니다.
Topology 프레임워크
Topology 파일 포맷
ASoC Topology는 오디오 그래프 (widgets, routes, kcontrols)를 바이너리 파일로 정의하여 런타임에 로드하는 메커니즘입니다. 주로 DSP 기반 시스템에서 사용됩니다.
# topology.conf (alsatplg 입력 파일)
SectionPCM."Media Playback" {
id 0
dai."SSP2 Pin" {
id 0
}
pcm."playback" {
capabilities "SND_SOC_TPLG_DAI_PCM_CAP_PLAYBACK"
}
}
SectionWidget."media0 in" {
type "aif_in"
stream_name "ssp2-0 Rx"
}
SectionWidget."Output Mixer" {
type "mixer"
mixer {
name "Media0 Playback"
channel "left"
mux 0
}
}
SectionGraph."dsp-graph" {
lines [
"media0 in, , SSP2 CODEC IN"
"Output Mixer, Media0 Playback, media0 in"
"SSP2 CODEC OUT, , Output Mixer"
]
}
alsatplg 도구로 텍스트 파일을 바이너리 .tplg 파일로 컴파일:
$ alsatplg -c topology.conf -o dsp-topology.tplg
$ cp dsp-topology.tplg /lib/firmware/
커널 Topology API
/* sound/soc/soc-topology.c */
struct snd_soc_tplg_ops {
int (*widget_load)(struct snd_soc_component *,
int index,
struct snd_soc_dapm_widget *,
struct snd_soc_tplg_dapm_widget *);
int (*dai_load)(struct snd_soc_component *,
int index,
struct snd_soc_dai_driver *,
struct snd_soc_tplg_pcm *,
struct snd_soc_dai *);
int (*link_load)(struct snd_soc_component *,
int index,
struct snd_soc_dai_link *,
struct snd_soc_tplg_link_config *);
int (*manifest)(struct snd_soc_component *,
int index,
struct snd_soc_tplg_manifest *);
};
/* Topology 로드 */
int err = snd_soc_tplg_component_load(component,
&my_tplg_ops,
firmware,
SND_SOC_TPLG_INDEX_ALL);
Sound Open Firmware (SOF)
SOF 아키텍처
SOF (Sound Open Firmware)는 Intel/AMD DSP용 오픈소스 펌웨어 프로젝트입니다. 커널 드라이버는 DSP와 IPC (Inter-Processor Communication)로 통신하며 topology 기반 오디오 파이프라인을 구성합니다.
SOF Topology (Pipeline)
SOF는 topology 파일로 DSP 내 오디오 처리 파이프라인을 정의합니다:
# sof-topology: PCM → Volume → Mixer → I2S Out
Object.Pipeline {
playback-pipeline {
id 1
Object.Widget {
host-copier {
stream_name "Playback 0"
}
volume {
ramp_step_ms 100
}
mixer {
num_input_pins 2
}
dai-copier {
dai_name "SSP2"
direction "playback"
}
}
}
}
SOF Driver 구조
/* sound/soc/sof/sof-priv.h */
struct snd_sof_dev {
struct device *dev;
struct snd_soc_component_driver plat_drv;
const struct sof_dev_desc *desc; /* 플랫폼 디스크립터 */
const struct snd_sof_dsp_ops *pdata; /* 플랫폼 ops */
struct snd_sof_ipc *ipc; /* IPC 컨텍스트 */
struct list_head pcm_list; /* PCM 리스트 */
struct list_head kcontrol_list; /* kcontrol 리스트 */
struct list_head widget_list; /* widget 리스트 */
struct list_head pipeline_list; /* pipeline 리스트 */
void __iomem *bar[SND_SOF_BARS]; /* MMIO BARs */
void *mailbox; /* IPC mailbox */
};
/* Platform ops */
struct snd_sof_dsp_ops {
int (*probe)(struct snd_sof_dev *sdev);
int (*remove)(struct snd_sof_dev *sdev);
/* Firmware loading */
int (*load_firmware)(struct snd_sof_dev *sdev);
int (*run_firmware)(struct snd_sof_dev *sdev);
/* IPC */
void (*send_msg)(struct snd_sof_dev *sdev,
struct snd_sof_ipc_msg *msg);
irqreturn_t (*irq_handler)(int irq, void *context);
irqreturn_t (*irq_thread)(int irq, void *context);
/* PCM ops */
int (*pcm_open)(struct snd_sof_dev *sdev,
struct snd_pcm_substream *substream);
int (*pcm_close)(struct snd_sof_dev *sdev,
struct snd_pcm_substream *substream);
int (*pcm_hw_params)(struct snd_sof_dev *sdev,
struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params);
snd_pcm_uframes_t (*pcm_pointer)(struct snd_sof_dev *sdev,
struct snd_pcm_substream *substream);
/* Power management */
int (*suspend)(struct snd_sof_dev *sdev);
int (*resume)(struct snd_sof_dev *sdev);
int (*runtime_suspend)(struct snd_sof_dev *sdev);
int (*runtime_resume)(struct snd_sof_dev *sdev);
};
/* IPC 메시지 전송 */
int sof_ipc_tx_message(struct snd_sof_ipc *ipc,
u32 header,
void *msg_data,
size_t msg_bytes,
void *reply_data,
size_t reply_bytes);
HD Audio (HDA) 서브시스템
HDA 버스 아키텍처
Intel HD Audio는 고품질 오디오 버스 표준으로, 하나의 컨트롤러에 여러 코덱을 연결할 수 있습니다. CORB (Command Output Ring Buffer)와 RIRB (Response Input Ring Buffer)로 명령/응답을 주고받습니다.
Codec Verb (명령 형식)
HDA 코덱은 Verb 명령으로 제어됩니다. Verb는 32비트 형식으로, 코덱 주소, Node ID (NID), 명령 ID, 파라미터를 포함합니다.
Verb Format (32-bit):
[31:28] Codec Address (4 bits, 0-14)
[27:20] Node ID (NID) (8 bits)
[19:8] Command/Verb ID (12 bits)
[7:0] Parameter/Payload (8 bits)
예:
0x01770500 → Codec 0, NID 0x17, Set Pin Widget Control, value 0x00
주요 Verb ID:
0xF00: Get Parameter (코덱 기능 정보 읽기)0x701-0x71C: Set/Get Pin Widget Control, Connection Select 등0x300-0x3FF: Amplifier Gain/Mute0xA00-0xAFF: Power State
Pin Configuration Register
각 핀 (잭)은 Configuration Default 레지스터로 물리적 속성을 정의합니다:
Pin Config (32-bit):
[31:30] Port Connectivity (Internal/Jack/Both/None)
[29:24] Location (Front/Rear/Top/Bottom/...)
[23:20] Default Device (Line Out/Speaker/HP/Mic/Line In/...)
[19:16] Connection Type (1/8", RCA, Optical, HDMI, ...)
[15:12] Color (Black/Grey/Blue/Green/Red/Orange/Yellow/White/...)
[11:8] Misc (Jack Detect Capable, ...)
[7:4] Default Association (Grouping)
[3:0] Sequence (Order in group)
예: 0x01014010 = Jack, Rear, Line Out, 1/8", Green, Jack Detect, Assoc 1, Seq 0 (Front Left).
Generic Parser와 Fixup
현대 HDA 드라이버는 generic parser를 사용하여 대부분의 코덱을 자동 지원합니다. 보드별 특이사항은 Fixup으로 처리:
/* sound/pci/hda/patch_realtek.c */
enum {
ALC269_FIXUP_DELL1_MIC_NO_PRESENCE,
ALC269_FIXUP_DELL2_MIC_NO_PRESENCE,
ALC269_FIXUP_LENOVO_DOCK,
ALC269_FIXUP_HP_GPIO_LED,
/* ... */
};
static const struct hda_fixup alc269_fixups[] = {
[ALC269_FIXUP_DELL1_MIC_NO_PRESENCE] = {
.type = HDA_FIXUP_PINS,
.v.pins = (const struct hda_pintbl[]) {
{ 0x19, 0x01a1913c }, /* headset mic w/o jack detect */
{ }
},
},
[ALC269_FIXUP_LENOVO_DOCK] = {
.type = HDA_FIXUP_FUNC,
.v.func = alc269_fixup_lenovo_dock,
.chained = true,
.chain_id = ALC269_FIXUP_PINCFG_NO_HP_TO_LINEOUT,
},
[ALC269_FIXUP_HP_GPIO_LED] = {
.type = HDA_FIXUP_FUNC,
.v.func = alc269_fixup_hp_gpio_led,
},
};
/* PCI SSID 기반 quirk 매칭 */
static const struct snd_pci_quirk alc269_fixup_tbl[] = {
SND_PCI_QUIRK(0x1028, 0x05ca, "Dell",
ALC269_FIXUP_DELL2_MIC_NO_PRESENCE),
SND_PCI_QUIRK(0x17aa, 0x21e9, "Lenovo ThinkCentre",
ALC269_FIXUP_LENOVO_DOCK),
SND_PCI_QUIRK(0x103c, 0x1983, "HP Pavilion",
ALC269_FIXUP_HP_GPIO_LED),
{}
};
USB Audio 심화
USB Audio Class 버전 비교
| 기능 | UAC 1.0 | UAC 2.0 | UAC 3.0 |
|---|---|---|---|
| 최대 샘플레이트 | 96 kHz | 384 kHz | 768 kHz |
| 최대 비트 깊이 | 24-bit | 32-bit | 32-bit |
| 최대 채널 | 8 | 32 | 32 |
| 클럭 소스 | 고정 | 다중 클럭 소스 | 다중 + 동기화 |
| 전원 관리 | 기본 | 고급 (Suspend/Resume) | 매우 고급 (Low Power) |
| 효과/처리 | 제한적 | Effect Units | Enhanced Processing |
| OS 지원 | 모든 OS | Windows 10+, Linux 2.6.35+ | Windows 10 RS5+, Linux 4.20+ |
Isochronous 전송 모드
USB Audio는 Isochronous Transfer로 오디오 스트림을 전송합니다. 지연시간 보장을 위해 에러 재전송 없이 고정 대역폭을 할당합니다.
- Asynchronous: 디바이스가 자체 클럭 사용. 피드백 엔드포인트로 샘플레이트 조정 요청.
- Adaptive: 디바이스가 호스트 SOF (Start of Frame) 클럭에 적응.
- Synchronous: 디바이스와 호스트가 동일 클럭 공유 (드물음).
/* sound/usb/pcm.c */
static int configure_endpoint(struct snd_usb_substream *subs)
{
struct audioformat *fmt = subs->cur_audiofmt;
unsigned int maxsize;
/* 패킷 크기 계산 */
maxsize = ((snd_usb_get_speed(subs->dev) == USB_SPEED_HIGH) ?
(125 * fmt->maxpacksize) : fmt->maxpacksize);
if (fmt->sync_ep) {
/* Async mode: 피드백 엔드포인트 설정 */
snd_usb_init_pitch(subs->stream->chip, subs->interface,
subs->altset_idx, fmt);
}
return snd_usb_init_sample_rate(subs->stream->chip,
subs->interface,
subs->altset_idx,
fmt,
fmt->rate_max);
}
snd-usb-audio 드라이버
리눅스 USB Audio 드라이버는 sound/usb/에 있으며, 대부분의 UAC 1.0/2.0/3.0 디바이스를 자동 지원합니다. 특이한 디바이스는 quirks 테이블로 처리:
/* sound/usb/quirks-table.h */
{
USB_DEVICE(0x0763, 0x2012), /* M-Audio Fast Track Pro */
.driver_info = (unsigned long)&(const struct snd_usb_audio_quirk) {
.vendor_name = "M-Audio",
.product_name = "Fast Track Pro",
.ifnum = QUIRK_ANY_INTERFACE,
.type = QUIRK_COMPOSITE,
.data = &(const struct snd_usb_audio_quirk[]) {
{
.ifnum = 0,
.type = QUIRK_AUDIO_STANDARD_MIXER,
},
{
.ifnum = 1,
.type = QUIRK_AUDIO_FIXED_ENDPOINT,
.data = &(const struct audioformat) {
.formats = SNDRV_PCM_FMTBIT_S24_3LE,
.channels = 8,
.iface = 1,
.altsetting = 1,
.altset_idx = 1,
.attributes = UAC_EP_CS_ATTR_SAMPLE_RATE,
.endpoint = 0x01,
.ep_attr = USB_ENDPOINT_XFER_ISOC |
USB_ENDPOINT_SYNC_ASYNC,
.rate_min = 44100,
.rate_max = 96000,
.nr_rates = 3,
.rate_table = (unsigned int[]){44100, 48000, 96000},
}
},
{
.ifnum = -1
}
}
}
},
전원 관리
Codec Suspend/Resume
ALSA 드라이버는 시스템 suspend/resume과 runtime PM을 지원합니다. ASoC는 regcache로 코덱 레지스터를 자동 백업/복원합니다.
/* ASoC component driver의 PM 콜백 */
static int wm8731_suspend(struct snd_soc_component *component)
{
/* 전원 OFF */
snd_soc_component_update_bits(component, WM8731_PWR, 0x40, 0x40);
/* Regmap cache로 레지스터 상태 자동 저장 */
regcache_cache_only(component->regmap, true);
regcache_mark_dirty(component->regmap);
return 0;
}
static int wm8731_resume(struct snd_soc_component *component)
{
/* Regmap cache에서 레지스터 복원 */
regcache_cache_only(component->regmap, false);
regcache_sync(component->regmap);
return 0;
}
static const struct snd_soc_component_driver wm8731_component = {
.suspend = wm8731_suspend,
.resume = wm8731_resume,
/* ... */
};
DAPM 자동 전원 제어
DAPM은 비활성 경로의 전원을 자동으로 차단합니다. Bias Level로 코덱 전원 상태를 관리:
enum snd_soc_bias_level {
SND_SOC_BIAS_OFF, /* 모든 전원 OFF */
SND_SOC_BIAS_STANDBY, /* 최소 전원 (빠른 wake-up) */
SND_SOC_BIAS_PREPARE, /* 스트림 시작 준비 */
SND_SOC_BIAS_ON, /* 완전 활성화 */
};
static int wm8731_set_bias_level(struct snd_soc_component *component,
enum snd_soc_bias_level level)
{
switch (level) {
case SND_SOC_BIAS_ON:
break;
case SND_SOC_BIAS_PREPARE:
/* Bias 전원 ON */
snd_soc_component_update_bits(component, WM8731_PWR,
0x180, 0);
break;
case SND_SOC_BIAS_STANDBY:
if (snd_soc_component_get_bias_level(component) == SND_SOC_BIAS_OFF) {
/* Fast VMID 충전 */
snd_soc_component_update_bits(component, WM8731_PWR,
0x180, 0x100);
msleep(100);
snd_soc_component_update_bits(component, WM8731_PWR,
0x180, 0x180);
}
break;
case SND_SOC_BIAS_OFF:
/* 모든 전원 OFF */
snd_soc_component_update_bits(component, WM8731_PWR,
0x1ff, 0x1ff);
break;
}
return 0;
}
Runtime Power Management
Runtime PM은 디바이스가 유휴 상태일 때 자동으로 저전력 모드로 전환합니다:
/* HDA 드라이버 runtime PM 예제 */
static int azx_runtime_suspend(struct device *dev)
{
struct snd_card *card = dev_get_drvdata(dev);
struct azx *chip = card->private_data;
/* 모든 스트림 중지 확인 */
if (!azx_has_pm_runtime(chip))
return 0;
/* 코덱 전원 OFF */
azx_stop_chip(chip);
azx_enter_link_reset(chip);
/* 컨트롤러 클럭 OFF */
azx_vs_set_state(chip->pci, DISABLED);
return 0;
}
static int azx_runtime_resume(struct device *dev)
{
struct snd_card *card = dev_get_drvdata(dev);
struct azx *chip = card->private_data;
/* 컨트롤러 클럭 ON */
azx_vs_set_state(chip->pci, ACTIVE);
/* 컨트롤러 재초기화 */
azx_init_chip(chip, true);
/* 코덱 재초기화 */
snd_hdac_set_codec_wakeup(&chip->bus, true);
snd_hdac_set_codec_wakeup(&chip->bus, false);
return 0;
}
static const struct dev_pm_ops azx_pm = {
SET_SYSTEM_SLEEP_PM_OPS(azx_suspend, azx_resume)
SET_RUNTIME_PM_OPS(azx_runtime_suspend, azx_runtime_resume, NULL)
};
/* Runtime PM 활성화 */
pm_runtime_use_autosuspend(&pci->dev);
pm_runtime_set_autosuspend_delay(&pci->dev, 3000); /* 3초 유휴 후 suspend */
pm_runtime_put_noidle(&pci->dev);
pm_runtime_allow(&pci->dev);
DMA와 버퍼 관리
Ring Buffer 구조
ALSA PCM은 ring buffer로 오디오 데이터를 관리합니다. hw_ptr (하드웨어 포인터)와 appl_ptr (애플리케이션 포인터)가 독립적으로 진행됩니다.
DMA Cache Coherency
DMA 버퍼는 CPU 캐시와 일관성을 유지해야 합니다:
/* Coherent DMA (권장) */
runtime->dma_area = dma_alloc_coherent(&pci->dev,
size,
&runtime->dma_addr,
GFP_KERNEL);
/* → CPU와 DMA가 동일 메모리 뷰 공유 (캐시 플러시 불필요) */
/* Non-coherent DMA (성능 향상 가능하나 복잡) */
runtime->dma_area = dma_alloc_noncoherent(&pci->dev,
size,
&runtime->dma_addr,
DMA_BIDIRECTIONAL,
GFP_KERNEL);
/* → 드라이버가 수동으로 캐시 동기화 필요:
* dma_sync_single_for_cpu() / dma_sync_single_for_device()
*/
DMA Buffer 타입 비교
| 타입 | 할당 방법 | 특징 | 용도 |
|---|---|---|---|
SNDRV_DMA_TYPE_CONTINUOUS |
kmalloc / vmalloc |
물리적으로 비연속적 가능 | 소프트웨어 mixing, dummy |
SNDRV_DMA_TYPE_DEV |
dma_alloc_coherent |
물리적 연속, 캐시 coherent | 대부분의 하드웨어 DMA (권장) |
SNDRV_DMA_TYPE_DEV_UC |
dma_alloc_coherent + uncached |
캐시 비활성화 (매우 느림) | 특수한 경우만 사용 |
SNDRV_DMA_TYPE_DEV_SG |
Scatter-Gather 리스트 | 물리적으로 비연속적, IOMMU 필요 | 대용량 버퍼, IOMMU 지원 플랫폼 |
SNDRV_DMA_TYPE_DEV_IRAM |
On-chip SRAM | 초저지연, 용량 제한 | 임베디드, 실시간 처리 |
SNDRV_DMA_TYPE_VMALLOC |
vmalloc |
가상 연속, 물리 비연속 | USB Audio (URB 전송) |
지연시간 최적화
지연시간 구성 요소
총 오디오 지연시간 = 버퍼 지연 + 프로세싱 지연 + 하드웨어 지연
Total Latency = (period_size * periods / sample_rate) + processing + hardware
예: 48kHz, 64 frames/period, 2 periods
= (64 * 2 / 48000) = 2.67ms (버퍼)
+ 0.5ms (processing, OS overhead)
+ 1.0ms (코덱 DAC 지연)
= 약 4.2ms
| 용도 | 목표 지연시간 | 권장 설정 |
|---|---|---|
| 일반 재생 (음악, 영상) | 50-200ms | period=1024, periods=4 |
| 게이밍 | 10-30ms | period=256, periods=2 |
| 프로페셔널 오디오 (DAW) | 2-10ms | period=64, periods=2 |
| 실시간 처리 (라이브 모니터링) | <3ms | period=32, periods=2 + RT kernel |
XRUN 방지
저지연 설정은 XRUN (Underrun/Overrun) 위험이 높습니다. 다음 기법으로 안정성을 높입니다:
- RT 스케줄링: 오디오 프로세스에
SCHED_FIFO또는SCHED_RR부여 - 메모리 잠금:
mlockall(MCL_CURRENT | MCL_FUTURE)로 페이지 스왑 방지 - CPU Isolation: 특정 CPU 코어를 오디오 전용으로 격리 (
isolcpus커널 파라미터) - IRQ Affinity: 오디오 IRQ를 특정 CPU에 고정
- Governor 설정:
performanceCPU governor 사용 (frequency scaling 비활성화) - PREEMPT_RT 커널: 실시간 커널 패치 적용
/* 사용자 공간: RT 우선순위 설정 */
#include <sched.h>
#include <sys/mman.h>
struct sched_param param;
param.sched_priority = 80; /* 1-99, 높을수록 우선순위 높음 */
if (sched_setscheduler(0, SCHED_FIFO, ¶m) != 0)
perror("sched_setscheduler");
/* 메모리 잠금 (페이지 fault 방지) */
if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0)
perror("mlockall");
/* PCM 열기 */
snd_pcm_open(&pcm, "hw:0,0", SND_PCM_STREAM_PLAYBACK, 0);
/* 저지연 설정 */
snd_pcm_hw_params_set_access(pcm, params, SND_PCM_ACCESS_MMAP_INTERLEAVED);
snd_pcm_hw_params_set_format(pcm, params, SND_PCM_FORMAT_S16_LE);
snd_pcm_hw_params_set_rate(pcm, params, 48000, 0);
snd_pcm_hw_params_set_channels(pcm, params, 2);
snd_pcm_uframes_t period = 64;
unsigned int periods = 2;
snd_pcm_hw_params_set_period_size_near(pcm, params, &period, 0);
snd_pcm_hw_params_set_periods_near(pcm, params, &periods, 0);
/* SW params: 즉시 wake-up */
snd_pcm_sw_params_set_avail_min(pcm, swparams, period);
snd_pcm_sw_params_set_start_threshold(pcm, swparams, 1);
지연시간 측정 도구
cyclictest: 커널 지연시간 측정 (RT 성능 평가)jack_iodelay: JACK 라운드트립 지연 측정pw-top: PipeWire 그래프 지연 모니터링/proc/asound/card*/pcm*/sub*/status: hw_ptr/appl_ptr 실시간 모니터링
$ cyclictest -p 80 -t 1 -n -i 500 -l 10000
T: 0 ( 1234) P:80 I:500 C: 10000 Min: 3 Act: 5 Avg: 4 Max: 23
$ cat /proc/asound/card0/pcm0p/sub0/status
state: RUNNING
owner_pid : 5678
trigger_time: 1234567890.123456789
tstamp : 1234567890.234567890
delay : 128
avail : 3968
avail_max : 4096
hw_ptr : 98304
appl_ptr : 98432
가상화 환경 오디오
virtio-snd
virtio-snd는 가상 머신에서 효율적인 오디오 전송을 위한 paravirtualized 드라이버입니다.
/* drivers/virtio/virtio_snd.c (simplified) */
struct virtio_snd {
struct virtio_device *vdev;
struct virtqueue *queues[VIRTIO_SND_VQ_MAX];
/* Control, Event, TX, RX queues */
struct snd_card *card;
struct list_head pcm_list;
struct list_head ctl_msgs;
};
/* virtqueue를 통한 PCM 데이터 전송 */
static int virtsnd_pcm_msg_send(struct virtio_pcm_substream *substream)
{
struct virtqueue *vqueue = substream->vqueue;
struct scatterlist sg;
sg_init_one(&sg, substream->buffer, substream->buffer_bytes);
return virtqueue_add_outbuf(vqueue, &sg, 1, substream, GFP_KERNEL);
}
QEMU Audio 백엔드
| Backend | 설명 | 지연시간 |
|---|---|---|
-audiodev pa |
PulseAudio (호스트) | 중간 (20-50ms) |
-audiodev alsa |
ALSA 직접 (호스트) | 낮음 (5-20ms) |
-audiodev pipewire |
PipeWire (호스트) | 낮음 (5-15ms) |
-audiodev spice |
SPICE 프로토콜 | 높음 (50-200ms) |
-device intel-hda |
에뮬레이트된 HDA | 중간 (10-30ms) |
-device virtio-sound |
virtio-snd (paravirt) | 낮음 (5-10ms) |
$ qemu-system-x86_64 \
-audiodev pa,id=snd0 \
-device intel-hda \
-device hda-duplex,audiodev=snd0
# 또는 virtio-snd 사용 (최신 QEMU + 게스트 커널 5.13+)
$ qemu-system-x86_64 \
-audiodev pa,id=snd0 \
-device virtio-sound-pci,audiodev=snd0
디버깅과 진단
사용자 공간 도구
| 도구 | 용도 | 예제 |
|---|---|---|
aplay |
WAV 파일 재생 | aplay -D hw:0,0 test.wav |
arecord |
오디오 녹음 | arecord -D hw:0,0 -f S16_LE -r 48000 out.wav |
amixer |
믹서 제어 | amixer sset Master 80% |
alsactl |
ALSA 상태 저장/복원 | alsactl store / alsactl restore |
speaker-test |
채널 테스트 (톤 생성) | speaker-test -c 2 -t wav |
alsa-info |
시스템 정보 수집 | alsa-info --no-upload |
cat /proc/asound/* |
커널 상태 읽기 | cat /proc/asound/cards |
# 사용 가능한 PCM 디바이스 나열
$ aplay -l
**** List of PLAYBACK Hardware Devices ****
card 0: PCH [HDA Intel PCH], device 0: ALC892 Analog [ALC892 Analog]
Subdevices: 1/1
Subdevice #0: subdevice #0
card 0: PCH [HDA Intel PCH], device 3: HDMI 0 [HDMI 0]
Subdevices: 1/1
Subdevice #0: subdevice #0
# 하드웨어 파라미터 확인
$ cat /proc/asound/card0/pcm0p/sub0/hw_params
access: MMAP_INTERLEAVED
format: S16_LE
subformat: STD
channels: 2
rate: 48000 (48000/1)
period_size: 1024
buffer_size: 4096
# Control elements 나열
$ amixer -c 0 contents
numid=1,iface=MIXER,name='Master Playback Volume'
; type=INTEGER,access=rw---R--,values=2,min=0,max=87,step=0
: values=70,70
| dBscale-min=-65.25dB,step=0.75dB,mute=0
numid=2,iface=MIXER,name='Master Playback Switch'
; type=BOOLEAN,access=rw------,values=2
: values=on,on
/proc/asound와 debugfs
# HDA 코덱 정보 (매우 상세)
$ cat /proc/asound/card0/codec#0
Codec: Realtek ALC892
Address: 0
AFG Function Id: 0x1 (unsol 1)
Vendor Id: 0x10ec0892
Subsystem Id: 0x14627b71
Revision Id: 0x100302
No Modem Function Group found
Default PCM:
rates [0x5f0]: 32000 44100 48000 88200 96000 192000
bits [0xe]: 16 20 24
formats [0x1]: PCM
...
Node 0x02 [Audio Output] wcaps 0x11: Stereo
Control: name="Front Playback Volume", index=0, device=0
ControlAmp: chs=3, dir=Out, idx=0, ofs=0
Device: name="ALC892 Analog", type="Audio", device=0
Converter: stream=5, channel=0
PCM:
rates [0x5f0]: 32000 44100 48000 88200 96000 192000
bits [0xe]: 16 20 24
formats [0x1]: PCM
Power states: D0 D1 D2 D3 EPSS
Power: setting=D0, actual=D0
# debugfs (CONFIG_DEBUG_FS 필요)
$ sudo cat /sys/kernel/debug/asound/card0/pcm0p/sub0/xrun_debug
0
# XRUN 디버그 활성화
$ echo 1 | sudo tee /sys/kernel/debug/asound/card0/pcm0p/sub0/xrun_debug
$ dmesg | tail
[12345.678] ALSA: PCM: [Q] Lost interrupts?: (stream=0, delta=128, new_hw_ptr=256, old_hw_ptr=128)
커널 디버깅
# CONFIG_SND_DEBUG, CONFIG_SND_VERBOSE_PRINTK 활성화 후 커널 빌드
# Dynamic debug (runtime)
$ echo 'module snd_hda_intel +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
$ echo 'file sound/pci/hda/* +p' | sudo tee /sys/kernel/debug/dynamic_debug/control
# ftrace로 ALSA 함수 추적
$ sudo su
# cd /sys/kernel/debug/tracing
# echo function_graph > current_tracer
# echo snd_pcm_* > set_ftrace_filter
# echo 1 > tracing_on
# cat trace_pipe
aplay-1234 [002] .... 1234.567: snd_pcm_open() {
aplay-1234 [002] .... 1234.568: snd_pcm_open_substream() {
aplay-1234 [002] .... 1234.569: snd_pcm_attach_substream() {
aplay-1234 [002] .... 1234.570: azx_pcm_open() {
...
자주 발생하는 문제
| 증상 | 원인 | 해결법 |
|---|---|---|
| 소리 안 남 | 뮤트 상태, 잘못된 디바이스 | amixer로 볼륨 확인, aplay -L로 디바이스 확인 |
| Crackling/Popping | 버퍼 너무 작음, CPU 부하 | period/buffer 크기 증가, CPU governor 조정 |
| XRUN 발생 | 실시간 스케줄링 부족 | RT 우선순위 설정, buffer 크기 증가 |
| 높은 지연시간 | 버퍼 너무 큼, PulseAudio | period/buffer 감소, ALSA 직접 사용 |
| 한쪽 채널만 나옴 | 잘못된 채널맵, 불량 케이블 | speaker-test로 테스트, 하드웨어 확인 |
| USB Audio 끊김 | USB 전원 관리 | echo on > /sys/bus/usb/devices/*/power/control |
| HDMI 소리 안 남 | ELD 정보 없음, 모니터 대기 | cat /proc/asound/card*/eld*, 모니터 전원 확인 |
| Headphone detection 안 됨 | Pin configuration 오류 | HD Audio: hdajackretask 또는 커널 파라미터 |
커널 소스 구조와 Kconfig
sound/ 디렉터리 구조
sound/
├── core/ # ALSA Core
│ ├── init.c # 카드 초기화
│ ├── device.c # snd_device 관리
│ ├── pcm.c # PCM core
│ ├── pcm_native.c # PCM ioctl
│ ├── pcm_lib.c # PCM 헬퍼
│ ├── pcm_memory.c # 버퍼 할당
│ ├── control.c # Control 인터페이스
│ ├── hwdep.c # HW Dep
│ ├── rawmidi.c # Raw MIDI
│ ├── timer.c # Timer
│ ├── seq/ # Sequencer
│ └── compress_offload.c # Compress API
│
├── drivers/ # 가상/테스트 드라이버
│ ├── dummy.c # Dummy 사운드 카드
│ ├── aloop.c # Loopback
│ └── virmidi.c # Virtual MIDI
│
├── pci/ # PCI 사운드 카드
│ ├── hda/ # HD Audio
│ │ ├── hda_intel.c # Intel HDA 컨트롤러
│ │ ├── hda_codec.c # Codec core
│ │ ├── hda_generic.c # Generic parser
│ │ ├── patch_realtek.c # Realtek codecs
│ │ ├── patch_hdmi.c # HDMI audio
│ │ └── ...
│ ├── ac97/ # AC'97 codec 지원
│ ├── intel8x0.c # Intel ICH
│ ├── emu10k1/ # Sound Blaster Live/Audigy
│ ├── via82xx.c # VIA chipset
│ └── ...
│
├── usb/ # USB Audio
│ ├── card.c # USB Audio 카드
│ ├── pcm.c # PCM 스트림
│ ├── mixer.c # Mixer
│ ├── quirks.c # 디바이스별 quirks
│ └── quirks-table.h # Quirk 테이블
│
├── soc/ # ASoC (System on Chip)
│ ├── soc-core.c # ASoC core
│ ├── soc-pcm.c # PCM operations
│ ├── soc-dapm.c # DAPM 엔진
│ ├── soc-topology.c # Topology 로더
│ ├── soc-compress.c # Compress offload
│ ├── codecs/ # Codec 드라이버
│ │ ├── wm8731.c
│ │ ├── rt5640.c
│ │ ├── da7219.c
│ │ └── ... (100+ codecs)
│ ├── generic/ # Generic machine drivers
│ │ ├── simple-card.c
│ │ └── audio-graph-card.c
│ ├── intel/ # Intel platform drivers
│ │ ├── boards/ # Machine drivers
│ │ ├── common/ # 공통 코드
│ │ └── skylake/ # Skylake DSP
│ ├── sof/ # Sound Open Firmware
│ │ ├── core.c
│ │ ├── ipc.c
│ │ ├── pcm.c
│ │ ├── topology.c
│ │ ├── intel/ # Intel DSP
│ │ └── amd/ # AMD DSP
│ ├── fsl/ # Freescale/NXP i.MX
│ ├── qcom/ # Qualcomm
│ ├── samsung/ # Samsung Exynos
│ ├── rockchip/ # Rockchip
│ ├── mediatek/ # MediaTek
│ └── ...
│
├── firewire/ # FireWire audio
├── i2c/ # I2C-based audio chips
├── isa/ # ISA 사운드 카드 (레거시)
├── mips/ # MIPS platform audio
├── parisc/ # PA-RISC platform audio
├── pcmcia/ # PCMCIA 사운드 카드
├── ppc/ # PowerPC platform audio
├── sh/ # SuperH platform audio
├── sparc/ # SPARC platform audio
├── spi/ # SPI-based audio chips
├── synth/ # Software synthesizers
├── virtio/ # Virtio sound
│ └── virtio_card.c
├── x86/ # x86 platform drivers
└── xen/ # Xen paravirt audio
주요 Kconfig 옵션
| 옵션 | 설명 | 권장 |
|---|---|---|
CONFIG_SOUND |
사운드 카드 지원 (최상위) | Y |
CONFIG_SND |
Advanced Linux Sound Architecture | Y |
CONFIG_SND_TIMER |
ALSA Timer | Y |
CONFIG_SND_PCM |
PCM 서브시스템 | Y |
CONFIG_SND_HWDEP |
Hardware Dependent 인터페이스 | Y |
CONFIG_SND_RAWMIDI |
Raw MIDI | Y (MIDI 사용 시) |
CONFIG_SND_COMPRESS_OFFLOAD |
Compress Offload API | Y (모바일/임베디드) |
CONFIG_SND_SEQUENCER |
ALSA Sequencer | Y (MIDI 라우팅 필요 시) |
CONFIG_SND_SEQ_DUMMY |
Sequencer Dummy client | Y |
CONFIG_SND_OSSEMUL |
OSS 에뮬레이션 | N (레거시, 불필요) |
CONFIG_SND_PCM_OSS |
OSS PCM 에뮬레이션 | N |
CONFIG_SND_DYNAMIC_MINORS |
동적 minor 번호 할당 | Y (많은 디바이스) |
CONFIG_SND_SUPPORT_OLD_API |
구형 API 지원 | N |
CONFIG_SND_PROC_FS |
/proc/asound/ 인터페이스 | Y (디버깅) |
CONFIG_SND_VERBOSE_PROCFS |
상세한 /proc 정보 | Y (디버깅) |
CONFIG_SND_VERBOSE_PRINTK |
상세한 printk 메시지 | Y (개발), N (프로덕션) |
CONFIG_SND_DEBUG |
디버그 체크 | Y (개발), N (프로덕션) |
CONFIG_SND_DEBUG_VERBOSE |
매우 상세한 디버그 | N (성능 영향) |
CONFIG_SND_PCM_XRUN_DEBUG |
XRUN 디버그 (debugfs) | Y (개발) |
CONFIG_SND_VMASTER |
Virtual Master control | Y |
CONFIG_SND_DMA_SGBUF |
Scatter-Gather DMA | Y (x86) |
CONFIG_SND_HDA_INTEL |
Intel HD Audio (PCI) | Y (데스크톱/노트북) |
CONFIG_SND_HDA_HWDEP |
HDA hwdep 인터페이스 | Y |
CONFIG_SND_HDA_RECONFIG |
HDA 재설정 (sysfs) | Y (디버깅) |
CONFIG_SND_HDA_PATCH_LOADER |
HDA Patch 로더 | Y |
CONFIG_SND_HDA_CODEC_REALTEK |
Realtek HD Audio codecs | Y (대부분 필요) |
CONFIG_SND_HDA_CODEC_HDMI |
HDMI/DisplayPort audio | Y |
CONFIG_SND_HDA_POWER_SAVE_DEFAULT |
HDA 전원 절약 타임아웃 (초) | 1 (aggressive) ~ 60 (safe) |
CONFIG_SND_USB_AUDIO |
USB Audio Class 1/2/3 | Y |
CONFIG_SND_USB_UA101 |
Edirol UA-101/UA-1000 (전용) | N (특정 디바이스만) |
CONFIG_SND_USB_CAIAQ |
Native Instruments USB audio | N (특정 디바이스만) |
CONFIG_SND_SOC |
ALSA System on Chip (ASoC) | Y (임베디드/SoC) |
CONFIG_SND_SOC_TOPOLOGY |
ASoC Topology | Y (DSP 시스템) |
CONFIG_SND_SOC_INTEL_SST_TOPLEVEL |
Intel SST (Smart Sound Technology) | Y (Intel 플랫폼) |
CONFIG_SND_SOC_SOF_TOPLEVEL |
Sound Open Firmware (SOF) | Y (최신 Intel/AMD) |
CONFIG_SND_SOC_SOF_PCI |
SOF PCI/PCIe 지원 | Y |
CONFIG_SND_SOC_SOF_INTEL_TOPLEVEL |
Intel DSP 지원 | Y |
CONFIG_SND_SIMPLE_CARD |
Simple Audio Card (DT) | Y (임베디드) |
CONFIG_SND_AUDIO_GRAPH_CARD |
Audio Graph Card (DT) | Y (임베디드) |
CONFIG_SND_VIRTIO |
Virtio sound driver | Y (가상 머신 게스트) |
빌드 예제
# menuconfig에서 ALSA 활성화
$ make menuconfig
Device Drivers --->
<*> Sound card support --->
<*> Advanced Linux Sound Architecture --->
[*] PCI sound devices --->
<*> Intel HD Audio --->
[*] Build hwdep interface
[*] Build Realtek HD-audio codec support
[*] Build HDMI/DisplayPort HD-audio codec support
[*] USB sound devices --->
<*> USB Audio/MIDI driver
<*> ALSA for SoC audio support --->
[*] Sound Open Firmware Support --->
<*> SOF PCI enumeration support
<*> Intel Platforms
# 또는 .config 직접 편집
CONFIG_SOUND=y
CONFIG_SND=y
CONFIG_SND_TIMER=y
CONFIG_SND_PCM=y
CONFIG_SND_HDA_INTEL=y
CONFIG_SND_HDA_CODEC_REALTEK=y
CONFIG_SND_HDA_CODEC_HDMI=y
CONFIG_SND_USB_AUDIO=y
CONFIG_SND_SOC=y
CONFIG_SND_SOC_SOF_PCI=y
# 빌드
$ make -j$(nproc)
# 모듈만 설치 (드라이버 업데이트 시)
$ sudo make modules_install
$ sudo depmod -a
# 모듈 로드
$ sudo modprobe snd-hda-intel
$ sudo modprobe snd-usb-audio
CONFIG_SND_DEBUG와 CONFIG_SND_DEBUG_VERBOSE는 디버그 메시지를 대량 출력하여 성능에 영향을 줍니다. 프로덕션 시스템에서는 비활성화해야 합니다.
CONFIG_DYNAMIC_DEBUG=y 설정으로 런타임에 디버그 메시지를 활성화할 수 있습니다:
echo 'file sound/core/pcm_native.c +p' > /sys/kernel/debug/dynamic_debug/control