Linux音頻子系統(7) - PCM

  • 瞭解PCM

1.PCM

  PCM(Pulse-code modulation)脈衝編碼調製,是將模擬信號轉化爲數字信號的一種方法。聲音的轉化的過程爲,先對連續的模擬信號按照固定頻率週期性採樣,將採樣到的數據按照一定的精度進行量化,量化後的信號和採樣後的信號差值叫做量化誤差,將量化後的數據進行最後的編碼存儲,最終模擬信號變化爲數字信號。

  與音頻採樣相關的名詞:

  • Sample : 樣本長度. 代表一個聲道的音頻數據, 大小取決於採樣深度, 常見的有8位和16位.
  • Channel : 聲道數. 常見的有單聲道、雙聲道(立體聲)、5.1聲道、7.1聲道等.
  • Frame : 幀, 構成一個完整的聲音單元. Frame = Channel * Sample, 例如對於單聲道, 它的大小是1Sample, 對於5.1聲道, 它的大小是6Sample.
    這裏幀的概念類似於LCD中的幀, 它是硬件傳送的基本單位. 例如I2S每次傳輸一個完整的幀, 當一幀傳輸完畢後, 會產生一個硬件中斷.
  • Rate : 採樣率, 指採樣一幀數據所需的時間. 例如44.1KHz代表採樣一幀數據需要1s/44100 = 0.022ms.
  • Period : 週期. 當用戶空間把音頻數據寫入RAM後, 底層是用DMA把數據從RAM搬移到I2S的FIFO. DMA每搬移完一段數據後就會產生一次中斷,. 我們把DMA搬移這段數據的過程稱爲一個週期. 這段數據的大小是可以配置的, 因此用戶空間可以設置Period的大小, 如果週期設定得較大, 則單次搬移的數據較多, 這意味着單位時間內硬件中斷的次數較少, CPU 也就有更多時間處理其他任務, 功耗也更低, 但這樣也帶來一個顯著的弊端——數據處理的時延會增大.
  • period_size : 一個週期內的幀數. 它間接決定了週期的大小. 用戶空間在設定週期大小時, 給定的參數也是這個幀數.
  • period_bytes : 對於DMA硬件來說, 它只關心數據具體有多少字節. period_bytes = period_size * Channels * Sample / 8.
  • Buffer : 代表一塊RAM, 用戶空間與內核通過這塊RAM交換數據. DMA也是從這塊RAM搬移數據. 一塊Buffer內包含多個Period.
  • buffer_size : 一塊Buffer內幀數, 這塊Buffer內包含多個period.
  • buffer_bytes : Buffer以字節爲單位的大小, 通常在分配RAM時需要該數據. buffer_bytes = buffer_size * Channels * Sample / 8.

  PCM兩個重要屬性:

  • 採樣率: 單位時間內採樣的次數,採樣頻率越高越高,
  • 採樣位數: 一個採樣信號的位數,也是對採樣精度的變現。

  人類而言,能接受聲音的頻率範圍是20Hz-20KHz, 所以採樣的頻率44.1KHz 以及16bit的採樣位數就可以有很好的保真能力(CD格式的採樣率和採樣位數)。

2.PCM中間層

  ALSA已經實現了功能強勁的PCM中間層,自己的驅動中只要實現一些底層的需要訪問硬件的函數即可。

  要訪問PCM的中間層代碼,首先要包含頭文件<sound/pcm.h>,另外如果需要訪問一些與 hw_param相關的函數,可能也要包含<sound/pcm_params.h>。

  每個聲卡最多可以包含4個pcm的實例,每個pcm實例對應一個pcm設備文件。pcm實例數量的這種限制源於linux設備號所佔用的位大小,如果以後使用64位的設備號,我們將可以創建更多的pcm實例。不過大多數情況下,在嵌入式設備中,一個pcm實例已經足夠了。

  一個pcm實例由一個playback stream和一個capture stream組成,這兩個stream又分別有一個或多個substreams組成。
在這裏插入圖片描述
  在嵌入式系統中,大多數情況下是一個聲卡,一個pcm實例,pcm下面有一個playback和capture stream,playback和capture下面各自有一個substream。

  • 一個pcm實例(例如pcm0)是card下的一個邏輯設備, 這個邏輯設備會在用戶空間創建兩個設備節點.

  • 一個pcm實例包含兩個stream : playback & capture. 每個stream對應一個設備節點.

  • 每個stream下可包含多個substream.

  在內核層, 每個substream都有一塊自己的Buffer來與用戶空間交換音頻數據. 從這個角度來看, substream存在的意義貌似是爲了分時複用底層的音頻硬件.

  在用戶空間, 每個設備節點可以被open多次, 每次open內核層都會找到一個空閒的substream與之對應, 如果內核層的substream被用完了, 則此次open操作會失敗. 這樣看來, 用戶空間的讀、寫、控制操作都是針對substream進行的, 這也進一步說明substream可以用來分時複用底層音頻硬件.

