1. 程式人生 > >從零開始學習PYTHON3講義(十四)寫一個mp3播放器

從零開始學習PYTHON3講義(十四)寫一個mp3播放器


《從零開始PYTHON3》第十四講

通常來說,Python解釋執行,執行速度慢,並不適合完整的開發遊戲。隨著電腦速度的快速提高,這種情況有所好轉,但開發遊戲仍然不是Python的重點工作。
大多應用是利用Python開發效率高的特點,進行遊戲原型驗證,或者在大的遊戲系統中,使用Python進行地圖、場景等定製。還有就是使用遊戲開發的技術和理念,將Python用於商業視覺展示、工程效果展示。

原型驗證:指的是有了一個好的遊戲想法,完整的開發出來肯定需要大量的人員、費用、時間,利用Python程式設計簡單高效的特點,先模擬完成一部分遊戲的功能,從而能夠展示給投資人、客戶,獲取大家的認可,進而得到經費投入。
地圖、場景定製:遊戲的開發肯定需要很多專業技術方面的高精尖人才,但遊戲的運營、地圖的設計、故事情節等。這都是商業或者藝術方面的專業強項,而這些人員不大可能使用c/c++等常用的遊戲開發工具來做這些工作。因此,遊戲開發過程中,通常完成Python語言的介面,讓這些商業、藝術工作人員也能使用比較方便的手段進行遊戲功能的調整。

此外,現代的遊戲開發已經是一個大團隊合作的產物,已經非常難以單打獨鬥完成一款遊戲。所以學習遊戲程式設計的目標並不是希望自己獨立完成一個遊戲,而是用這種思路來解決具體問題。
通常遊戲開發的工作分工是這樣的:
gameRD
其中音效、畫面都會由更專業的團隊完成。最後由程式人員整合在遊戲中。在遊戲中,音樂音效、操作控制、遊戲邏輯、畫面幾個部分,都是並行在同時進行的。它們必須共同生效,遊戲才會好玩。


Pygame程式設計和音樂播放

Pygame是一個強大的遊戲擴充套件包,首先也是安裝:

#使用管理員模式啟動cmd命令列,然後執行:
pip install pygame  #某些系統是pip3 install pygame

這個安裝擴充套件包的過程,我們重複了很多遍,這個算是最後一遍了。因為Pygame是我們課程講解的最後一個擴充套件包。比起來其它的軟體,Python的擴充套件包,只要你知道了名字,安裝幾乎都是相同的。即便不同的作業系統,差別也不大。

在這一講,我們會採用跟以前不同的方法來講述Pygame擴充套件包的使用。原因是Python有非常多的擴充套件包。即便官方內建的擴充套件包,也量非常大。如果完全等待別人教你使用這種方式是不可能的,此外即便是別人教過了,Python和擴充套件包的升級也非常的快。原有的使用方法,很可能現在已經不適用了。這些都要求你有自己探索的能力,在Python基本技能的學習掌握之後,根據自己的程式設計需求,選擇相應的擴充套件包,查詢資料、文件。在網上資料的幫助下,掌握擴充套件包的使用方法。

從目前行業內的使用情況看,最大的障礙在於目前主要的文件來源都是英文的,這要求我們具備一定的英文閱讀能力。此外,雖然版本的更新對擴充套件包的使用有一些差別,但這種差別畢竟不算大。所以在國內一些相對較早的文件幫助下,再對應國外新版本的文件,也能降低你的學習門檻。


只是播放mp3,Python有很多擴充套件包可以選,很多操作起來也更簡便。不過pygame是為了遊戲設計,除了背景音樂,音效、與畫面的協作也考慮的更多。所以雖然用起來複雜一些,我們依然還是選擇學習用Pygame播放mp3音樂。目的,更多是期望學習者除了學習python相關的知識,也更多理解現代計算機併發多工和多種約束條件下的程式設計思維。

拿到一個新的擴充套件包,通常你有這樣幾種途徑瞭解它的使用:

  • 到官網檢視官方文件(通常是英文)

  • 在搜尋引擎網站比如百度搜索中文的資料,這種情況比較多見,因為大多情況下,你之所以知道這個擴充套件包,也是在網上搜索相關資料的時候,別人介紹的。而通常這種情況下,都已經有包簡單實用的介紹。

  • 使用Python內建的dir()/help()函式,當前還是英文資料,適合已經瞭解擴充套件包的基本架構,只是在函式選擇、呼叫的時候查詢資料

    所以,實際上,通過搜尋引擎查詢相關資料,應當是你上手的最優選擇。以pygame為例,通過查詢中文的資料,總結之後,應當能寫出這樣的程式:

#MP3播放器

#引入擴充套件庫
import pygame
    
#歌曲檔案
file='rongHua.mp3'

