pcm用來描述alsa中數字音頻流。Alsa音頻的播放/錄制就是通過pcm來實現 的。
名詞解釋
聲音是連續模擬量,計算機將它離散化之后用數字表示,就有了以下幾個名詞術語。
Frame. 幀是音頻流中最小的單位,一段音頻數據就是由苦干幀組成的。
Channel. 通道表示每幀數據中包含的通道數。單聲道音頻Mono含有 1個通道,立體聲Stereo音頻通常為2個通道。
Bit Depth. 位深,也叫采樣精度,計算機對每個通道采樣量化時數字比特位數,通常有16/24/32位。
Frames Per Second. 采樣率表示每秒的采樣幀數。常用的采樣率如8KHz的人聲, 44.1KHz的mp3音樂, 96Khz的藍光音頻。
Bits Per Second. 比特率表示每秒的比特數。
上面幾個量有換算關系:比特率=采樣率×通道數×位深. 下圖是8K采樣率下 16bits/400Hz的單聲道正弦波音頻。pcm數據就是圖上采樣點幅值的16bit表示。
數據結構
snd_pcm結構用於表征一個PCM類型的snd_device.
struct snd_pcm {
struct snd_card *card; /* 指向所屬的card設備 */
int device; /* device number */
struct snd_pcm_str streams[2]; /* 播放和錄制兩個數據流 */
wait_queue_head_t open_wait; /* 打開pcm設備時等待打開一個可獲得的substream */
}
struct snd_pcm_str {
int stream; /* stream (direction) */
struct snd_pcm *pcm; /* 指向所屬的pcm設備 */
/* -- substreams -- */
unsigned int substream_count; /* 個數 */
unsigned int substream_opened; /* 在使用的個數 */
struct snd_pcm_substream *substream; /* 指向substream單鏈表 */
}
文件/proc/asound/cardX/pcmXp/info可以查看pcm的信息。一個pcm設備包含播 放/錄制兩個流,每個流有若干個substream.一個substream只能被一個進程占用。 snd_pcm_substream才是真正實現音頻的播放或錄制的結構。
struct snd_pcm_substream {
struct snd_pcm *pcm;
struct snd_pcm_str *pstr;
void *private_data; /* copied from pcm->private_data */
int number;
char name[32]; /* substream name */
int stream; /* stream (direction) */ /* 錄制/播放 */
struct pm_qos_request latency_pm_qos_req; /* pm_qos request */
size_t buffer_bytes_max; /* limit ring buffer size */
struct snd_dma_buffer dma_buffer;
unsigned int dma_buf_id;
size_t dma_max;
/* -- hardware operations -- */
const struct snd_pcm_ops *ops;
/* -- runtime information -- */
struct snd_pcm_runtime *runtime;
/* -- timer section -- */
struct snd_timer *timer; /* timer */
unsigned timer_running: 1; /* time is running */
/* -- next substream -- */
struct snd_pcm_substream *next;
/* -- linked substreams -- */
struct list_head link_list; /* linked list member */
struct snd_pcm_group self_group; /* fake group for non linked substream (with substream lock inside) */
struct snd_pcm_group *group; /* pointer to current group */
/* -- assigned files -- */
void *file; /* 指向 pcm_file, 不知道有什么用? */
int ref_count; /* 引用計數,打開 O_APPEND 時有用 */
atomic_t mmap_count; /* mmap 的引用計數 */
unsigned int f_flags; /* pcm 打開的文件標記 */
void (*pcm_release)(struct snd_pcm_substream *);
struct pid *pid; /* 所在進程的pid,有多個substream時用於選擇使用哪個 */
/* misc flags */
unsigned int hw_opened: 1; /* 若已打開,在釋放substream時需要調用close() */
};
文件/proc/asound/cardX/pcmXp/subX/info可以查看這個substream的信息。這 個結構里兩個最重要的成員是runtime和ops.
snd_pcm_ops是substream的操作方法集。
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); /* 用於實現幾個特定的IOCTL1_{RESET,INFO,CHANNEL_INFO,GSTATE,FIFO_SIZE} */
int (*hw_params)(struct snd_pcm_substream *substream,
struct snd_pcm_hw_params *params); /* 用於設定pcm參數,如采樣率/位深... */
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 (*wall_clock)(struct snd_pcm_substream *substream,
struct timespec *audio_ts); /* 通過hw獲得audio_tstamp */
int (*copy)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos,
void __user *buf, snd_pcm_uframes_t count); /* 除dma外的hw自身實現的數據傳輸方法 */
int (*silence)(struct snd_pcm_substream *substream, int channel,
snd_pcm_uframes_t pos, snd_pcm_uframes_t count); /* hw靜音數據的填充方法 */
struct page *(*page)(struct snd_pcm_substream *substream,
unsigned long offset); /* 硬件分配緩沖區的方法 */
int (*mmap)(struct snd_pcm_substream *substream, struct vm_area_struct *vma); /* */
int (*ack)(struct snd_pcm_substream *substream); /* 通知硬件寫了一次數據 */
};
這些操作方法集由各種聲卡如PCI,USB,SOC等子模塊來實現。
snd_pcm_runtime用於表示substream運行時狀態。
struct snd_pcm_runtime {
/* -- Status -- */ /* */
/* -- HW params -- */ /* 當前流的數據格式 */
/* -- SW params -- */ /* 用戶配置的參數如pcm_config */
/* -- mmap -- */
struct snd_pcm_mmap_status *status; /* 當前硬件指針位置及其狀態 */
struct snd_pcm_mmap_control *control; /* 當前的應用指針及其狀態 */
/* -- locking / scheduling -- */ /* 用於通知如數據空閑/溢出等事件 */
/* -- private section -- */
/* -- hardware description -- */ /* 硬件支持的參數及參數之間的約束條件 */
/* -- interrupt callbacks -- */ /* HW一次中斷傳輸完畢時的回調,似乎沒有哪個模塊用到它? */
void (*transfer_ack_begin)(struct snd_pcm_substream *substream);
void (*transfer_ack_end)(struct snd_pcm_substream *substream);
/* -- timer -- */
/* -- DMA -- */
struct snd_dma_buffer *dma_buffer_p; /* allocated buffer */
}
這是相當大的一個結構體,自帶的注釋很明晰,就不貼它的成員了。它反映了一個 substream運行時的狀態及實時信息。文件/proc/asound/*/subX/可以得到這個 結構的大部分信息。
PCM的狀態轉換
下圖是PCM的狀態的轉換圖。
除XRUN狀態之后,其它的狀態大多都由用戶空間的ioctl()顯式的切換。 以TinyAlsa的播放音頻流程為例。 pcm_open()的對應的流程就是:
open(pcm)后綁定一個substream,處於OPEN狀態
ioctl(SNDRV_PCM_IOCTL_SW_PARAMS)設定參數pcm_config.配置 runtime 的 sw_para.切換到SETUP狀態
Tinyalsa的pcm_wirte()流程:
ioctl(SNDRV_PCM_IOCTL_PREPARE)后,substream切換到PREPARE狀態。
ioctl(SNDRV_PCM_IOCTL_WRITEI_FRAMES)后,substream切換到RUNNING狀態。
TinyAlsa的pcm_mmap_write()流程:
ioctl(SNDRV_PCM_IOCTL_PREPARE)后,substream切換到PREPARE狀態。
ioctl(SNDRV_PCM_IOCTL_START)后,substream切換到RUNNING狀態。
TinyAlsa pcm_close流程:
ioctl(SNDRV_PCM_IOCTL_DROP)后,切換回SETUP狀態。
close()之后,釋放這個設備。
XRUN狀態又分有兩種,在播放時,用戶空間沒及時寫數據導致緩沖區空了,硬件沒有 可用數據播放導致UNDERRUN;錄制時,用戶空間沒有及時讀取數據導致緩沖區滿后溢出, 硬件錄制的數據沒有空閑緩沖可寫導致OVERRUN.
緩沖區的管理
音頻的緩沖區是典型的只有一個讀者和一個寫者的FIFO結構。 下圖是ALSA中FIFO緩沖區的示意圖。
上圖以播放時的緩沖管理為例,runtime->boundary一般都是較大的數,ALSA中默認接近 LONG_MAX/2.這樣FIFO的出隊入隊指針不是真實的緩沖區的地址偏移,經過轉換才得到 物理緩沖的偏移。這樣做的好處是簡化了緩沖區的管理,只有在更新hw指針的時候才需 要換算到hw_ofs.
當用戶空間由於系統繁忙等原因,導致hw_ptr>appl_ptr時,緩沖區已空,內核這里有兩種方案:
停止DMA傳輸,進入XRUN狀態。這是內核默認的處理方法。
繼續播放緩沖區的重復的音頻數據或靜音數據。
用戶空間配置stop_threshold可選擇方案1或方案2,配置silence_threshold選擇繼 續播放的原有的音頻數據還是靜意數據了。個人經驗,偶爾的系統繁忙導致的這種狀態, 重復播放原有的音頻數據會顯得更平滑,效果更好。
實現
pcm的代碼讓人難以理解的部分莫過於硬件指針的更新snd_pcm_update_hw_ptr0(),分 析見這里。它是將hw_ofs轉換成FIFO中 hw_ptr的過程,同時處理環形緩沖區的回繞,沒有中斷,中斷丟失等情況。
還有一處就是處理根據硬件參數的約束條件得到參數的代碼 snd_pcm_hw_refine(substream, params). 留待以后分析吧。
調試
sound/core/info.c是alsa為proc實現的接口。這也是用戶空間來調試內核alsa 最主要的方法了。打開內核配置選項 CONFIG_SND_VERBOSE_PROCFS/CONFIG_SND_PCM_XRUN_DEBUG,可看到以下的目錄樹。
/proc/asound/
|-- card0
| |-- id 聲卡名
| |-- pcm0c
| | |-- info pcm設備信息
| | |-- sub0
| | | |-- hw_params 硬件配置參數
| | | |-- info substream設備信息
| | | |-- status 實時的hw_ptr/appl_ptr
| | | `-- sw_params 軟件配置參數
| | `-- xrun_debug 控制內核alsa的調試日志輸出
| `-- pcm0p
|-- cards 內核擁有的聲卡
|-- devices 內核所有的snd_device設備
|-- pcm 所有的pcm設備
`-- version alsa的版本號
在ALSA播放/錄制異常時,若打開xrun_debug,內核日志會實時打印更多有用的信息, 往/proc/asound/card0/pcm0p/xrun_debug寫入相應的掩碼就好了。
#define XRUN_DEBUG_BASIC (1<<0)
#define XRUN_DEBUG_STACK (1<<1) /* dump also stack */
#define XRUN_DEBUG_JIFFIESCHECK (1<<2) /* do jiffies check */
#define XRUN_DEBUG_PERIODUPDATE (1<<3) /* full period update info */
#define XRUN_DEBUG_HWPTRUPDATE (1<<4) /* full hwptr update info */
#define XRUN_DEBUG_LOG (1<<5) /* show last 10 positions on err */
#define XRUN_DEBUG_LOGONCE (1<<6) /* do above only once */
相當冗長的一篇總結。與其它內核模塊比起來,這部分代碼似乎顯得更“晦澀”,原因 之一可能就是音頻流是實時的數據,而內核本身不是實時的系統,軟件上不能很好的保 證hw_ptr和appl_ptr的同步。