3.數據結構
在這裏插入圖片描述

  • snd_pcm是掛在snd_card下面的一個snd_device;
  • snd_pcm中的字段:streams[2],該數組中的兩個元素指向兩個snd_pcm_str結構,分別代表playback stream和capture stream;
  • snd_pcm_str中的substream字段,指向snd_pcm_substream結構;
  • snd_pcm_substream是pcm中間層的核心,絕大部分任務都是在substream中處理,尤其是他的ops(snd_pcm_ops)字段,許多user空間的應用程序通過alsa-lib對驅動程序的請求都是由該結構中的函數處理。它的runtime字段則指向snd_pcm_runtime結構,snd_pcm_runtime記錄這substream的一些重要的軟件和硬件運行環境和參數。

3.1.struct snd_pcm (代表一個pcm實例, 也是代表一個pcm邏輯設備)

  在ALSA架構下,pcm也被稱爲設備,所謂的邏輯設備。在linux系統中使用snd_pcm結構表示一個pcm設備。

struct snd_pcm {
	struct snd_card *card;
	struct list_head list;
	int device; /* device number */
	unsigned int info_flags;
	unsigned short dev_class;
	unsigned short dev_subclass;
	char id[64];
	char name[80];
	struct snd_pcm_str streams[2];      //指向它下屬的playback stream和capture stream. 
	struct mutex open_mutex;
	wait_queue_head_t open_wait;
	void *private_data;
	void (*private_free) (struct snd_pcm *pcm);
	struct device *dev; /* actual hw device this belongs to */
	bool internal; /* pcm is for internal use only */
	bool nonatomic; /* whole PCM operations are in non-atomic context */
#if defined(CONFIG_SND_PCM_OSS) || defined(CONFIG_SND_PCM_OSS_MODULE)
	struct snd_pcm_oss oss;
#endif
}

3.2.struct snd_pcm_str (代表一個pcm stream)

struct snd_pcm_str {
	int stream;				/* stream (direction) */
	struct snd_pcm *pcm;
	/* -- substreams -- */
	unsigned int substream_count;
	unsigned int substream_opened;
	struct snd_pcm_substream *substream;
#if IS_ENABLED(CONFIG_SND_PCM_OSS)
	/* -- OSS things -- */
	struct snd_pcm_oss_stream oss;
#endif
#ifdef CONFIG_SND_VERBOSE_PROCFS
	struct snd_info_entry *proc_root;
	struct snd_info_entry *proc_info_entry;
#ifdef CONFIG_SND_PCM_XRUN_DEBUG
	unsigned int xrun_debug;	/* 0 = disabled, 1 = verbose, 2 = stacktrace */
	struct snd_info_entry *proc_xrun_debug_entry;
#endif
#endif
	struct snd_kcontrol *chmap_kctl; /* channel-mapping controls */
	struct device dev;
};
  • dev : 一個steam對應一個字符設備節點. 這的dev與創建字符設備節點有關.
  • substream_count : 該stream下屬的substream的個數.
  • substream_opened : 有多少個substream已經被用戶空間open了. 用戶空間可以針對同一個設備節點open多次, 每次open內核層都會選一個空閒的substream與之對應. 如果所有的substream都被opened, 則新的open會失敗.
  • substream : 用鏈表的形式串聯多個substream.

3.3.struct snd_pcm_file

  • 每個被open的substream對應一個snd_pcm_file.
struct snd_pcm_file {
	struct snd_pcm_substream *substream;
	int no_compat_mmap;
	unsigned int user_pversion;	/* supported protocol version */
};

3.4.struct snd_pcm_substream

  • 代表一個pcm substream. substream的一個重要功能就是要準備一塊DMA buffer, 以便與用戶空間交換數據.
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;
	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;
	int ref_count;
	atomic_t mmap_count;
	unsigned int f_flags;
	void (*pcm_release)(struct snd_pcm_substream *);
	struct pid *pid;
#if IS_ENABLED(CONFIG_SND_PCM_OSS)
	/* -- OSS things -- */
	struct snd_pcm_oss_substream oss;
#endif
#ifdef CONFIG_SND_VERBOSE_PROCFS
	struct snd_info_entry *proc_root;
	struct snd_info_entry *proc_info_entry;
	struct snd_info_entry *proc_hw_params_entry;
	struct snd_info_entry *proc_sw_params_entry;
	struct snd_info_entry *proc_status_entry;
	struct snd_info_entry *proc_prealloc_entry;
	struct snd_info_entry *proc_prealloc_max_entry;
#ifdef CONFIG_SND_PCM_XRUN_DEBUG
	struct snd_info_entry *proc_xrun_injection_entry;
#endif
#endif /* CONFIG_SND_VERBOSE_PROCFS */
	/* misc flags */
	unsigned int hw_opened: 1;
};

3.5.struct snd_dma_buffer

  • 用於描述一塊DMA buffer.
struct snd_dma_buffer {
	struct snd_dma_device dev;	/* device type */
	unsigned char *area;	/* virtual pointer */
	dma_addr_t addr;	/* physical address */
	size_t bytes;		/* buffer size in bytes */
	void *private_data;	/* private for allocator; don't touch */
};
  • area : buffer的虛擬地址, 供CPU訪問buffer時使用.
  • addr : buffer的物理地址, 供DMA訪問buffer時使用.
  • bytes : buffer的大小.

3.6.struct snd_pcm_ops

  • PCM中間層定義的需要底層驅動實現的接口函數, 相當於interface. 中間層在恰當的時候會回調這些接口函數. 從底層驅動的角度來說, 絕大部分工作就是實現這個ops定義的函數(一般只需實現部分, 其它的中間層都有默認實現. 除非中間層的實現在自己的硬件上用不了, 才需要我們自己實現.), 然後向PCM中間層‘註冊’即可.
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 (*get_time_info)(struct snd_pcm_substream *substream,
			struct timespec *system_ts, struct timespec *audio_ts,
			struct snd_pcm_audio_tstamp_config *audio_tstamp_config,
			struct snd_pcm_audio_tstamp_report *audio_tstamp_report);
	int (*fill_silence)(struct snd_pcm_substream *substream, int channel,
			    unsigned long pos, unsigned long bytes);
	int (*copy_user)(struct snd_pcm_substream *substream, int channel,
			 unsigned long pos, void __user *buf,
			 unsigned long bytes);
	int (*copy_kernel)(struct snd_pcm_substream *substream, int channel,
			   unsigned long pos, void *buf, unsigned long bytes);
	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);
};

3.7.snd_pcm_new

int snd_pcm_new(struct snd_card *card, const char *id, int device, int playback_count, int capture_count, struct snd_pcm rpcm);

  • 參數device 表示目前創建的是該聲卡下的第幾個pcm,第一個pcm設備從0開始.
  • 參數playback_count 表示該pcm將會有幾個playback substream.
  • 參數capture_count 表示該pcm將會有幾個capture substream.

在這裏插入圖片描述

函數實現:

  • 構建一個struct snd_pcm數據結構來代表一個PCM實例
  • 調用snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_PLAYBACK, playback_count)構建playback stream, 並創建playback_count個substream.
  • 調用snd_pcm_new_stream(pcm, SNDRV_PCM_STREAM_CAPTURE, capture_count)構建capture stream, 並創建capture_count個substream.
  • 最後, 調用snd_device_new把這個實例作爲一個邏輯設備添加到card->devices鏈表下.

  請注意, 在card被註冊之前, 我們需要調用snd_pcm_set_ops爲此PCM實例設置回調函數, 因爲當用戶空間通過設備節點與PCM中間層交互時, PCM中間層需要回調底層驅動實現的ops函數.

3.8.設置pcm操作函數接口

void snd_pcm_set_ops(struct snd_pcm *pcm, int direction, struct snd_pcm_ops *ops);

3.9.PCM字符設備的創建

  當card被註冊時, 會掃描下屬的每個邏輯設備並註冊它們, 這裏創建的PCM邏輯設備也會在那時進行註冊. 當PCM邏輯設備被註冊時, ALSA系統層會回調邏輯設備的snd_device_ops. dev_register函數, 也就是snd_pcm_dev_register. 在該回調函數中, 會針對每一個stream調用snd_register_device, 進而在用戶空間創建對應的設備節點.

  PCM中間層的snd_pcm_f_ops會負責與用戶空間交互, 其主要功能包括:

  • open / release : 打開或者關閉某substream.
  • read / write / mmap : 用戶空間與PCM中間層交換音頻數據.
  • ioctl : 提供各種各樣的控制接口.

4.pcm設備創建完成邏輯圖
在這裏插入圖片描述
    大體上就是一棵樹,根節點是card0, 然後子節點是pcm設備,pcm設備分爲capture & playback stream, 然後在stream下又分爲substrem。

5.整個流程梳理
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章