#初始化聲音庫
pygame.mixer.init(frequency=44100)
print("播放音樂-絨花")

#載入音樂檔案
pygame.mixer.music.load(file)
#播放聲音
pygame.mixer.music.play()

程式每一條語句都有註釋,大概的框架上看,應當也是順序執行的。有一些引數可能你還不能明白,比如frequency=44100,不過應當不影響你抄過來用。這個是指定音訊庫使用的取樣頻率,44100一般已經是高保真音樂的取樣頻率了。通常mp3檔案都是這種格式。另外忘了交代,rongHua.mp3是我們要播放的聲音檔名稱,記得要提前準備好,放到程式同一個目錄。

執行程式之後發現,詭異的事情發生了,程式只顯示了一行文字:“播放音樂-絨花”,然後就退出了,並沒有事情發生,也沒有音樂播放出來。

一開始就說過了,本講重點不完全是播放一首音樂,而是希望能引導大家使用探索的方式,來了解一個新的擴充套件包如何學習和使用。所以不要等待著我說出答案,而是積極的思考,判斷出現了什麼問題,並且嘗試去解決。

首先要說明的是,程式本身引入pygame庫、庫的初始化還有播放語句語句本身都並沒有什麼錯誤。通常在網上查詢資料的時候,只要認真閱讀,比較容易保證這一點。難以馬上學會並應用到程式設計中的,是關於某個庫“架構”方面的內容,也就是影響程式結構方面的內容。如果覺得這句話比較抽象的話,你可以回憶一下上一講我們嘗試過的flask網路程式設計框架。框架、架構,這兩個詞在這裡基本可以劃等號了。

我們的程式沒有能播放出來音樂,也是這方面的原因。
通常遊戲程式要包含至少4部分的內容,我們用本講開始的那張圖來說明,音樂、畫面、操控、邏輯這四部分內容是並行執行,相互配合,才能展現給使用者一個圖文並茂、流暢、吸引人的遊戲。
因此作為遊戲的一部分,音樂的播放也不可能像我們前面學過的繪圖、計算等操作一樣,在音樂沒有播放完成前,程式停止在那裡一直等待。事實上通常遊戲的做法都是,發出播放音樂的命令之後,命令本身馬上返回,讓程式有能力並行去處理按鍵輸入、繪圖等動作。
而在我們上面的程式中,播放這個命令肯定是發出去了,但沒有等音樂聲響起,程式就已經結束退出了。程式的結束退出將自動的釋放程式開啟的各項資源,清理執行的痕跡,從而音樂也就不可能再放出來了。
這僅僅是我們推測分析的結果,我們來證明一下,方法就是在程式最後增加一行語句:

#程式等待5秒鐘
pygame.time.delay(1000*5)

使用這樣語句的目的是,如果我們上面的推測成立,那肯定要對程式做結構上的調整。這個工作量會比較大,所以我們先使用簡單的語句來驗證一下我們的思考。
再次執行程式,你會聽到音樂響了5秒鐘,然後程式退出,音樂也停止了。
這基本可以證明,我們的思考正確。此外似乎還有些別的問題,比如音樂一開始有一個“破音”,這讓人感覺不好。而且程式似乎有的時候能正常播放,有的時候還是不穩定,無法播放成功。
下面要如何改程序序呢?

通常我們會繼續在網上搜索pygame模組使用的案例,閱讀別人的程式,有的時候運氣好,你碰到的程式程式碼,跟你想寫的程式碼是完全相同的功能,這時候你可以拷貝過來直接使用。但大多時候,你只能找到功能相近的程式碼,所以仍然需要你閱讀別人的程式,並從其中學習對你有用的部分。
比如,你可能搜尋到我們第一講演示的遊戲,其中當然也有聲音處理的部分,你會重點閱讀這部分的程式碼,來找出同自己程式的區別,以求解決問題。

在這個過程中,我們又做出了一些判斷,當然這些判斷依然需要大量程式的經驗,所以並不能要求初學者也能輕易做到。但複雜的做不到,你可以從簡單的入手,逐漸積累。這裡只是想告訴你正確的學習思路:

  • Pygame作為一個遊戲開發庫,聲音的播放需要依賴一個視窗,也就是遊戲的畫面。沒有視窗的情況下,播放程序無法穩定的工作。這一項原因推測來自於,很多網上找到的程式碼,在聲音處理上並沒有太多不同,但能正常工作,所以會有這樣的猜測。
  • Python的各個功能,初始化一般意味著建立各項必須的資源,完成工作後,退出之前,應當釋放掉這些資源,特別是系統公用的聲音、顯示等,如果程式只是退出,沒有釋放,就可能導致再次執行的時候,聲音無法正確完成初始化,畢竟一個系統的裝置,是被所有程式所公用的。
  • 系統本身原因,不能快速的連續的初始化及釋放,兩次執行之間應當等待片刻。這個判斷,在多次執行程式,查詢規律的過程中,能很快的發現,當然需要你足夠的細心觀察。
  • “破音”是因為在聲音裝置初始化後,尚未穩定之前就開始傳送音訊資料,此時的資料無法被正常解析,造成破音。這僅為猜測,需要實驗的證實。

