Linux ALSA音频系统:soundcard
8.1聲卡和PCM設備的建立過程
前面分析了codec,platform,machine驅動的組成部分及其注冊過程,這三者都是物理設備相關的。
??? pcm邏輯設備,我們習慣稱之為PCM中間層或pcm native,起著承上啟下的作用:往上是與用戶態接口的交互,實現音頻數據在用戶和內核態之間的拷貝;往下是觸發codec,platform,machine的操作函數,實現音頻數據在dma_buffer<->cpu_dai<->codec之間的傳輸。
聲卡驅動中,一般掛載著多個邏輯設備,看看我們計算機的聲卡驅動有幾個邏輯設備:
$ cat /proc/asound/devices 2: [ 0] : control3: [ 0- 0]: digital audio playback4: [ 0- 0]: digital audio capture5: [ 0- 3]: digital audio playback6: [ 0- 7]: digital audio playback7: [ 0- 8]: digital audio playback8: [ 0- 0]: hardware dependent9: [ 0- 3]: hardware dependent33: : timer?
| digital audio playback | 用于回放的 PCM 設備 |
| digital audio capture | 用于錄制的 PCM 設備 |
| control | 用于聲卡控制的 CTL 設備,如通路控制、音量調整等 |
| timer | 定時器設備 |
| sequencer | 音序器設備 |
嵌入式系統中,我們更關心PCM和CTL這兩種設備。
$ ll /dev/snd/ total 0 drwxr-xr-x 2 root root 60 7月 14 22:19 by-path crw-rw----+ 1 root audio 116, 2 7月 14 22:19 controlC0 crw-rw----+ 1 root audio 116, 8 7月 14 22:19 hwC0D0 crw-rw----+ 1 root audio 116, 9 7月 14 22:19 hwC0D3 crw-rw----+ 1 root audio 116, 4 7月 15 13:43 pcmC0D0c crw-rw----+ 1 root audio 116, 3 7月 15 15:35 pcmC0D0p crw-rw----+ 1 root audio 116, 5 7月 14 22:20 pcmC0D3p crw-rw----+ 1 root audio 116, 6 7月 14 22:20 pcmC0D7p crw-rw----+ 1 root audio 116, 7 7月 14 22:20 pcmC0D8p crw-rw----+ 1 root audio 116, 1 7月 14 22:19 seq crw-rw----+ 1 root audio 116, 33 7月 14 22:19 timer可以看到這些設備節點的Major=116,Minor則與/proc/asound/devices所列的對應起來,都是字符設備。上層可以通過open/close/read/write/ioctl等系統調用來操作聲卡設備,這和其他字符設備類似,但一般情況下我們會使用已封裝好的用戶接口庫如alsa-lib。
8.2聲卡結構概述
回顧下ASoC是如何注冊聲卡的,這里僅簡單陳述下:
- Machine驅動初始化時,.name = "soc-audio"的platform_device與platform_driver匹配成功,觸發soc_probe()調用;
- 繼而調用snd_soc_register_card():??
- 隨后調用soc_new_pcm():
- 最后調用snd_card_register()注冊聲卡。
下面詳細分析聲卡和PCM邏輯設備的注冊過程。
上面提到聲卡驅動上掛著多個邏輯子設備,有pcm音頻數據流,control混音器,midi,timer定時器,sequencer音序器等。
?
+-----------+| snd_card |+-----------+| | |+-----------+ | +------------+| | | +-----------+ +-----------+ +-----------+| snd_pcm | |snd_control| | snd_timer | ...+-----------+ +-----------+ +-----------+這些與聲音相關的邏輯設備都在結構體snd_card管理之下,可以說snd_card是alsa中最頂層的結構。我們在看看alsa聲卡驅動的大致結構圖。
?
snd_cards:記錄著所注冊的聲卡實例,每個聲卡實例有著各自的邏輯設備,如PCM設備,CTL設備,MIDI設備等,并一一記錄到snd_card的device鏈表上
snd_minors:記錄著所有邏輯設備的上下文信息,它是聲卡邏輯設備與系統調用api之間的橋梁;每個snd_minor在邏輯設備注冊時被填充,在邏輯設備使用時就可以從該結構中的到相應的信息(主要是系統調用函數集file_operations)
8.3聲卡的創建
聲卡實例通過函數snd_card_new()來創建,其函數原型:
/*** snd_card_new - create and initialize a soundcard structure* @parent: the parent device object* @idx: card index (address) [0 ... (SNDRV_CARDS-1)]* @xid: card identification (ASCII string)* @module: top level module for locking* @extra_size: allocate this extra size after the main soundcard structure* @card_ret: the pointer to store the created card instance** Creates and initializes a soundcard structure.** The function allocates snd_card instance via kzalloc with the given* space for the driver to use freely. The allocated struct is stored* in the given card_ret pointer.** Return: Zero if successful or a negative error code.*/int snd_card_new(struct device *parent, int idx, const char *xid, struct module *module, int extra_size,struct snd_card **card_ret)主要參數說明:
- parent: 父設備對象
- idx:聲卡的編號,如為-1,則由系統自動分配
- xid:聲卡標識符,如為NULL,則以snd_card的shortname或longname代替
- card_ret:返回所創建的聲卡實例的指針
如下是我ubuntu16.04的聲卡信息:
$ cat /proc/asound/cards0 [PCH ]: HDA-Intel - HDA Intel PCHHDA Intel PCH at 0xf2530000 irq 31- number:? 0
- id: PCH
- shortname: HDA Intel PCH
- longname:HDA Intel PCH at 0xf7c30000 irq 47
shortname,longname常用于打印信息,上面的聲卡信息是通過如下函數打印出來的:
static void snd_card_info_read(struct snd_info_entry *entry,struct snd_info_buffer *buffer){int idx, count;struct snd_card *card;for (idx = count = 0; idx < SNDRV_CARDS; idx++) {mutex_lock(&snd_card_mutex);if ((card = snd_cards[idx]) != NULL) {count++;snd_iprintf(buffer, "%2i [%-15s]: %s - %s\n",idx,card->id,card->driver,card->shortname);snd_iprintf(buffer, " %s\n",card->longname);}mutex_unlock(&snd_card_mutex);}if (!count)snd_iprintf(buffer, "--- no soundcards ---\n");}8.4邏輯設備的創建
當聲卡實例建立后,接著可以創建聲卡下面的各個邏輯設備看。每個邏輯設備創建式,都會調用snd_device_new()生成一個snd_device實例,并把該實例掛到聲卡snd_card的devices鏈表上。alsa驅動為各種邏輯設備提供了創建接口,如下:
| PCM | snd_pcm_new() |
| CONTROL | snd_ctl_create() |
| MIDI | snd_rawmidi_new() |
| TIMER | snd_timer_new() |
| SEQUENCER | snd_seq_device_new() |
| JACK | snd_jack_new() |
這些接口的一般過程如下:
int snd_xxx_new() {//這些接口提供邏輯設備注冊時回調static struct snd_device_ops ops = {.dev_free = snd_xxx_dev_free,.dev_register = snd_xxx_dev_register,.dev_disconnect = snd_xxx_dev_disconnect,};//邏輯設備實例初始化//新建一個設備實例snd_device,掛到snd_card的devices鏈表上,把該邏輯設備納入聲卡的管理當中,SNDRV_DEV_XXX是邏輯設備的類型snd_device_new(card, SNDRV_DEV_XXX, xxx, &ops); }其中snd_device_ops是聲卡邏輯設備的注冊函數集,dev_register()回調尤其重要,它在聲卡注冊時被調用,用于建立系統的設備節點,/dev/snd/目錄的設備節點都是在這里創建的,通過這些設備節點可系統調用open/release/read/write/ioctl..訪問操作該邏輯設備。
snd_ctl_dev_register():
static const struct file_operations snd_ctl_f_ops ={ .owner = THIS_MODULE,.read = snd_ctl_read,.open = snd_ctl_open,.release = snd_ctl_release,.llseek = no_llseek,.poll = snd_ctl_poll,.unlocked_ioctl = snd_ctl_ioctl,.compat_ioctl = snd_ctl_ioctl_compat,.fasync = snd_ctl_fasync,};/** registration of the control device*/static int snd_ctl_dev_register(struct snd_device *device){struct snd_card *card = device->device_data;return snd_register_device(SNDRV_DEVICE_TYPE_CONTROL, card, -1,&snd_ctl_f_ops, card, &card->ctl_dev);} /*** snd_register_device - Register the ALSA device file for the card* @type: the device type, SNDRV_DEVICE_TYPE_XXX * @card: the card instance* @dev: the device index* @f_ops: the file operations* @private_data: user pointer for f_ops->open()* @device: the device to register** Registers an ALSA device file for the given card.* The operators have to be set in reg parameter.** Return: Zero if successful, or a negative error code on failure.*/int snd_register_device(int type, struct snd_card *card, int dev,const struct file_operations *f_ops,void *private_data, struct device *device){int minor;int err = 0;struct snd_minor *preg; if (snd_BUG_ON(!device))return -EINVAL; preg = kmalloc(sizeof *preg, GFP_KERNEL);if (preg == NULL)return -ENOMEM;preg->type = type;preg->card = card ? card->number : -1;preg->device = dev;preg->f_ops = f_ops;preg->private_data = private_data;preg->card_ptr = card;mutex_lock(&sound_mutex);minor = snd_find_free_minor(type, card, dev);if (minor < 0) {err = minor;goto error;}preg->dev = device;device->devt = MKDEV(major, minor);err = device_add(device);if (err < 0)goto error;snd_minors[minor] = preg;error:mutex_unlock(&sound_mutex);if (err < 0)kfree(preg);return err;}從snd_ctl_dev_register函數中可以看到:
- 分配并初始化一個snd_minor實例,
- 分配設備節點
- 保存snd_minor實例到snd_minors數組中
上面過程是聲卡注冊時才被回調的。
8.5聲卡的注冊
當聲卡下的所有邏輯設備都已經準備就緒后,就可以調用snd_card_register()注冊聲卡了:
- 創建聲卡的sysfs設備;
- 調用snd_device_register_all()注冊所有掛在該聲卡下的邏輯設備;
- 建立proc信息文件和sysfs屬性文件。
至此完成了聲卡及聲卡下的所有邏輯設備的注冊,用戶態可以通過系統調用來訪問這些設備。
8.6PCM設備的創建
?
snd_pcm_set_ops:設置PCM設備的操作接口,設置完成后,在PCM設備層即可訪問操作底層音頻物理設備。
snd_pcm_new:
- 創建一個PCM設備實例snd_pcm;
- 創建playback stream和capture stream,旗下的substream也同時建立;
- 調用snd_device_new()把pcm設備掛到聲卡的devices鏈表上。
在看看pcm設備的系統調用:
const struct file_operations snd_pcm_f_ops[2] = {3681 {3682 .owner = THIS_MODULE,3683 .write = snd_pcm_write,3684 .write_iter = snd_pcm_writev,3685 .open = snd_pcm_playback_open,3686 .release = snd_pcm_release,3687 .llseek = no_llseek,3688 .poll = snd_pcm_playback_poll,3689 .unlocked_ioctl = snd_pcm_playback_ioctl,3690 .compat_ioctl = snd_pcm_ioctl_compat,3691 .mmap = snd_pcm_mmap,3692 .fasync = snd_pcm_fasync,3693 .get_unmapped_area = snd_pcm_get_unmapped_area,3694 },3695 {3696 .owner = THIS_MODULE,3697 .read = snd_pcm_read,3698 .read_iter = snd_pcm_readv,3699 .open = snd_pcm_capture_open,3700 .release = snd_pcm_release,3701 .llseek = no_llseek,3702 .poll = snd_pcm_capture_poll,3703 .unlocked_ioctl = snd_pcm_capture_ioctl,3704 .compat_ioctl = snd_pcm_ioctl_compat,3705 .mmap = snd_pcm_mmap,3706 .fasync = snd_pcm_fasync,3707 .get_unmapped_area = snd_pcm_get_unmapped_area,3708 }3709 };snd_pcm_f_ops作為snd_register_device_for_dev()的參數傳入,并被記錄在snd_minors[minor]中的字段f_ops中。snd_pcm_f_ops[0]是回放使用的系統調用接口,snd_pcm_f_ops[1]是錄制使用的系統調用接口。
9.Frame && Period
音頻數據中的幾個重要概念:
- Sample:樣本長度,音頻數據最基本的單位,常見的有8bit和16bit;
- channel:聲道數,分為單聲道mono和立體聲stereo;
- Frame:幀,構成一個完整的聲音單元,所謂的聲音單元是?一個采樣樣本, Frame = Sample * channel;
- Rate:又稱sample rate,采樣率,即毎秒的采樣次數,針對幀而言;
- Period Size:周期,每次硬件中斷處理音頻數據的幀數,對于音頻設備的數據讀寫,以此為單位;
- Buffer Size:數據緩沖區大小,這里這指runtime的buffer size,而不是結構圖snd_pcm_hardware中定義的buffer_bytes_max;一般來說buffer_size = period_size * period_count,period_count相當于處理完一個buffer數據所需的硬件中斷次數。
下面是一章直觀的表示buffer/period/frame/sample之間的關系:
這個buffer中有4個period,每當DMA搬運完一個period的數據就會出生一次中斷,因此搬運這個buffer中的數據將產生4次中斷。
ALSA為什么這樣做?因為數據緩沖區可能很大,一次傳輸可能會導致不可接收的延遲;為了解決這個問題,alsa把緩沖區拆分成多個周期,以周期為單元傳輸數據。
9.1frames&periods
alsa官網對periods的解釋:https://www.alsa-project.org/main/index.php/FramesPeriods
- Frame:幀,構成一個完整的聲音單元,它的大小等于sample_bits * channels;
- Period:周期大小,即每次dma運輸處理音頻數據的幀數,如果周期大小設定的較大,則單次處理的數據較多,這意味著單位時間內硬件中斷的次數較少,CPU也就有更多時間處理其他任務,功耗也更低,但這樣帶來一個顯著的弊端-數據處理的時延會增大。
- period bytes,對于dma處理來說,它直接關心的是數據大小,而非period_size(一個周期的幀數),有個轉換關系:period_bytes =period_size * sample_bits * channels / 8
由于i2s總線采樣率是穩定的,我們可以計算i2s傳輸一個周期的數據所需的時間:transfer_time = 1 * period_size /sample_rate,in second.
例如period_size = 1024, sample_rate = 48khz,那么一個周期數據的傳輸時間是:1*1024/48000 = 21.3(ms)
?
?
總結
以上是生活随笔為你收集整理的Linux ALSA音频系统:soundcard的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《实用VC编程之玩转控件》第3课:But
- 下一篇: vmware安装linux后没有声音,安