第12篇:Linux Kazam屏幕錄制+聲音采集設(shè)置

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)窟感。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末讨彼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子柿祈,更是在濱河造成了極大的恐慌哈误,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件躏嚎,死亡現(xiàn)場(chǎng)離奇詭異蜜自,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)紧索,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門袁辈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)菜谣,“玉大人珠漂,你說(shuō)我怎么就攤上這事晚缩。” “怎么了媳危?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵荞彼,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我待笑,道長(zhǎng)鸣皂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任暮蹂,我火速辦了婚禮寞缝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘仰泻。我一直安慰自己荆陆,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布集侯。 她就那樣靜靜地躺著被啼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棠枉。 梳的紋絲不亂的頭發(fā)上浓体,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音辈讶,去河邊找鬼命浴。 笑死,一個(gè)胖子當(dāng)著我的面吹牛贱除,可吹牛的內(nèi)容都是我干的咳促。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼勘伺,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼跪腹!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起飞醉,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤冲茸,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后缅帘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體轴术,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年钦无,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了逗栽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡失暂,死狀恐怖彼宠,靈堂內(nèi)的尸體忽然破棺而出鳄虱,到底是詐尸還是另有隱情,我是刑警寧澤凭峡,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布拙已,位于F島的核電站,受9級(jí)特大地震影響摧冀,放射性物質(zhì)發(fā)生泄漏倍踪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一索昂、第九天 我趴在偏房一處隱蔽的房頂上張望建车。 院中可真熱鬧,春花似錦椒惨、人聲如沸癞志。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)凄杯。三九已至,卻和暖如春秉宿,著一層夾襖步出監(jiān)牢的瞬間戒突,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工描睦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留膊存,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓忱叭,卻偏偏與公主長(zhǎng)得像隔崎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子韵丑,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345