驗證思考最好的辦法就是修改程式,然後再次執行實驗,因此我們再完成一版程式:

#引入擴充套件庫
import pygame
    
#歌曲檔案
file='rongHua.mp3'

#初始化pygame顯示庫
pygame.display.init()
#開啟一個視窗
screen = pygame.display.set_mode([200,100])
#初始化pygame聲音庫
pygame.mixer.init(frequency=44100)
print("播放音樂-絨花")
#載入音樂檔案
pygame.mixer.music.load(file)
#儲存當前音量
v = pygame.mixer.music.get_volume()
#設定為靜音,防止開始的爆破音
pygame.mixer.music.set_volume(0)
#播放聲音
pygame.mixer.music.play()
#延時0.2秒開啟聲音,避過爆破音
pygame.time.delay(200)
pygame.mixer.music.set_volume(v)
#播放5秒鐘
pygame.time.delay(1000*5)
#停止播放
pygame.mixer.music.stop()
#退出聲音庫和顯示庫
pygame.mixer.quit()
pygame.display.quit()

每一行程式碼都有註釋,我只講解跟上一版不同的程式碼:

  • 初始化的時候開啟一個視窗,雖然什麼也沒有顯示,但讓播放器有了載體。
  • 一開始關閉聲音,延時再開啟音量,避開一開始的爆破音。
  • 程式退出前關閉播放,釋放各項資源。

此外這些工作中,用到了很多新的函式,這些函式一開始你並不可能知道。這些函式的學習一般是兩個方向,一是概要的瀏覽pygame的手冊或者幫助,在心中有一個粗的概念,這樣用到什麼功能的時候,你會想起來可能有某個函式能完成這個功能,然後再精細檢視。第二是希望用到某個功能,在網上查詢使用Python或者pygame如何做到這個功能。當然還有另外一種渠道,有可能你直接搜尋到了功能相近的程式碼,從中間直接抄過來使用。

試執行之後我們開心的發現,穩定性問題和爆破音都解決了,剩下最關鍵的,如何完整的播放音樂檔案?
這涉及到了我們前面講過的程式結構問題,也是一個框架型的程式庫對程式結構的要求。這一部分一般沒有好辦法,只能通過閱讀官方的文件或者閱讀其它程式的成熟程式碼來獲取,這個過程一般會較長。好在我們大多情況下不會上來就碰到這麼複雜的問題,都是循序漸進。並且大多的擴充套件包只是增加功能性的函式,並不要求程式的結構有多少改變。

我們通過一張對比圖來說明pygame對程式結構的要求:
python3-14.001
傳統程式雖然我們不怎麼熟悉聲音處理,但結構我們都比較熟悉。程式中可能有迴圈,但總體是序列執行的,完成一件事情,才去做另外一件。
從外觀上看,右側的遊戲程式結構,跟左側不過多了一個迴圈。但你要記得,這裡面每一項都是並行執行的,每一個步驟並不會等待這一項工作做完,就會返回接受新的命令,所以程式的聲音、影象、程式邏輯、鍵盤控制,才可能一起發生作用。
這種並行處理的程式,同傳統的程式比,有很多不可協調的理念區別,pygame為了做到並行,採用了“事件驅動”的理念來完成這種控制。
事件驅動實際是存在很久的程式設計方式了,一般傳統的Windows程式,都使用微軟公司提供的訊息迴圈,來處理所有的視窗事件。Python pygame的事件處理,也是採用類似的機制。
總結一下使用事件驅動的方式來編寫pygame程式的要點:

  • 聲音、影象、鍵盤滑鼠輸入、遊戲邏輯必須並行進行,任何一個區域性不能長時間無限制的執行(網路程式設計實際也是並行的,但在小型網站專案中,沒有體現那麼清晰和嚴格)
  • 各個環節之間的同步、配合,都是通過互相傳送訊息的方式來完成的。從獨立一個功能(模組)角度來看,往往是得到某個訊息之後,開始進行某項任務,這種方式叫做事件驅動
  • 各種訊息都是通過核心的訊息傳遞模組完成的,程式的主迴圈一般就是不停的讀取訊息,根據訊息的定義分發給不同模組,並執行不同功能,也稱為訊息迴圈

我們根據剛才這些理念,重新改寫程式,這個程式最終形成code4.py,這裡只介紹重點的訊息迴圈部分:

