內核Alsa之pcm


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的同步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM