PyGame声音

版权声明:本文为博主原创文章,转载请注明出处: https://blog.csdn.net/Hubz131/article/details/86771378

一、前言

        声音是游戏中必要的元素之一,音效可以给予用户良好的反馈体验。赛车的时候可以听到振奋人心的启动时的引擎声和刹车时轮胎摩擦声,射击游戏中枪支弹药的音效和呐喊助威的嗓音,无一不是让人热血沸腾的要因。

        宛若电影,最初的电影史无声的,而自从1927年第一部公认的有声电影放映之后,人们的娱乐项目一下子丰富了好多;游戏中也是啊,好的配音绝对可以给我们的作品增色不少。这几次就是给我们的pygame作品中增加美妙的声音的。

二、声音三要素

音调:声音的高低,由频率决定,频率大,音调高
响度:声音的大小,由振幅决定,振幅大,响度大
音色:声音的好坏,由发声体材料和结构决定

三、声音的存储

声音完全是一种模拟的信号,而我们的计算机只能存储数字(二进制)信号,所以要对声音进行数字化

(以下内容摘录修改自轩辕天数-丝竹的文章

         以最常见的WAV文件为例,要把声音记录成WAV格式,电脑要先把声音的波形“画在一张坐标纸上”。然后呢,电脑要看了“横坐标第一格处,波形图的纵坐标是多少啊?哦,差不多是500啊(仅仅是打比方,而且这个“差不多”很关键),那么横坐标第二格呢?…”最后,电脑就得出来一大堆坐标值。然后再经过一些其他后续工作,电脑就把这些坐标值保存下来了。

       当要放音的时候,电脑要根据这些“坐标值在坐标纸上面画点”,最后“用线把点连起来”,差不多就把原先的波形还原出来了。其实数字化录音基本上就是这样的原理。

        电脑记录波形时,用的坐标纸格子越密,自然记录下来的点就越多、越精确,将来还原出来的波形就越接近原始波形?上边例子的横坐标轴格子密度就相当于采样频率(比如,44.1KHz),纵坐标格子密度就相当于量化精度(比如,16BIT)。这就是“KHZ”、“BIT”的值越高,音乐的音质越好的原因。

       这个世界上自然不仅仅有WAV这一种存储声音的文件格式,宛若图像文件格式中的BMP一样,WAV是一种无压缩的格式,体积最大;而OGG则好像PNG,是无损的压缩,可以完全保持图像的本真,但是大小又比较小;常用的MP3,则是类似于JPG的有损压缩格式

四、声音处理

      想要获得声音,最简单的自然是录制,不过有的时候比较困难,比如录制心跳要很高昂的仪器,而录制火山爆发的声音实在过于……

        这时候我们可以手动合成声音,而录制获得的声音还需要经过处理,比如净化等,有很多软件可以选择,开源的Audacity就是一个很不错的选择。

网上也有很多声音素材可供下载,好的专业的素材都是卖钱的,哎这个世界什么都是钱啊~~

五、Pygame中声音的初始化

       在pygame中,使用mixer模块来播放声音,不过在实际播放之前,我们需要使用pygame.mixer.init函数来初始化一些参数,不过在有的平台上,pygame.mixer.init会随着pygame.init一起被初始化,pygame干脆提供了一个pygame.mixer.pre_init()来进行最先的初始化工作,参数说明如下:

frequency 

声音文件的采样率,尽管高采样率可能会降低性能,但是再次的声卡都可以轻松对应44.1KHz的声音回放
size  量化精度

stereo

立体声效果,1:mono,2:stereo,具体请百度或者搜狗,一般设为2
buffer 缓冲大小,2的倍数,一般设为4096

你可以像这样初始化声音:

pygame.mixer.pre_init(44100, 16, 2, 4096)

pygame.init()

这里先用pre_init来设定了参数,然后在pygame.init中初始化所有的东西。

如果你需要重新设定声音的参数,那么你需要先执行pygame.mixer.quit然后再执行pygame.mixer.init,不过一般用不到吧……

紧接着上一次,我们继续来看如何在Pygame中使用声音。

六、Sound对象

在初始化声音系统之后,我们就可以读取一个音乐文件到一个Sound对象中了。pygame.mixer.Sound()接受一个文件名,或者也可以使一个文件对象,不过这个文件必须是WAV或者OGG,切记!

hello_sound = Pygame.mixer.Sound("hello.ogg")

一旦这个Sound对象出来了,你可以使用play方法来播放它。play(loop, maxtime)可以接受两个参数,loop自然就是重复的次数,-1意味着无限循环,1呢?是两次,记住是重复的次数而不是播放的次数;maxtime是指多少毫秒后结束,这个很简单。当你不使用任何参数调用的时候,意味着把这个声音播放一次。一旦play方法调用成功,就会返回一个Channel对象,否则返回一个None。

七、Channel对象

Channel,也就是声道,可以被声卡混合(共同)播放的数据流。游戏中可以同时播放的声音应该是有限的,pygame中默认是8个,你可以通过pygame.mixer.get_num_channels()来得知当前系统可以同时播放的声道数,而一旦超过,调用sound对象的play方法就会返回一个None,如果你确定自己要同时播放很多声音,可以用set_num_channels()来设定一下,最好一开始就设,因为重新设定会停止当前播放的声音。

那么Channel对象有什么用呢?如果你只是想简单的播放一下声音,那么根本不用管这个东西,不过如果你想创造出一点有意境的声音,比如说一辆火车从左到右呼啸而过,那么应该是一开始左声道比较响,然后相当,最后右声道比较响,直至慢慢消失。这就是Channel能做到的事情。Channel的set_volume(left, right)方法接受两个参数,分别是代表左声道和右声道的音量的小数,从0.0~1.0。

八、竖起我们的耳朵

OK,来个例子试试吧~

这个例子里我们通过释放金属小球并让它们自由落体和弹跳,听碰撞的声音。这里面不仅仅有这次学习的声音,还有几个一起没有接触到的技术,最重要的一个就是重力的模拟,我们可以设置一个重力因子来影响小球下落的速度,还有一个弹力系数,来决定每次弹跳损失的能量,虽然不难,但是游戏中引入这个东西能让我们的游戏仿真度大大提高。

ball.png:ball.png                        mousesor.png:mousecursor.png            bounce.ogg

# -*- coding: utf-8 -*- 
# Time : 2019/2/7 19:52 
# Author : hubozhi
import pygame
from pygame.locals import *
from random import randint
from gameobjects.vector2 import Vector2
SCREEN_SIZE = (640, 480)

# 重力因子,实际上是单位 像素/平方秒
GRAVITY = 250.0
# 弹力系数,不要超过1!
BOUNCINESS = 0.7

def stero_pan(x_coord,screen_width):
    """这个函数根据位置决定要播放声音左右声道的音量"""
    right_volume = float(x_coord)  / screen_width
    left_volume = 1.0 - right_volume
    return (left_volume, right_volume)
class Ball():
    """小球类,实际上我们可以使用Sprite类来简化"""
    def __init__(self,position,speed,image,bounce_sound):
        self.position= Vector2(position)
        self.speed= Vector2(speed)
        self.image= image
        self.bounce_sound= bounce_sound
        self.age= 0.0

    def update(self,time_passed):
        w,h= self.image.get_size()
        screen_width,screen_height= SCREEN_SIZE
        x,y= self.position
        x-= w / 2
        y-= h / 2
        # 是不是要反弹了
        bounce= False

          # 小球碰壁了么?
        if y+h>= screen_height:
            self.speed.y= -self.speed.y*BOUNCINESS
            self.position.y= screen_height - h/2.0- 1.0
            bounce = True

        elif y <= 0:
            self.speed.y = -self.speed.y * BOUNCINESS
            self.position.y = h*0.5
            bounce = True

        if x <= 0:
            self.speed.x = -self.speed.x*BOUNCINESS
            self.position.x = w/2.0 + 1
            bounce = True

        elif x+w >= screen_width:
            self.speed.x= -self.speed.x*BOUNCINESS
            self.position.x= screen_width - w/2.0- 1
            bounce= True

        # 根据时间计算现在的位置,物理好的立刻发现这其实不标准,
        # 正规的应该是“s = 1/2*g*t*t”,不过这样省事省时一点,咱只是模拟~
        self.position += self.speed*time_passed
          # 根据重力计算速度
        self.speed.y += time_passed*GRAVITY

        if bounce:
            self.play_bounce_sound()
            self.age += time_passed


    def play_bounce_sound(self):
        """这个就是播放声音的函数"""
        channel= self.bounce_sound.play()
        if channel is not None:
            # 设置左右声道的音量
            left,right= stero_pan(self.position.x,SCREEN_SIZE[0])
            channel.set_volume(left,right)

    def render(self,surface):
        # 真有点麻烦了,有爱的,自己用Sprite改写下吧……
        w,h= self.image.get_size()
        x,y= self.position
        x -= w / 2
        y-= h / 2
        surface.blit(self.image,(x,y))

def run():
    # 上一次的内容
    pygame.mixer.pre_init(44100,16,2,1024 * 4)
    pygame.init()
    pygame.mixer.set_num_channels(8)
    screen= pygame.display.set_mode(SCREEN_SIZE,0)

    pygame.mouse.set_visible(False)
    clock= pygame.time.Clock()

    ball_image= pygame.image.load("./image/ball.png").convert_alpha()
    mouse_image= pygame.image.load("./image/mousecursor.png").convert_alpha()


    # 加载声音文件
    bounce_sound = pygame.mixer.Sound("./sound/bounce.ogg")
    balls = []

    while True:
        for event in pygame.event.get():
            if event.type == QUIT:
                return
            if event.type == MOUSEBUTTONDOWN:
                # 刚刚出来的小球,给一个随机的速度
                random_speed= (randint(-400,400), randint(-300,0))
                new_ball= Ball(event.pos,random_speed, ball_image,bounce_sound)
                balls.append(new_ball)

        time_passed_seconds= clock.tick() / 1000.
        screen.fill((255,255,255))
        # 为防止小球太多,把超过寿命的小球加入这个“死亡名单”

        dead_balls= []
        for ball in balls:
            ball.update(time_passed_seconds)
            ball.render(screen)
        # 每个小球的的寿命是10秒
            if ball.age> 10.0:
                dead_balls.append(ball)

        for ball in dead_balls:
            balls.remove(ball)


        mouse_pos= pygame.mouse.get_pos()
        screen.blit(mouse_image,mouse_pos)
        pygame.display.update()

if __name__ == "__main__":
    run()

这么久了,咱们的游戏终于有了声音,太棒了!不过,是不是游戏的背景音乐也是用mixer来播放的呢?这不是一个好主意,因为背景音乐往往很长,比较占资源,pygame中另外提供了一个pygame.mixer.music类来控制背景音乐的播放。

pygame.mixer并不适合播放长时间的音乐播放,我们要使用pygame.mixer.music。

pygame.mixer.music用来播放MP3和OGG音乐文件,不过MP3并不是所有的系统都支持(Linux默认就不支持MP3播放),所以最好还是都用Ogg文件,我们可以很容易把MP3转换为Ogg文件,自己搜一下吧。

我们使用pygame.mixer.music.load()来加载一个文件,然后使用pygame.mixer.music.play()来播放,这里并没有一个类似Music这样的类和对象,因为背景音乐一般般只要有一个在播放就好了不是么~不放的时候就用stop()方法来停止就好了,当然很自然有类似录影机上的pause()和unpause()方法。

九、音效和音乐方法总结

Sound对象

方法名

作用

fadeout

淡出声音,可接受一个数字(毫秒)作为淡出时间

get_length

获得声音文件长度,以秒计

get_num_channels

声音要播放多少次

get_volume

获取音量(0.0 ~ 1.0)

play

开始播放,返回一个Channel对象,失败则返回None

set_volume

设置音量

stop

立刻停止播放

Channels对象

方法名

作用

fadeout

类似

get_busy

如果正在播放,返回true

get_endevent

获取播放完毕时要做的event,没有则为None

get_queue

获取队列中的声音,没有则为None

get_volume

类似

pause

暂停播放

play

类似

queue

将一个Sound对象加入队列,在当前声音播放完毕后播放

set_endevent

设置播放完毕时要做的event

set_volume

类似

stop

立刻停止播放

unpause

继续播放

Music对象:

方法名

作用

fadeout

类似

get_endevent

类似

get_volume

类似

load

加载一个音乐文件

pause

类似

play

类似

rewind

从头开始重新播放

set_endevent

类似

set_volume

类似

stop

立刻停止播放

unpause

继续播放

get_pos

获得当前播放的位置,毫秒计

虽然很简单,不过还是提供一个例程吧,这里面音乐的播放很简单,就是上面讲过的,不过其中还有一点其他的东西,希望大家学习一下pygame中按钮的实现方法。

界面如上,运行的时候,脚本读取./MUSIC下所有的OGG和MP3文件(如果你不是Windows,可能要去掉MP3的判断),显示的也很简单,几个控制按钮,下面显示当前歌名(显示中文总是不那么方便的,如果你运行失败,请具体参考代码内的注释自己修改):

# -*- coding: utf-8 -*-
# Time : 2019/2/8 11:17
# Author : hubozhi
import pygame
from pygame.locals import *
from math import sqrt
import os
import os.path

# 存放音乐文件的位置
MUSIC_PATH = "./sound/"

SCREEN_SIZE = (800, 600)

def get_music(path):
    # 从文件夹来读取所有的音乐文件
    raw_filenames = os.listdir(path)
    print('raw_filenames',raw_filenames)
    music_files = []

    for filename in raw_filenames:
        # 不是Windows的话,还是去掉mp3吧
        if filename.lower().endswith('.ogg') or filename.lower().endswith('.mp3'):
            music_files.append(os.path.join(MUSIC_PATH, filename))

    return sorted(music_files)

class Button(object):
    """这个类是一个按钮,具有自我渲染和判断是否被按上的功能"""
    def __init__(self,image_filename,position):
        self.position = position
        self.image = pygame.image.load(image_filename)

    def render(self, surface):
        # 家常便饭的代码了
        x, y = self.position
        w, h = self.image.get_size()
        x -= w / 2
        y -= h / 2
        surface.blit(self.image, (x, y))

    def is_over(self,point):
        # 如果point在自身范围内,返回True
        point_x,point_y= point
        x,y= self.position
        w,h= self.image.get_size()
        x-= w/2
        y-= h/ 2

        in_x= point_x>= x and point_x < x + w
        in_y= point_y>= y and point_y < y + h

        return in_x and in_y


def run():
    pygame.mixer.pre_init(44100,16,2,1024*4)
    pygame.init()
    screen= pygame.display.set_mode(SCREEN_SIZE,0)

    #font = pygame.font.SysFont("default_font", 50, False)

    # 为了显示中文,我这里使用了这个字体,具体自己机器上的中文字体请自己查询
    # 详见本系列第四部分://eyehere.net/2011/python-pygame-novice-professional-4/
    font= pygame.font.SysFont("simsunnsimsun",50,False)
    x = 100
    y = 240

    button_width = 150
    buttons = {}
    buttons["prev"] = Button("./image/prev.png", (x, y))
    buttons["pause"] = Button("./image/pause.png", (x+button_width*1,y))
    buttons["stop"] = Button("./image/stop.png", (x+button_width*2, y))
    buttons["play"] = Button("./image/play.png", (x+button_width*3, y))
    buttons["next"] = Button("./image/next.png", (x+button_width*4, y))


    music_filenames = get_music(MUSIC_PATH)
    if  len(music_filenames) == 0:
        print("No music files found in ",MUSIC_PATH)
        return

    white = (255,255,255)
    label_surfaces = []
    #一系列的文件名render
    for filename in music_filenames:
        txt = os.path.split(filename)[-1]
        print("Track:",txt)
        # 这是简体中文Windows下的文件编码,根据自己系统情况请酌情更改
        txt = txt.split('.')[0]#.decode('gb2312')
        surface = font.render(txt, True, (100, 0, 100))
        label_surfaces.append(surface)

    current_track = 0
    max_tracks = len(music_filenames)
    pygame.mixer.music.load(music_filenames[current_track])


    clock = pygame.time.Clock()
    playing = False
    paused = False


    # USEREVENT是什么?请参考本系列第二部分:
    # //eyehere.net/2011/python-pygame-novice-professional-2/
    TRACK_END = USEREVENT + 1
    pygame.mixer.music.set_endevent(TRACK_END)


    while True:
        button_pressed = None

        for event in pygame.event.get():
            if  event.type == QUIT:
                return

            if event.type == MOUSEBUTTONDOWN:
                # 判断哪个按钮被按下
                for button_name, button in buttons.items():
                    if button.is_over(event.pos):
                        print(button_name,"pressed")
                        button_pressed = button_name
                        break

            if event.type == TRACK_END:
                # 如果一曲播放结束,就“模拟”按下"next"
                button_pressed = "next"


        if button_pressed is not None:
            if button_pressed == "next":
                current_track = (current_track + 1) % max_tracks
                pygame.mixer.music.load( music_filenames[current_track])
                if playing:
                    pygame.mixer.music.play()

            elif button_pressed == "prev":
                # prev的处理方法:
                #  已经播放超过3秒,从头开始,否则就播放上一曲
                if pygame.mixer.music.get_pos() > 3000:
                    pygame.mixer.music.stop()
                    pygame.mixer.music.play()
                else:
                    current_track = (current_track - 1) % max_tracks
                    pygame.mixer.music.load( music_filenames[current_track] )
                    if playing:
                        pygame.mixer.music.play()

            elif button_pressed == "pause":
                if paused:
                    pygame.mixer.music.unpause()
                    paused = False
                else:
                    pygame.mixer.music.pause()
                    paused = True

            elif button_pressed == "stop":
                pygame.mixer.music.stop()
                playing = False

            elif button_pressed == "play":
                if paused:
                    pygame.mixer.music.unpause()
                    paused = False
                else:
                    if not playing:
                        pygame.mixer.music.play()
                        playing = True

        screen.fill(white)

        # 写一下当前歌名
        label = label_surfaces[current_track]
        w, h = label.get_size()
        screen_w = SCREEN_SIZE[0]
        screen.blit(label, ((screen_w - w)/2, 450))


        # 画所有按钮
        for button in buttons.values():
            button.render(screen)

        # 因为基本是不动的,这里帧率设的很低
        clock.tick(5)
        pygame.display.update()

if __name__ == "__main__":
    run()

 

猜你喜欢

转载自blog.csdn.net/Hubz131/article/details/86771378