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는 다음과 같은 개선사항을 제공합니다:

사용자 공간 스택

현대 리눅스 오디오 스택은 커널 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/
├── 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 오디오는 다음 파라미터로 정의됩니다:

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 디바이스에 접근합니다.

ASoC 프레임워크 심화

ASoC (ALSA System on Chip)는 임베디드/SoC 플랫폼을 위한 고수준 오디오 프레임워크입니다. 코덱, 플랫폼 (CPU DAI + DMA), 머신 (보드 특화) 드라이버를 분리하여 재사용성을 극대화합니다.

ASoC 아키텍처

ASoC는 세 계층으로 구성됩니다:

Machine Driver (Board-specific) snd_soc_card, dai_link[], DAPM routes, jack detection Example: sound/soc/fsl/imx-wm8731.c Platform Driver (CPU DAI) SoC Audio Interface • I2S/PCM/TDM controller • DMA engine integration • Clock management Codec Driver Audio Codec Chip • Regmap (I2C/SPI control) • DAPM widgets • Kcontrols (mixer) Hardware SoC I2S/TDM/PCM DMA Controller Hardware Codec IC (WM8731, RT5640...) DAC/ADC, Amplifier I2S/TDM Bus (BCLK, LRCLK, SDATA) I2C/SPI Control Machine driver는 dai_link로 CPU DAI와 Codec DAI를 연결하여 하나의 PCM 디바이스 생성

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 그래프 시각화

Capture Path LLINEIN RLINEIN MICIN Mic Bias (SUPPLY) Input Mux Line / Mic / CD (MUX) Input PGA (Volume) ADC (A/D Conv) AIF IN (I2S to CPU) Playback Path AIF OUT (I2S from CPU) DAC (D/A Conv) Output Mixer DAC + Bypass + Sidetone (MIXER) Line Bypass HP Amp (PGA) SPK Amp (PGA) LHPOUT (Headphone) RHPOUT (Headphone) LSPK (Speaker) RSPK (Speaker) DAPM은 활성 경로의 모든 widget만 전원 ON (사용 안 하는 DAC/ADC/Amp 자동 OFF → 전력 절감) Powered ON Powered OFF External I/O Audio Interface
자동 전원 관리: 사용자가 재생 또는 녹음을 시작하면 DAPM은 활성 경로를 추적하여 필요한 widget만 전원을 켭니다. 예: 헤드폰 재생 시 DAC → Output Mixer → HP Amp → LHPOUT/RHPOUT 경로의 widget만 ON, 스피커 앰프와 ADC는 OFF 상태 유지.

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

지연시간 최적화

지연시간 구성 요소

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

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