背景音楽

(Tested on CPython3.9.7 + Kivy2.1.0)

BGM再生を手助けするmodule

これが要った理由

このような物が要った理由は kivy.core.audio.Sound がそのままでは扱い難かったからです。 例えば kivy.core.audio.Sound.seek() の説明には

Most sound providers cannot seek when the audio is stopped. Play then seek.

とありますが単純に

sound = SoundLoader.load(...)
sound.play()
sound.seek(...)

として良いかというとそうではなく、実際には .play() の後に少し間を置かないと期待通りに動きませんでした。

sound = SoundLoader.load(...)
sound.play()
await async_library.sleep(...)
sound.seek(...)

そしてどうやらこれは .seek() に限った話ではなく、 .play().stop() を呼んだ後は少し間を置いてから sound に触らないと安定しないようです。 つまり kivy.core.audio.Sound を直接触るコードは仕様にすら書かれていないのに必要な でコードが汚されてしまう事になります。

そこでこのmoduleの出番です。 このmoduleは を完全に覆い隠すのに加えBGM再生に必要になると思われる次の機能を提供します。

  • 音を鳴らす時には前回停めた位置から再開。

  • 音を停める時には徐々に音量を下げる。

  • 音を鳴らす時には徐々に音量を上げる。

使い方

もしアプリが一種類のBGMしか鳴らさないのであれば Bgm だけで十分です。

from kivy.core.audio import SoundLoader
from kivyx.misc.bgmplayer import Bgm

sound = SoundLoader.load(r"path/to/bgm.ogg")
sound.loop = True
bgm = Bgm(sound)

# 鳴らしたくなったら
bgm.play()

# 停めたくなったら
bgm.stop()

そして複数のBGMがある場合は BgmPlayer が使えます。

from kivyx.misc.bgmplayer import BgmPlayer

bgmplayer = BgmPlayer()
bgmplayer.play(r"path/to/bgm1.ogg")  # bgm1.oggが鳴る
bgmplayer.play(r"path/to/bgm2.ogg")  # bgm1.oggが停まりbgm2.oggが鳴る
bgmplayer.play(r"path/to/bgm1.ogg")  # bgm2.oggが停まりbgm1.oggが前回停まった位置から鳴る
bgmplayer.stop()                     # bgm1.oggが停まる

BgmPlayer は既定では一度読み込んだBGMはずっと持ち続けるので次回以降の再生が早くなります。 言うまでもなくこれはメモリを惜しみなく使っている事によります。 もしこの振る舞いを良しとせずメモリの使用量を抑えたいのであれば MemoryEfficientLoader が使えます。

from kivyx.misc.bgmplayer import BgmPlayer, MemoryEfficientLoader

bgmplayer = BgmPlayer(loader=MemoryEfficientLoader())

MemoryEfficientLoader はBGMが別の物に切り替えられ次第すぐに以前の物を棄てるのでメモリの使用量が抑えられる…はずです [1] 。 反面切り替えられる度に kivy.core.audio.Sound を作り直すので二回目以降であってもあまり再生は早くならないです [2]

Loaderの自作

Loaderは自作することもできます。例えば以下のように予めBGMを一括で読み込んでおけば再生開始時の遅延を限りなく抑えられるでしょう。

loader = {
    fn: (sound := SoundLoader.load(fn), setattr(sound, 'loop', True), ) and Bgm(sound)
    for fn in filenames
}
bgmplayer = BgmPlayer(loader=loader)

loaderは loader[key] という式で Bgm のinstanceを取り出せるようになっている物なら何でも良く、 うまく使えば独自のキャッシュ戦略や読み込み戦略を採れるかもしれません。

class kivyx.misc.bgmplayer.Bgm(*args, **kwargs)[source]

Bases: EventDispatcher

一つ分のBGMの再生を司る下層のclass。

Parameters:

sound (kivy.core.audio.Sound) –

fade_in_duration = 2.0

何秒かけて音量を徐々に上げるか

fade_out_duration = 2.0

何秒かけて音量を徐々に下げるか

internal_delay = 0.5

kivy.core.audio.Sound を操作する度に設ける内部用の待ち時間。

length = None

(read-only) BGMの長さ。単位は秒。instance化直後はこの値はNoneなので実際の長さが入れられるまで少し待たないといけない。

import asynckivy as ak

bgm = Bgm(...)
if bgm.length is None:
    await ak.event(bgm, 'length')
print(f"BGMの長さは{bgm.length}秒です")
play(*, reset_pos=False)[source]

BGMを鳴らす。既定では前回の停止位置から再開するが reset_pos が真だと常に最初から鳴る。

property pos

再生位置。単位は秒。 kivy.core.audio.Sound.seek() とは違って停止中にも自由に動かせる。

stop()[source]

BGMを停める

unload()[source]

kivy.core.audio.Sound.unload() を呼んで資源を解放する。以後はこのオブジェクトを操作しても何も起こらない。

volume = 1.0

上げきった時の音量

class kivyx.misc.bgmplayer.BgmPlayer(*args, **kwargs)[source]

Bases: EventDispatcher

fade_in_duration = 2.0

何秒かけて音量を徐々に上げるか

fade_out_duration = 2.0

何秒かけて音量を徐々に下げるか

internal_delay = 0.5

Bgm.internal_delay

play(key, *, reset_pos=False)[source]

BGMを鳴らす。既定では前回の停止位置から再開するが reset_pos が真だと常に最初から鳴る。

stop()[source]

BGMを停める

volume = 1.0

上げきった時の音量

class kivyx.misc.bgmplayer.CachedLoader[source]

Bases: dict

一度読み込んだBGMをずっと保持し続けるLoader

class kivyx.misc.bgmplayer.MemoryEfficientLoader[source]

Bases: object

BGM読み込み時に以前のBGMを破棄してメモリ使用量を抑えるLoader