看起來一切已經就緒,我們選擇了最熟悉的各種組件庫,解決了音樂源的問題,似乎可以開始大刀闊斧的開工了。且慢!現在還不行,還要解決一些問題才能開工。我把這些問題稱爲技術障礙,必須先克服這些技術障礙,才能開始動手編碼。
一、如何播放音頻文件?
首先需要確認的是,音頻播放。這裏我們假設電腦上已經安裝了Ffmpeg、PyAudio這兩個組件庫,那麼問題是如何在Python中調用這些庫播放音樂?
我做了個小實驗,用了下面這段代碼:
def play_from_url(song_url):
"""
從 URL 地址播放一段音樂
:param str song_url: 音樂地址
"""
request = Request(song_url)
pipe = urlopen(url=request, timeout=DEFAULT_TIMEOUT)
with NamedTemporaryFile("w+b", suffix=".wav") as f:
f.seek(0)
while True:
data = pipe.read(1024)
if not len(data):
break
f.write(data)
subprocess.call([
PLAYER, "-nodisp", "-autoexit", "-hide_banner", f.name])
做完這個實驗,我發現一切都工作的很不錯,程序的執行過程是這樣的:首先根據音樂的URL信息從服務器上讀取音頻數據,通過Python的臨時文件保存在電腦上,再通過子進程調用的方式用本地安裝的播放器播放音頻文件。
這段代碼的確能夠工作,但存在一(很)些(多)問題。最顯而易見的是在命令行模式下這段代碼運行的很好,但是在GUI模式下,他會卡住一小會兒,等緩存結束就又正常了,這是爲什麼呢?答案就是線程!仔細看這段代碼,while循環退出的標記是data的長度爲0,那麼在有數據讀取的過程中,循環會一直執行,這在命令模式下顯然沒什麼問題,但是在GUI模式下,這樣的循環會導致GUI界面無法操作,實際上是因爲緩存循環阻塞了Qt主線程的循環過程,那麼畫面肯定就無法操作了!
二、齊頭並進,我們需要多線程
爲了解決上面的問題,我們需要用到線程。我們希望在緩存文件的同時不影響Qt主線程循環,用戶能在主界面上通過鼠標或鍵盤進行操作。
Python爲我們提供了完整的多線程操作機制,但如何在Python中使用多線程與這個故事並沒有太大的關係,如果你現在對Python的多線程機制還不熟悉,又想看完這個故事,我建議可以先看看《Python cookbook 第三版》的第十二章,在那裏可以學到很多併發編程的技巧。
另外我並不打算使用Python原生的多線程方案,而是使用PySide提供的QThread類進行多線程開發,理由是在多線程通信機制方面QThread提供與PySide UI一樣的“信號-槽”機制,後文我會介紹“信號-槽”機制的使用方法。
QThread的使用與Python的Thread使用方式類似,都是通過線程類創建對象,調用start()接口方法啓動線程,線程啓動之後是從run()入口方法開始執行,通過調用wait()接口方法可以讓線程進入等待狀態,直到線程全部執行完畢或遇到quit()或exit()方法調用時線程纔會退出。但請注意並不是調用quit()或者exit()接口方法後線程就會立即退出,這可以通過QThread的isRunning()方法來判斷。
有了這些方法,我們就可以修改上面的代碼,用Player類封裝:
class Player(QThread):
def play_from_url(song_url):
# coder here
def run(self, *args, **kwargs):
"""
開始播放
"""
self.play_from_url()
然後創建Player的實例,再調用實例的start()方法就能啓動播放線程。在這個軟件裏面有很多地方都會用到線程操作,比如下載、播放等待動畫、緩存音樂等等,後面還會講到與線程有關的內容。
三、音樂播放到哪裏了?
通過使用多線程,我們的播放器在GUI界面中也能正常播放音樂,而且所有的界面操作都不會受到影響。但是其他問題依然存在!我們的播放器沒有辦法顯示播放進度!這顯然無法滿足使用需求。
通過分析上面的播放代碼也不難發現,我播放音樂是通過子進程調用本地播放器實現的,這種方式對於播放背景音樂、特效音樂來說完全沒有問題,因爲播放這些音樂不需要用戶干預,用戶只要設置播放或者不播放就行,而對一款音樂播放軟件來說,這是無論如何都不能接受的,這時就需要請出PyAudio。
PyAudio不僅能播放音樂還能錄音。通過閱讀PyAudio官方文檔和官方提供的範例就能大致理解如何通過PyAudio播放音樂,官方提供的範例代碼很短,但確實能播放WAV文件,細心的你很快就能發現,代碼中同樣沒有現成的播放進度數據可以使用。
下面是PyAudio官方提供的範例代碼:
"""PyAudio Example: Play a WAVE file."""
import pyaudio
import wave
import sys
CHUNK = 1024
if len(sys.argv) < 2:
print("Plays a wave file.\n\nUsage: %s filename.wav" %
sys.argv[0])
sys.exit(-1)
wf = wave.open(sys.argv[1], 'rb')
p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
channels=wf.getnchannels(),
rate=wf.getframerate(),
output=True)
data = wf.readframes(CHUNK)
while data != '':
stream.write(data)
data = wf.readframes(CHUNK)
stream.stop_stream()
stream.close()
p.terminate()
從官方代碼中可以發現,創建PyAudio對象後,就能通過open()方法(注意該方法所提供的參數分別是:採樣格式、聲道數、碼率、是否輸出)獲得播放音樂的數據流對象,我們可以稱之爲聲卡數據流。然後不斷地向聲卡數據流寫入數據,我們就能通過電腦音響或耳機聽到音樂了。
這段代碼中的另一個重點是向聲卡流寫入的數據,這些數據並不是直接從文件讀取到的數據,而是音頻文件的音頻數據幀,音頻數據幀是struct類型數據,一般會包含幀頭、CRC校驗(由幀頭第16bit決定是否有CRC校驗)、幀數據、VBR頭這幾個重要信息,因此在向聲卡數據流寫入數據時,一定要確認寫入的是幀數據。
如果我們能夠獲得每個幀數據播放持續的時間,就能通過下面的公式計算獲得當前播放的時間長度:
當前播放的時間長度(秒) = 播放過的幀數 * 每幀持續時間(毫秒) * 1000
一般來說幀的持續時間爲2.5ms~60ms,對於一首音樂我們可以通過下面的計算公式來計算每幀的持續時間:
每幀持續時間(毫秒) = 每幀採樣數 / 採樣頻率 * 1000
有了上面的兩組公式就能順利計算當前播放時間,在播放界面上就能像商業播放軟件那樣動態顯示音樂的播放時間。
def play(self):
"""
從 self.start_position 時間開始播放音樂
"""
if not self.is_valid:
return False
self.is_playing = True
self.emit(SIGNAL('before_play()'))
self.time = self.start_position
audio_stream = self.audio.open(
format=self.audio.get_format_from_width(
self.audio_segment.sample_width),
channels=self.audio_segment.channels,
rate=self.audio_segment.frame_rate,
output=True
)
index = 0
# for chunk in self.chunks:
while True:
if not self.is_playing:
break
while self.is_paused:
sleep(0.5)
continue
if self.time >= self.duration:
self.emit(SIGNAL('play_finished()'))
break
self.time += self.chunk_duration
if index < len(self.chunks):
audio_stream.write(self.chunks[index].raw_data)
index += 1
try:
self.emit(SIGNAL('playing()'))
except Exception as e:
continue
audio_stream.close()
self.is_playing = False
self.emit(SIGNAL('stopped()'))
從上面的代碼中可以看出來,在播放時把每次播放過的chunk的播放時間加在一起,就是當前播放的時間進度,只要在播放界面上獲取這個值,就能用來設置播放進度。
四、爲什麼要等很久才播放音樂?
如果你做了第一小節中的那個實驗,你會發現這段代碼除了對GUI界面有影響,還有一個問題,就是音樂不是立即開始播放的,你要等待一會兒才能聽到音樂,並且根據音樂碼率、時間長度的不同,有可能要等待比較長的時間才能聽到音樂,而我們以前用過的在線音樂播放軟件則很快就能開始播放音樂。
這是由我們的設計結構造成的,我們的方法是把文件全部緩存之後再播放,這就是導致要等待很久的主要原因!如何縮短等待時間呢?
方法就是邊緩存邊播放,在文件第一次緩存成功之後,就立即讀取出來,分析出能夠讀取到的所有的音頻幀數據,將其保存在一個數據列隊中,在一個新的線程中將音頻數據幀循環寫入聲卡數據流開始播放;第二次緩存成功之後也是這樣操作,只是讀取數據時不要從頭開始,而是從上次讀取結束的地方開始;第三次...第四次...;直到所有的幀數據都被讀取到數據列隊中。
每次緩存的數據量可以調整到合適的大小,一般在200KB~400KB之間,對絕大多數網絡環境來說,緩存這麼多數據一般在1-2秒左右即可完成,隨後就能聽到音樂,用戶體驗自然要好很多。
五、緩存真的成功了嗎?
音樂文件肯定需要緩存,但是不能作爲臨時文件緩存,特別是不能設置爲有超時時間的緩存文件,因爲文件會在超時時間之後將被刪除。音頻文件要緩存在指定的緩存目錄中,並且下次播放時還要能找到這個文件。
改變緩存文件的位置並不複雜,但請注意如果音頻文件已經開始緩存,但並沒有緩存完畢,用戶就切換到另一首音樂繼續播放,那麼這次緩存的文件肯定已經在緩存目錄中存在,下次再播放這首音樂時將按優先讀取緩存的原則,載入這個緩存文件進行播放,這顯然有很大的問題,因爲用戶無法聽完整首音樂就會結束。所以需要確認文件是否成功緩存完畢,否則下次播放同一首音樂的時候要覆蓋寫入,重新緩存才行。
def check_audio_cache(self):
"""
檢查緩存文件是否存在,且是否緩存完畢
:return: False 或 AudioSegment 對象
"""
cache_song = QM_DEFAULT_CACHE_PATH + self.song_info['filename']
if not os.path.isfile(cache_song):
return False
if os.path.getsize(cache_song) < 1024:
return False
audio_seg = AudioSegment.from_file(cache_song)
interval = int(self.song_info['interval']) * 1000
duration = audio_seg.duration_seconds * 1000
if duration > interval:
return audio_seg
else:
return False
那我們就先判斷文件是否已經緩存完畢,從上面的代碼可以看出來,首先確認緩存文件是否存在,然後再確認文件大小,如果小於1024字節,就不用繼續檢查了,因爲後面的檢查比較消耗時間,當然1024這個值可以調整爲更合理的值。如果能創建AudioSegment對象,那麼就把音頻文件的播放時長和上面取得的音樂信息中的時長進行比較,一般音頻文件的時長會一直讀取到毫秒級,而從騰訊服務器上讀取到的音頻信息只會記錄到秒,那麼判斷的標準就很清楚了,只要實際時長大於音頻信息中的時長,那麼緩存就是成功完成的。
AudioSegment 是 Pydub庫中的核心類,用於創建音頻剪輯對象,生成音頻數據幀以及獲取很多其他音頻信息。在GitHub上,Pydub的作者給出了很多範例和比較詳細的API文檔,建議去看一看。
六、簡約時尚的界面
簡約時尚可沒那麼簡單!構建GUI界面的工作絕對是非常麻煩、細緻、沒有絕對標準、考驗你的審美觀的工作,爲了讓這項工作變得比較簡單,就只有祭出模仿這個神器!從上面唯一的一張圖片可以看出來,模仿的是QQ音樂PC版的GUI界面。
看上去需要請個PS高手給我切出很多圖片來才能構建這個GUI界面。其實用不着,因爲PySide有很多設置GUI組件界面樣式的函數,可以直接調用它們來設置界面的樣式,但是這很麻煩,而且不易於從外部改變界面的樣式風格。更值的慶幸的是PySide允許我們通過設置QApplication或QWidget組件的CSS樣式表屬性來改變GUI組件的顯示樣式,這爲我們構建簡約時尚的GUI界面提供了強有力的支持!
如果你學過CSS,那麼後面的內容對你來說就很容易理解。如果你沒有學過,那也沒關係,Qt官方有文檔告訴你他們的組件都支持哪些CSS樣式表屬性,總的來說像:color、background-color、padding、margin、border、border-radius、height、width、min-width、max-width等一些常用的屬性PySide都是支持的。這些屬性的具體值的設置可以去看CSS樣式表手冊,裏面不僅文檔齊全,還有很多範例。由於CSS的設計初衷是給HTML用於定義Web頁面樣式使用的,因此手冊中的範例都是用HTML文檔的方式實現的。
在這個故事中,我們要讓CSS應用在PySide的GUI組件上,使用方式肯定和HTML不一樣。你可以在 http://doc.qt.io/qt-4.8/stylesheet-examples.html 這裏看到很多如何在PySide上使用CSS樣式的範例。