Kazam+PulseAudio組合幾乎成了所有Linux發(fā)布版的屏幕錄制+聲音采集的首選工具犀斋,但到了Ubuntu 20.04之后。 Kazam潛在一個(gè)Bug览效,那就是在“首選項(xiàng)”下朽肥,我看不到“揚(yáng)聲器”和“麥克風(fēng)”。那么導(dǎo)致你在錄制過(guò)程中無(wú)法同步采集聲音持钉。
如果你對(duì)Kazam的源代碼了解的話衡招,它底層還是基于Python寫的,其音頻模塊位于如下路徑的
/usr/lib/python3/dist-packages/kazam/pulseaudio/pulseaudio.py
造成該bug的原因是原pulseaudio.py模塊中的相關(guān)代碼仍然調(diào)用time.clock()函數(shù)每强。 但從Python3.3起該函數(shù)已棄用始腾,并且Ubuntu20.04操作系統(tǒng)已經(jīng)默認(rèn)預(yù)裝python3.7+,那么修復(fù)該Bug非常簡(jiǎn)單空执,只要模塊中的所有time.clock()調(diào)用都必須替換為time.perf_counter()
下面是修改好的pulseaudio.py模塊的完整代碼浪箭,只要覆蓋原有的模塊即可辨绊。
# -*- coding: utf-8 -*-
#
# pulseaudio.py
#
# Copyright 2012 David Klasinc <bigwhale@lubica.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
import time
import logging
logger = logging.getLogger("PulseAudio")
from kazam.pulseaudio.error_handling import *
from kazam.backend.prefs import *
try:
from kazam.pulseaudio.ctypes_pulseaudio import *
except:
raise PAError(PA_LOAD_ERROR, "Unable to load pulseaudio wrapper lib. Is PulseAudio installed?")
class pulseaudio_q:
def __init__(self):
"""pulseaudio_q constructor.
Initializes and sets all the necessary startup variables.
Args:
None
Returns:
None
Raises:
None
"""
self.pa_state = -1
self.sources = []
self._sources = []
self._return_result = []
self.pa_status = PA_STOPPED
#
# Making sure that we don't lose references to callback functions
#
self._pa_state_cb = pa_context_notify_cb_t(self.pa_state_cb)
self._pa_sourcelist_cb = pa_source_info_cb_t(self.pa_sourcelist_cb)
self._pa_sourceinfo_cb = pa_source_info_cb_t(self.pa_sourceinfo_cb)
self._pa_context_success_cb = pa_context_success_cb_t(self.pa_context_success_cb)
def pa_context_success_cb(self, context, c_int, user_data):
self._pa_ctx_success = c_int
return
def pa_state_cb(self, context, userdata):
"""Reads PulseAudio context state.
Sets self.pa_state depending on the pa_context_state and
raises an error if unable to get the state from PulseAudio.
Args:
context: PulseAudio context.
userdata: n/a.
Returns:
Zero on success or raises an exception.
Raises:
PAError, PA_GET_STATE_ERROR if pa_context_get_state() failed.
"""
try:
state = pa_context_get_state(context)
if state in [PA_CONTEXT_UNCONNECTED, PA_CONTEXT_CONNECTING, PA_CONTEXT_AUTHORIZING,
PA_CONTEXT_SETTING_NAME]:
self.pa_state = PA_STATE_WORKING
elif state == PA_CONTEXT_FAILED:
self.pa_state = PA_STATE_FAILED
elif state == PA_CONTEXT_READY:
self.pa_state = PA_STATE_READY
logger.debug("State connected.")
except:
raise PAError(PA_GET_STATE_ERROR, "Unable to read context state.")
return 0
def pa_sourcelist_cb(self, context, source_info, eol, userdata):
"""Source list callback function
Called by mainloop thread each time list of audio sources is requested.
All the parameters to this functions are passed to it automatically by
the caller.
Args:
context: PulseAudio context.
source_info: data returned from mainloop.
eol: End Of List marker if set to non-zero there is no more date
to read and we should bail out.
userdata: n/a.
Returns:
self.source_list: Contains list of all Pulse Audio sources.
self.pa_status: PA_WORKING or PA_FINISHED
Raises:
None
"""
if eol == 0:
logger.debug("pa_sourcelist_cb()")
logger.debug(" IDX: {0}".format(source_info.contents.index))
logger.debug(" Name: {0}".format(source_info.contents.name))
logger.debug(" Desc: {0}".format(source_info.contents.description))
self.pa_status = PA_WORKING
self._sources.append([source_info.contents.index,
source_info.contents.name.decode('utf-8'),
" ".join(source_info.contents.description.decode('utf-8').split())])
else:
logger.debug("pa_sourcelist_cb() -- finished")
self.pa_status = PA_FINISHED
return 0
def pa_sourceinfo_cb(self, context, source_info, eol, userdata):
"""Source list callback function
Called by mainloop thread each time info for a single audio source is requestd.
All the parameters to this functions are passed to it automatically by
the caller. This is here for convenience.
Args:
context: PulseAudio context.
index: Source index
source_info: data returned from mainloop.
eol: End Of List marker if set to non-zero there is no more date
to read and we should bail out.
userdata: n/a.
Returns:
self.source_list: Contains list of all Pulse Audio sources.
self.pa_status: PA_WORKING or PA_FINISHED
Raises:
None
"""
if eol == 0:
logger.debug("pa_sourceinfo_cb()")
logger.debug(" IDX: {0}".format(source_info.contents.index))
logger.debug(" Name: {0}".format(source_info.contents.name))
logger.debug(" Desc: {0}".format(source_info.contents.description))
self.pa_status = PA_WORKING
cvolume = pa_cvolume()
v = pa_volume_t * 32
cvolume.channels = source_info.contents.volume.channels
cvolume.values = v()
for i in range(0, source_info.contents.volume.channels):
cvolume.values[i] = source_info.contents.volume.values[i]
self._return_result = [source_info.contents.index,
source_info.contents.name.decode('utf-8'),
cvolume,
" ".join(source_info.contents.description.decode('utf-8').split())]
else:
try:
logger.debug("pa_sourceinfo_cb() -- Hit EOL")
logger.debug(" EOL IDX: {0}".format(source_info.contents.index))
logger.debug(" EOL Name: {0}".format(source_info.contents.name))
logger.debug(" EOL Desc: {0}".format(source_info.contents.description))
except:
logger.debug("pa_sourceinfo_cb() -- EOL no data!")
self.pa_status = PA_FINISHED
logger.debug("pa_sourceinfo_cb() -- finished")
return 0
def start(self):
"""Starts PulseAudio threaded mainloop.
Creates mainloop, mainloop API and context objects and connects
to the PulseAudio server.
Args:
None
Returns:
None
Raises:
PAError, PA_STARTUP_ERROR - if unable to create PA objects.
PAError, PA_UNABLE_TO_CONNECT - if connection to PA fails.
PAError, PA_UNABLE_TO_CONNECT2 - if call to connect() fails.
PAError, PA_MAINLOOP_START_ERROR - if not able to start mainloop.
"""
try:
logger.debug("Starting mainloop.")
self.pa_ml = pa_threaded_mainloop_new()
logger.debug("Getting API.")
self.pa_mlapi = pa_threaded_mainloop_get_api(self.pa_ml)
logger.debug("Setting context.")
self.pa_ctx = pa_context_new(self.pa_mlapi, None)
logger.debug("Set state callback.")
pa_context_set_state_callback(self.pa_ctx, self._pa_state_cb, None)
except:
raise PAError(PA_STARTUP_ERROR, "Unable to access PulseAudio API.")
try:
logger.debug("Connecting to server.")
if pa_context_connect(self.pa_ctx, None, 0, None):
raise PAError(PA_UNABLE_TO_CONNECT, "Unable to connect to PulseAudio server.")
except:
raise PAError(PA_UNABLE_TO_CONNECT2, "Unable to initiate connection to PulseAudio server.")
try:
logger.debug("Start mainloop.")
pa_threaded_mainloop_start(self.pa_ml)
time.sleep(0.1) # Mainloop needs some time to start ...
pa_context_get_state(self.pa_ctx)
except:
raise PAError(PA_MAINLOOP_START_ERROR, "Unable to start mainloop.")
def end(self):
"""Disconnects from PulseAudio server.
Disconnects from PulseAudio server, it should be called after all the
operations are finished.
Args:
None
Returns:
None
Raises:
PAError, PA_MAINLOOP_END_ERROR - if not able to disconnect.
"""
try:
logger.debug("Disconnecting from server.")
pa_context_disconnect(self.pa_ctx)
self.pa_ml = None
self.pa_mlapi = None
self.pa_ctx = None
except:
raise PAError(PA_MAINLOOP_END_ERROR, "Unable to end mainloop.")
def get_audio_sources(self):
try:
logger.debug("get_audio_sources() called.")
pa_context_get_source_info_list(self.pa_ctx, self._pa_sourcelist_cb, None)
t = time.perf_counter()
while time.perf_counter() - t < 5:
if self.pa_status == PA_FINISHED:
self.sources = self._sources
self._sources = []
return self.sources
raise PAError(PA_GET_SOURCES_TIMEOUT, "Unable to get sources, operation timed out.")
except:
logger.debug("Unable to get audio sources.")
raise PAError(PA_GET_SOURCES_ERROR, "Unable to get sources.")
def get_source_info_by_index(self, index):
try:
logger.debug("get_source_info_by_index() called. IDX: {0}".format(index))
pa_context_get_source_info_by_index(self.pa_ctx, index, self._pa_sourceinfo_cb, None)
t = time.perf_counter()
while time.perf_counter() - t < 5:
if self.pa_status == PA_FINISHED:
time.sleep(0.1)
ret = self._return_result
self._return_result = []
return ret
raise PAError(PA_GET_SOURCE_TIMEOUT, "Unable to get source, operation timed out.")
except:
raise PAError(PA_GET_SOURCE_ERROR, "Unable to get source.")
def set_source_volume_by_index(self, index, cvolume):
try:
pa_context_set_source_volume_by_index(self.pa_ctx, index, cvolume,
self._pa_context_success_cb, None)
t = time.perf_counter()
while time.perf_counter() - t < 5:
if self.pa_status == PA_FINISHED:
return 1
raise PAError(PA_GET_SOURCES_TIMEOUT, "Unable to get sources, operation timed out.")
except:
raise PAError(PA_GET_SOURCES_ERROR, "Unable to get sources.")
def set_source_mute_by_index(self, index, mute):
try:
pa_context_set_source_mute_by_index(self.pa_ctx, index, mute,
self._pa_context_success_cb, None)
t = time.perf_counter()
while time.perf_counter() - t < 5:
if self.pa_status == PA_FINISHED:
return 1
raise PAError(PA_GET_SOURCES_TIMEOUT, "Unable to get sources, operation timed out.")
except:
raise PAError(PA_GET_SOURCES_ERROR, "Unable to get sources.")
def cvolume_to_linear(self, cvolume):
avg = 0
for chn in range(cvolume.channels):
avg = avg + cvolume.values[chn]
avg = avg / cvolume.channels
volume = pa_sw_volume_to_linear(uint32_t(int(avg)))
return volume
def cvolume_to_dB(self, cvolume):
avg = 0
for chn in range(cvolume.channels):
avg = avg + cvolume.values[chn]
avg = avg / cvolume.channels
volume = pa_sw_volume_to_dB(uint32_t(int(avg)))
return volume
def linear_to_cvolume(self, index, volume):
info = self.get_source_info_by_index(index)
cvolume = pa_cvolume()
v = pa_volume_t * 32
cvolume.channels = info[2].channels
cvolume.values = v()
for i in range(0, info[2].channels):
cvolume.values[i] = pa_sw_volume_from_linear(volume)
return cvolume
def dB_to_cvolume(self, channels, volume):
cvolume = pa_cvolume()
v = pa_volume_t * 32
cvolume.channels = channels
cvolume.values = v()
value = pa_sw_volume_from_dB(volume)
for i in range(0, channels):
cvolume.values[i] = value
return cvolume
將Kazam重啟一次奶栖,我們看看Kazam的首選項(xiàng)
我們打開kazam,勾選[來(lái)自揚(yáng)聲器的聲音],這里暫時(shí)不要點(diǎn)[捕獲]按鈕
接下來(lái)门坷,我們先確定要錄制的屏幕區(qū)域宣鄙,例如,我經(jīng)常選擇區(qū)域錄制默蚌,我們選擇[區(qū)域]選項(xiàng)后冻晤,kazam會(huì)提醒我們需要拖動(dòng)錄制的區(qū)域大小,如下圖绸吸。
聲音的設(shè)定部分鼻弧,PulseAudio安裝之后设江,我們?cè)谌蝿?wù)欄的音量控制圖標(biāo)那里的音頻列表
- Speakers(Built-in Audio Analog Stereo),這是硬件層的物理?yè)P(yáng)聲器,也就是你能聽到的
- Simultaneous output to Built-in Audio Analog Stereo,這是一個(gè)邏輯上的音頻輸出設(shè)備攘轩,這是PulseAudio驅(qū)動(dòng)的虛擬揚(yáng)聲器
這樣做的目地是讓用戶有多一個(gè)選擇叉存,我們?cè)趫?zhí)行聲音采集過(guò)程中,希望做到采集過(guò)程中做到安靜采集度帮,不想打擾身邊其他人鹉胖,此時(shí)我們只需要將虛擬揚(yáng)聲器設(shè)定為默認(rèn)設(shè)備,并選擇[通過(guò)次設(shè)備播放所有音頻] 將操作系統(tǒng)的所有音頻播放流重定向到該虛擬揚(yáng)聲器够傍,此時(shí)我們將物理?yè)P(yáng)聲器的音量調(diào)成靜音即可甫菠。
不過(guò)過(guò)做上面的設(shè)定還是不夠的的,我們打開音頻控制的系統(tǒng)設(shè)置冕屯,將音頻輸出配置的如下選項(xiàng)都勾選上
- 一個(gè)可以同步輸出到所有本地聲卡的虛擬輸出設(shè)備
- 當(dāng)輸出可用時(shí)寂诱,自動(dòng)切換所有流
一切設(shè)定好后,點(diǎn)擊[捕獲]按鈕安聘,任務(wù)欄的錄像按鈕表示Kazam已經(jīng)在后臺(tái)執(zhí)行屏幕已經(jīng)在后臺(tái)執(zhí)行屏幕錄像和聲音采集了痰洒。
如果你發(fā)現(xiàn)你采取的視頻沒(méi)有聲音或第一次使用kazam的話,請(qǐng)打開PulseAudio 音量控制面板浴韭,這個(gè)工具是安裝PulseAudio時(shí)已經(jīng)附帶的丘喻,此時(shí)檢測(cè)一下輸入設(shè)備一欄,如果發(fā)現(xiàn)當(dāng)前虛擬聲卡輸出的音量是跳動(dòng)念颈,并且錄音一欄也有一個(gè)Python圖標(biāo)kazam的音量標(biāo)也是跳動(dòng)的泉粉。代碼音聲采集一切都是正常工作的。
我們看看取樣的結(jié)果
- 視頻壓制格式h.264榴芳,
- 幀刷新率:50嗡靡,對(duì)于視頻錄制格式建議不要低于50,因?yàn)闀?huì)造成圖像刷新時(shí)出現(xiàn)水平抖動(dòng)的狀況
- 分辨率:跟你視頻采集方式有關(guān)
- 音頻采樣率:44100Hz,這個(gè)跟Kazam毫無(wú)關(guān)系,和操作系統(tǒng)的音頻輸出設(shè)置和PulseAudio的虛擬聲卡設(shè)置有關(guān)窟感。