#... 初始化及基本播放程式碼忽略...
#自定義一條訊息(一個事件)用於表示播放結束
#pygame.USEREVENT是pygame中預定義的使用者訊息起始值
MUSIC_END = pygame.USEREVENT + 1
#設定當前音樂播放完成後,傳送自定義的訊息
pygame.mixer.music.set_endevent(MUSIC_END)

#延時0.2秒開啟聲音,避過爆破音
pygame.time.delay(200)
pygame.mixer.music.set_volume(v)

#定義一個退出程式標誌
requireQuit = False
#程式主迴圈
while not requireQuit:
    #迴圈接受各種事件
    for event in pygame.event.get():
        #如果是自定義的播放完成訊息
        if event.type == MUSIC_END:
            requireQuit=True  #退出
            break
        #介面視窗選單關閉申請
        elif event.type == pygame.QUIT:
            requireQuit=True
            break
        #有鍵盤擡起
        elif event.type == pygame.KEYUP:
            #q鍵
            if event.key == pygame.K_q:
                requireQuit=True
                break
#... 退出操作 ...

程式中,我們自己定義了一條訊息。所謂訊息,並不是平常人類喜聞樂見的一條簡訊或者語音,其實就是一個整數數字。為了容易記憶,我們當然自己定義了一個變數名來代表它,但實際它就是一個數字。
原因是對計算機來講,其實一切都是數字,我們用一個字串反而讓計算機執行的更慢。
隨後,因為我們的訊息迴圈中肯定還可能巢狀迴圈,一個break語句只能打破內部的迴圈,並不能讓外部迴圈也退出,所以我們定義了一個bool的變數,來表示程式是否需要退出迴圈。
這裡的訊息迴圈從技術上並沒有啥難度,主要是你需要適應這麼多新的函式和預定義的變數(這裡當然當做常量來用,比如表示pygame需要退出)。
在內部迴圈中,我們判斷了三種可能需要退出的訊息。一是自己定義的,如果音樂播放結束,應當退出;二是使用者用滑鼠關閉視窗,程式應當退出;三是按q鍵表示使用者希望退出播放。
按下按鍵遊戲採取相應動作是很常見的遊戲處理工作,我們在這裡等待使用者按下按鍵然後再鬆開的這一刻退出,這樣防止使用者按下q鍵一直沒有鬆手所導致的程式退出後,螢幕上還會出現很多q字元的情況。

現在的程式已經能正常的播放音樂了,實際上我們的程式還能進一步優化。比如1.新增播放的時間顯示;2.向前向後跳轉播放。
這兩個功能都可以在訊息迴圈中處理,這樣程式才是並行的。現在你可能感覺到了,實際上訊息迴圈中,才是程式的主要邏輯。的確如此,其實所有的遊戲基本都是在訊息迴圈中做所有的主要工作,當然具體工作細節,都是由已經定義好的函式或叫子程式來具體執行完成的,在主迴圈中,只是對這些函式的組織、管理和呼叫。

顯示播放位置:

#程式主迴圈
while not requireQuit:
    #獲取當前播放位置
    pos=pygame.mixer.music.get_pos()
    #顯示
    print("Playing:", pos,end='\r')

訊息迴圈中,在按鍵部分新增程式碼:

  #如果是向右鍵,則前跳10秒
   elif event.key == pygame.K_LEFT:
      pygame.mixer.music.set_pos(pos/1000-10)
  #如果是向左鍵,則後跳10秒
   elif event.key == pygame.K_RIGHT:
      pygame.mixer.music.set_pos(pos/1000+10)

這樣的功能增加,依賴於你對pygame擴充套件庫越來越熟悉,通過閱讀文件,發現pygame擴充套件庫能提供什麼樣的功能。而這個功能你又需要,就可以加入到程式中。


練習時間

其實本講可以說從開始到現在都是挑戰,因此沒有再設定單獨的挑戰環節。

我們直接進入練習的環節:

  • 以本講前面最終版程式碼code5.py為藍本,修改程式,實現由命令列引數接受mp3檔名,並播放
  • 除了q鍵之外,請設定ESC鍵也作為退出按鍵。提示,ESC鍵的程式碼為:pygame.K_ESCAPE

本講小結

  • python並不是很適合進行遊戲程式設計,但遊戲程式設計的學習能讓你的程式更友好,並具有豐富的表現力
  • 並行、事件驅動的程式設計思想,是現代程式開發的前沿思想,對於提高程式的效率和穩定性有重要的幫助
  • 在一個新模組的學習中,循序漸進,逐步完善程式碼是常用的一種手段。在本講,我們更側重講述,你接觸到一個新的擴充套件包,如何查詢資料、分析問題,最終掌握它的使用

練習答案

請參考mp3Player.py程式。
(所有本系列中出現、使用過的原始碼將會在連載完成後統一整理提供下載。)