ALSA 서브시스템

ALSA(Advanced Linux Sound Architecture)를 리눅스 오디오 데이터 경로의 표준 기반으로 심층 분석합니다. PCM 재생·녹음 파이프라인과 ring buffer/period 모델, control 인터페이스와 mixer 토폴로지, MIDI/타이머 서브시스템, HD Audio·USB Audio·SOF 아키텍처 차이, DPCM과 compress offload 활용, DMA 기반 저지연 스트리밍, xrun 복구와 clock drift 대응, 전원관리 및 suspend/resume 안정화, 사용자 공간(PulseAudio/PipeWire) 연계 포인트까지 실전 오디오 드라이버 운영에 필요한 핵심을 다룹니다.

문서 구조 재정렬: 이 문서는 ALSA 코어 흐름 중심으로 유지합니다. 플랫폼별 심화는 ASoC & DAPM, USB 서브시스템 문서를 함께 참고하세요.
전제 조건: 디바이스 드라이버DMA 문서를 먼저 읽으세요. 멀티미디어/가속기 경로는 대용량 버퍼 이동과 동기화가 성능의 핵심이므로, 메모리 경로와 큐 모델을 먼저 파악해야 합니다.
일상 비유: 이 주제는 영상 제작 파이프라인과 비슷합니다. 촬영·편집·인코딩 단계가 끊기지 않아야 결과가 나오듯이, 버퍼 큐와 하드웨어 스케줄링의 연속성이 중요합니다.

핵심 요약

  • 초기화 순서 — 탐색, 바인딩, 자원 등록 순서를 점검합니다.
  • 제어/데이터 분리 — 빠른 경로와 설정 경로를 분리 설계합니다.
  • IRQ/작업 분할 — 즉시 처리와 지연 처리를 구분합니다.
  • 안전 한계 — 전원/열/타이밍 임계값을 함께 관리합니다.
  • 운영 복구 — 오류 시 재초기화와 롤백 경로를 준비합니다.

단계별 이해

  1. 장치 수명주기 확인
    probe부터 remove까지 흐름을 점검합니다.
  2. 비동기 경로 설계
    IRQ, 워크큐, 타이머 역할을 분리합니다.
  3. 자원 정합성 검증
    DMA/클록/전원 참조를 교차 확인합니다.
  4. 현장 조건 테스트
    연결 끊김/복구/부하 상황을 재현합니다.
관련 표준 및 사양:
  • 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는 다음과 같은 개선사항을 제공합니다:

사용자 공간 스택

현대 리눅스 오디오 스택은 커널 ALSA 위에 여러 계층으로 구성됩니다:

계층 컴포넌트 역할
애플리케이션 Firefox, Chrome, VLC, OBS 최종 오디오 소비자/생산자
세션 매니저 PipeWire, PulseAudio, JACK 오디오 라우팅, 믹싱, 재샘플링
라이브러리 libasound (alsa-lib) 커널 ALSA API 래퍼
커널 ALSA 드라이버 하드웨어 제어
PipeWire: 최신 배포판은 PipeWire를 기본 오디오 서버로 채택하고 있으며, PulseAudio와 JACK의 호환 레이어를 제공하면서 낮은 지연시간과 프로페셔널 오디오 지원을 통합합니다.

커널 구조

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) 스트림을 의미합니다.

전체 아키텍처 다이어그램

