一个在线音乐软件的故事(四、现在就可以开始编码了吗?)

来源:互联网 发布:数控镗铣床编程代码 编辑:程序博客网 时间:2024/04/30 15:07

看起来一切已经就绪,我们选择了最熟悉的各种组件库,解决了音乐源的问题,似乎可以开始大刀阔斧的开工了。且慢!现在还不行,还要解决一些问题才能开工。我把这些问题称为技术障碍,必须先克服这些技术障碍,才能开始动手编码。

一、如何播放音频文件?

首先需要确认的是,音频播放。这里我们假设电脑上已经安装了FfmpegPyAudio这两个组件库,那么问题是如何在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的使用与PythonThread使用方式类似,都是通过线程类创建对象,调用start()接口方法启动线程,线程启动之后是从run()入口方法开始执行,通过调用wait()接口方法可以让线程进入等待状态,直到线程全部执行完毕或遇到quit()exit()方法调用时线程才会退出。但请注意并不是调用quit()或者exit()接口方法后线程就会立即退出,这可以通过QThreadisRunning()方法来判断。

有了这些方法,我们就可以修改上面的代码,用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界面有影响,还有一个问题,就是音乐不是立即开始播放的,你要等待一会儿才能听到音乐,并且根据音乐码率、时间长度的不同,有可能要等待比较长的时间才能听到音乐,而我们以前用过的在线音乐播放软件则很快就能开始播放音乐。

这是由我们的设计结构造成的,我们的方法是把文件全部缓存之后再播放,这就是导致要等待很久的主要原因!如何缩短等待时间呢?

方法就是边缓存边播放,在文件第一次缓存成功之后,就立即读取出来,分析出能够读取到的所有的音频帧数据,将其保存在一个数据列队中,在一个新的线程中将音频数据帧循环写入声卡数据流开始播放;第二次缓存成功之后也是这样操作,只是读取数据时不要从头开始,而是从上次读取结束的地方开始;第三次...第四次...;直到所有的帧数据都被读取到数据列队中。

每次缓存的数据量可以调整到合适的大小,一般在200KB400KB之间,对绝大多数网络环境来说,缓存这么多数据一般在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允许我们通过设置QApplicationQWidget组件的CSS样式表属性来改变GUI组件的显示样式,这为我们构建简约时尚的GUI界面提供了强有力的支持!

如果你学过CSS,那么后面的内容对你来说就很容易理解。如果你没有学过,那也没关系,Qt官方有文档告诉你他们的组件都支持哪些CSS样式表属性,总的来说像:colorbackground-colorpaddingmarginborderborder-radiusheightwidthmin-widthmax-width等一些常用的属性PySide都是支持的。这些属性的具体值的设置可以去看CSS样式表手册,里面不仅文档齐全,还有很多范例。由于CSS的设计初衷是给HTML用于定义Web页面样式使用的,因此手册中的范例都是用HTML文档的方式实现的。

在这个故事中,我们要让CSS应用在PySideGUI组件上,使用方式肯定和HTML不一样。你可以在 http://doc.qt.io/qt-4.8/stylesheet-examples.html 这里看到很多如何在PySide上使用CSS样式的范例。

阅读全文
0 0
原创粉丝点击