Kazam+PulseAudio組合幾乎成了所有Linux發(fā)布版的屏幕錄制+聲音采集的首選工具,但到了Ubuntu 20.04之后。 Kazam潛在一個Bug,那就是在“首選項”下,我看不到“揚聲器”和“麥克風”。那么導致你在錄制過程中無法同步采集聲音。

如果你對Kazam的源代碼了解的話,它底層還是基于Python寫的,其音頻模塊位于如下路徑的
/usr/lib/python3/dist-packages/kazam/pulseaudio/pulseaudio.py
造成該bug的原因是原pulseaudio.py模塊中的相關代碼仍然調用time.clock()函數(shù)。 但從Python3.3起該函數(shù)已棄用,并且Ubuntu20.04操作系統(tǒng)已經默認預裝python3.7+,那么修復該Bug非常簡單,只要模塊中的所有time.clock()調用都必須替換為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的首選項

我們打開kazam,勾選[來自揚聲器的聲音],這里暫時不要點[捕獲]按鈕

接下來,我們先確定要錄制的屏幕區(qū)域,例如,我經常選擇區(qū)域錄制,我們選擇[區(qū)域]選項后,kazam會提醒我們需要拖動錄制的區(qū)域大小,如下圖。

聲音的設定部分,PulseAudio安裝之后,我們在任務欄的音量控制圖標那里的音頻列表
- Speakers(Built-in Audio Analog Stereo),這是硬件層的物理揚聲器,也就是你能聽到的
- Simultaneous output to Built-in Audio Analog Stereo,這是一個邏輯上的音頻輸出設備,這是PulseAudio驅動的虛擬揚聲器
這樣做的目地是讓用戶有多一個選擇,我們在執(zhí)行聲音采集過程中,希望做到采集過程中做到安靜采集,不想打擾身邊其他人,此時我們只需要將虛擬揚聲器設定為默認設備,并選擇[通過次設備播放所有音頻] 將操作系統(tǒng)的所有音頻播放流重定向到該虛擬揚聲器,此時我們將物理揚聲器的音量調成靜音即可。

不過過做上面的設定還是不夠的的,我們打開音頻控制的系統(tǒng)設置,將音頻輸出配置的如下選項都勾選上
- 一個可以同步輸出到所有本地聲卡的虛擬輸出設備
- 當輸出可用時,自動切換所有流

一切設定好后,點擊[捕獲]按鈕,任務欄的錄像按鈕表示Kazam已經在后臺執(zhí)行屏幕已經在后臺執(zhí)行屏幕錄像和聲音采集了。

如果你發(fā)現(xiàn)你采取的視頻沒有聲音或第一次使用kazam的話,請打開PulseAudio 音量控制面板,這個工具是安裝PulseAudio時已經附帶的,此時檢測一下輸入設備一欄,如果發(fā)現(xiàn)當前虛擬聲卡輸出的音量是跳動,并且錄音一欄也有一個Python圖標kazam的音量標也是跳動的。代碼音聲采集一切都是正常工作的。

我們看看取樣的結果
- 視頻壓制格式h.264,
- 幀刷新率:50,對于視頻錄制格式建議不要低于50,因為會造成圖像刷新時出現(xiàn)水平抖動的狀況
- 分辨率:跟你視頻采集方式有關
- 音頻采樣率:44100Hz,這個跟Kazam毫無關系,和操作系統(tǒng)的音頻輸出設置和PulseAudio的虛擬聲卡設置有關。