User Space Applications (Firefox, VLC) PipeWire (Audio Server) JACK (Pro Audio) libasound (alsa-lib) Utilities (aplay, amixer) Kernel Space ALSA Core (sound/core/) PCM snd_pcm Control snd_ctl MIDI snd_rawmidi Timer snd_timer HW Dep snd_hwdep Compress snd_compr Driver Layer ASoC Framework sound/soc/ DAPM DPCM Topology SOF HD Audio sound/pci/hda/ Intel HDA Codecs Generic Parser Quirks USB Audio sound/usb/ UAC 1/2/3 Isochronous Adaptive Sync Quirks PCI/Other sound/pci/ intel8x0 emu10k1 via82xx dummy/aloop Hardware Audio Codec DSP/Firmware DMA Engine I2S/TDM/PDM /dev/snd/ controlC* pcmC*D* midiC*D* Bus: PCI / PCIe / USB / Platform (I2C, SPI) Physical Audio I/O (Speakers, Microphones, Line-in/out, S/PDIF, HDMI)
프로세스 흐름: 애플리케이션 → 오디오 서버 (PipeWire/PulseAudio) → libasound → /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/` 디버그 인터페이스 구조 /proc/asound/ 전역 정보 파일 cards (카드 요약) devices (전체 디바이스) timers (타이머) pcm (PCM 목록) 시스템 전체 상태 관찰 카드별 디렉터리 card0/, card1/, ... id (카드 ID) codec#0 (HDA 코덱) eld#0.0 (HDMI ELD) 카드 하드웨어 상세 PCM 서브스트림 pcm0p/, pcm0c/, ... sub0/info sub0/hw_params sub0/sw_params, status 실시간 스트림 상태 추적 활용 포인트 드라이버 디버깅: 카드 인식/코덱 상태/PCM 파라미터 확인 커스텀 엔트리: `snd_card_proc_new()` + `snd_info_set_text_ops()`로 추가 예: `/proc/asound/card0/my_status`

드라이버는 커스텀 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 오디오는 다음 파라미터로 정의됩니다:

Bandwidth 계산:
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);
};

콜백 설명:

하드웨어 제약 시스템

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 상태 머신

OPEN SETUP PREPARED RUNNING XRUN DRAINING PAUSED SUSPENDED DISCONNECTED hw_params() prepare() START Underrun/Overrun prepare() DRAIN 완료 PAUSE RESUME PM suspend PM resume STOP Device removal 상태 설명: OPEN: 스트림 열림 (hw 미설정) | SETUP: hw_params 설정 완료 | PREPARED: 재생/녹음 준비 완료 RUNNING: 스트림 활성화 | XRUN: 버퍼 Underrun/Overrun | DRAINING: 버퍼 비우는 중 PAUSED: 일시정지 | SUSPENDED: 전원 관리 대기 | DISCONNECTED: 디바이스 제거됨
XRUN (Underrun/Overrun): Playback Underrun은 애플리케이션이 충분히 빠르게 데이터를 공급하지 못해 버퍼가 고갈된 상태. Capture Overrun은 애플리케이션이 데이터를 충분히 빠르게 읽지 못해 버퍼가 넘친 상태. XRUN 발생 시 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;
}
snd_pcm_period_elapsed(): 이 함수는 하나의 period가 완료될 때마다 호출해야 합니다 (보통 DMA 인터럽트 핸들러에서). 이 호출로 사용자 공간의 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 이름 예시:

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 스트림과 동기화하거나 독립적인 타이머로 사용할 수 있습니다.

/* 타이머 생성 */
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 디바이스에 접근합니다.

DPCM (Dynamic PCM)

Front-End / Back-End 분리

DPCM (Dynamic PCM)은 ASoC의 고급 기능으로, 하나의 PCM 스트림을 여러 백엔드 (코덱/디지털 인터페이스)로 동적 라우팅할 수 있습니다. 주로 DSP 기반 시스템에서 사용됩니다.

User Space Applications Music Player Voice Call Notification FE DAI 0 (pcmC0D0p) FE DAI 1 (pcmC0D1p) FE DAI 2 (pcmC0D2p) DSP Routing / Mixing (Dynamic path selection via DAPM) BE: Codec (WM8731) HP/SPK BE: BT (Bluetooth) A2DP/SCO BE: HDMI (Digital) 8ch BE: S/PDIF (Optical) 2ch FE PCM은 DAPM 경로에 따라 동적으로 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"},
};
Trigger 전파: DPCM에서는 FE의 START/STOP 트리거가 활성 BE에 자동 전파됩니다. 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 기반 오디오 파이프라인을 구성합니다.

User Space ALSA libasound PipeWire / PulseAudio ALSA Core (PCM/Control) sound/core/ SOF Driver sound/soc/sof/ IPC, Topology, Power Mgmt Firmware loading Intel PCI/ACPI sof-pci-dev.c HDA/ADSP AMD Platform amd/ Renoir/Rembrandt DSP Firmware (SOF) /lib/firmware/intel/sof/ Audio Pipeline: Host PCM → Volume → Mixer → DAI Out Components: Volume, EQ, Compressor Sample Rate Converter Echo Cancellation (AEC) IPC Messages (Mailbox, Doorbell IRQ) Hardware Codec (I2S/HDA)

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)로 명령/응답을 주고받습니다.

HDA Controller (Intel ICH/PCH) CORB (Command Out Ring Buffer) RIRB (Response In Ring Buffer) DMA Engines (BDL) Playback / Capture Streams (up to 30) HD Audio Link (Serial bus, 24MHz, 48MHz) Codec 0 Analog Audio Realtek ALC892 HP/Mic/LineOut Codec 1 HDMI Audio Intel Display Audio 8ch PCM Codec 2 Internal Modem (Optional) Voice/Fax Codec 3 DisplayPort Audio (Optional) Multi-stream Addr 0 Addr 1 Addr 2 Addr 3 최대 15개 코덱 연결 가능 (Addr 0-14). CORB/RIRB로 Verb 명령 송수신. DMA는 오디오 데이터 전송.

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:

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로 오디오 스트림을 전송합니다. 지연시간 보장을 위해 에러 재전송 없이 고정 대역폭을 할당합니다.

/* 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);
자동 Suspend: Runtime PM은 마지막 PCM 스트림이 닫힌 후 지정된 시간 (autosuspend delay) 동안 유휴 상태가 지속되면 자동으로 디바이스를 저전력 모드로 전환합니다. 새 스트림이 열리면 자동으로 resume됩니다.

DMA와 버퍼 관리

Ring Buffer 구조

ALSA PCM은 ring buffer로 오디오 데이터를 관리합니다. hw_ptr (하드웨어 포인터)와 appl_ptr (애플리케이션 포인터)가 독립적으로 진행됩니다.

Period 0 Period 1 Period 2 Period 3 hw_ptr appl_ptr Available Playback: appl_ptr: 애플리케이션이 쓴 데이터 끝 hw_ptr: 하드웨어가 재생한 데이터 끝 Available Frames: avail = buffer_size - (appl_ptr - hw_ptr) Playback: 쓸 수 있는 공간 Capture: 읽을 수 있는 데이터 avail >= period_size 시 애플리케이션 wake-up Underrun (Playback): hw_ptr가 appl_ptr를 따라잡으면 (버퍼 고갈) XRUN 발생 → 노이즈/끊김 Overrun (Capture): appl_ptr가 hw_ptr를 따라잡으면 (버퍼 넘침) XRUN 발생 → 데이터 손실

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 전송)

지연시간 최적화

지연시간 구성 요소

총 오디오 지연시간 = 버퍼 지연 + 프로세싱 지연 + 하드웨어 지연

ALSA 레이턴시 구성 요소 분석 Total Latency = Buffer Latency + Processing Latency + Hardware Latency 예: 48kHz, period_size=64 frames, periods=2 (64 × 2 / 48000) + 0.5ms + 1.0ms = 2.67ms + 0.5ms + 1.0ms ≈ 4.2ms 레이턴시 스택 (시간 축) Buffer Latency period_size × periods / sample_rate = 2.67ms Processing 0.5ms OS overhead Hardware 1.0ms 코덱 DAC 지연 총 레이턴시 ≈ 4.2ms 주요 파라미터 period_size IRQ 주기당 처리 프레임 수 작을수록 레이턴시 ↓ XRUN 위험 ↑ periods 링 버퍼 내 period 수 최소 2 (더블 버퍼링) buffer_size = period×periods sample_rate 샘플/초 (44100 / 48000 / 96000) 높을수록 같은 period_size에서 레이턴시 ↓ (시간 단축) 용도별 목표 레이턴시 일반 재생 50-200ms / period=1024 게이밍 10-30ms / period=256 프로페셔널 오디오 2-10ms / period=64 실시간 (RT 커널) <3ms / period=32
용도 목표 지연시간 권장 설정
일반 재생 (음악, 영상) 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 우선순위 설정 */
#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 -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, pcm.c, pcm_native.c pcm_lib.c, pcm_memory.c, control.c hwdep.c, rawmidi.c, timer.c, seq/ compress_offload.c drivers/ + pci/ drivers/: dummy.c, aloop.c, virmidi.c pci/hda: hda_intel.c, hda_codec.c patch_realtek.c, patch_hdmi.c, ac97/ intel8x0.c, emu10k1/, via82xx.c usb/ + 기타 버스 usb/: card.c, pcm.c, mixer.c, quirks.c firewire/, i2c/, isa/, pcmcia/, spi/ virtio/, x86/, xen/, ppc/, sparc/ 등 soc/ (ASoC) soc-core.c, soc-pcm.c, soc-dapm.c soc-topology.c, soc-compress.c codecs/: wm8731.c, rt5640.c, da7219.c generic/: simple-card.c, audio-graph-card.c intel/, sof/, qcom/, samsung/, mediatek/ rockchip/, fsl/ 등 플랫폼 머신 드라이버 구조 해석 1) core/는 ALSA 공통 ABI(PCM/Control/MIDI/Timer)를 제공합니다. 2) bus별 드라이버(pci/usb/...)는 하드웨어 제어와 DMA 처리를 담당합니다. 3) soc/는 Codec + CPU DAI + Machine 드라이버를 결합해 모바일 오디오를 구성합니다. 4) SOF는 DSP 오프로딩, Topology 로딩, IPC 경로를 담당합니다. 5) 실무에서는 core/ ABI를 기준으로 드라이버 계층을 추적하면 디버깅이 빠릅니다.

주요 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_DEBUGCONFIG_SND_DEBUG_VERBOSE는 디버그 메시지를 대량 출력하여 성능에 영향을 줍니다. 프로덕션 시스템에서는 비활성화해야 합니다.
동적 디버그: 디버그 심볼 없이 빌드한 커널에서도 CONFIG_DYNAMIC_DEBUG=y 설정으로 런타임에 디버그 메시지를 활성화할 수 있습니다:
echo 'file sound/core/pcm_native.c +p' > /sys/kernel/debug/dynamic_debug/control

ALSA 서브시스템과 관련된 다른 문서를 더 깊이 이해하고 싶다면 다음을 참고하세요.