changeset 783:ec1e06402a97

Replace SDL2_mixer with the kira crate
author Link Mauve <linkmauve@linkmauve.fr>
date Fri, 21 Nov 2025 10:21:59 +0100
parents a30ce01b9154
children 1f152ca95658
files README formats/src/th06/mod.rs formats/src/th06/pos.rs python/Cargo.toml python/src/audio.rs python/src/lib.rs pytouhou/formats/music.py pytouhou/game/game.pxd pytouhou/game/music.pxd pytouhou/game/music.py pytouhou/games/eosd/interface.py pytouhou/lib/_sdl.pxd pytouhou/lib/sdl.pxd pytouhou/lib/sdl.pyx pytouhou/resource/loader.py pytouhou/ui/gamerunner.pyx pytouhou/ui/music.pyx pytouhou/vm/msgrunner.py scripts/pytouhou setup.py
diffstat 20 files changed, 165 insertions(+), 316 deletions(-) [+]
line wrap: on
line diff
--- a/README
+++ b/README
@@ -17,7 +17,7 @@
     * A working OpenGL driver
     * libepoxy
     * SDL2
-    * SDL2_image, SDL2_mixer, SDL2_ttf
+    * SDL2_image, SDL2_ttf
     * A TTF font file, placed as “font.ttf” in the game directory.
 
 
--- a/formats/src/th06/mod.rs
+++ b/formats/src/th06/mod.rs
@@ -5,3 +5,4 @@
 pub mod ecl;
 pub mod std;
 pub mod msg;
+pub mod pos;
new file mode 100644
--- /dev/null
+++ b/formats/src/th06/pos.rs
@@ -0,0 +1,21 @@
+//! POS music track format support.
+
+use nom::{IResult, Parser, number::complete::le_u32};
+
+/// A struct describing the loop points of Touhou background music.
+#[derive(Debug, Clone)]
+pub struct LoopPoints {
+    /// Time to which the music should loop after reaching end.
+    pub start: u32,
+
+    /// Time at which to loop back to start.
+    pub end: u32,
+}
+
+impl LoopPoints {
+    /// Parse a slice of bytes into a `LoopPoints` struct.
+    pub fn from_slice(input: &[u8]) -> IResult<&[u8], LoopPoints> {
+        let (i, (start, end)) = (le_u32, le_u32).parse(input)?;
+        Ok((i, LoopPoints { start, end }))
+    }
+}
--- a/python/Cargo.toml
+++ b/python/Cargo.toml
@@ -17,6 +17,7 @@
 pyo3 = "0.27"
 image = { version = "0.25", default-features = false, features = ["png"], optional = true }
 glob = "0.3.3"
+kira = "0.11.0"
 
 [features]
 default = []
new file mode 100644
--- /dev/null
+++ b/python/src/audio.rs
@@ -0,0 +1,101 @@
+use kira::sound::static_sound::StaticSoundData;
+use kira::sound::streaming::{StreamingSoundData, StreamingSoundHandle};
+use kira::sound::FromFileError;
+use kira::{AudioManager, AudioManagerSettings, Tween};
+use pyo3::prelude::*;
+use std::collections::HashMap;
+use std::io::Cursor;
+use std::path::PathBuf;
+
+#[pyclass(module = "libtouhou")]
+pub struct Audio {
+    loader: Py<super::Loader>,
+    manager: AudioManager,
+    cache: HashMap<String, StaticSoundData>,
+    bgms: [Option<(String, String)>; 4],
+    current_music: Option<StreamingSoundHandle<FromFileError>>,
+}
+
+#[pymethods]
+impl Audio {
+    #[new]
+    fn new(loader: Py<super::Loader>, bgms: [Option<(String, String)>; 4]) -> Audio {
+        let manager =
+            AudioManager::<kira::DefaultBackend>::new(AudioManagerSettings::default()).unwrap();
+        let cache = HashMap::new();
+        Audio {
+            loader,
+            manager,
+            cache,
+            bgms,
+            current_music: None,
+        }
+    }
+
+    fn play_bgm(&mut self, py: Python, number: usize) {
+        let Some((_name, filename)) = &self.bgms[number] else {
+            eprintln!("Unspecified bgm number {number}");
+            return;
+        };
+
+        // Load the loop points corresponding to this bgm.
+        let mut filename = PathBuf::from(filename);
+        filename.set_extension("pos");
+        let loader = self.loader.borrow(py);
+        let loop_points = loader
+            .get_loop_points(filename.file_name().unwrap().to_str().unwrap())
+            .unwrap();
+
+        // Then try to open the music file.
+        filename.set_extension("*");
+        let path = loader
+            .game_dir
+            .clone()
+            .and_then(|dir| Some(dir.join(&filename)))
+            .unwrap_or(filename);
+        let mut music = None;
+        for path in glob::glob(path.to_str().unwrap())
+            .unwrap()
+            .map(Result::unwrap)
+            .map(PathBuf::from)
+        {
+            match StreamingSoundData::from_file(&path) {
+                Ok(sound) => {
+                    music = Some(sound);
+                    break;
+                }
+                Err(err) => {
+                    eprintln!("Error while opening {path:?} as a music file: {err:?}");
+                    continue;
+                }
+            }
+        }
+        let Some(music) = music else {
+            eprintln!("Unable to find bgm, let’s keep the previous one playing…");
+            return;
+        };
+
+        // Stop the previous playing one.
+        if let Some(current_music) = &mut self.current_music {
+            current_music.stop(Tween::default());
+        }
+
+        // And now we can start playing the new one!
+        let mut current_music = self.manager.play(music).unwrap();
+        // TODO: Fetch the sample rate from the file, instead of hardcoding it.
+        current_music.set_loop_region(
+            (loop_points.start as f64 / 44100.0)..(loop_points.end as f64 / 44100.0),
+        );
+        self.current_music = Some(current_music);
+    }
+
+    fn play(&mut self, py: Python, name: &str) {
+        let sound = self.cache.entry(name.to_string()).or_insert_with(|| {
+            let loader = self.loader.borrow(py);
+            let bytes = loader.get_file_internal(name).unwrap();
+            let cursor = Cursor::new(bytes);
+            StaticSoundData::from_cursor(cursor).unwrap()
+        });
+        self.manager.play(sound.clone()).unwrap();
+    }
+}
--- a/python/src/lib.rs
+++ b/python/src/lib.rs
@@ -1,6 +1,7 @@
 use pyo3::exceptions::{PyIOError, PyKeyError};
 use pyo3::prelude::*;
 use pyo3::types::{PyBytes, PyTuple};
+use touhou_formats::th06::pos::LoopPoints;
 use touhou_formats::th06::pbg3;
 use touhou_formats::th06::std as stage;
 use touhou_formats::th06::msg;
@@ -13,6 +14,8 @@
 #[cfg(feature = "glide")]
 mod glide;
 
+mod audio;
+
 #[pyclass(module = "libtouhou")]
 struct PyModel {
     inner: stage::Model,
@@ -96,7 +99,7 @@
                 msg::Instruction::SpawnEnemySprite() => (6, ().into_pyobject(py)?),
                 msg::Instruction::ChangeMusic(track) => (7, (track,).into_pyobject(py)?),
                 msg::Instruction::DisplayDescription(side, index, text) => (8, (side, index, text).into_pyobject(py)?),
-                msg::Instruction::ShowScores(unk1) => (8, (unk1,).into_pyobject(py)?),
+                msg::Instruction::ShowScores(unk1) => (9, (unk1,).into_pyobject(py)?),
                 msg::Instruction::Freeze() => (10, ().into_pyobject(py)?),
                 msg::Instruction::NextStage() => (11, ().into_pyobject(py)?),
                 msg::Instruction::Unk2() => (12, ().into_pyobject(py)?),
@@ -205,11 +208,22 @@
     }
 }
 
+impl Loader {
+    fn get_loop_points(&self, name: &str) -> Result<LoopPoints, ()> {
+        let vec = self.get_file_internal(name).unwrap();
+        let (_, inner) = LoopPoints::from_slice(&vec).unwrap();
+        Ok(inner)
+    }
+}
+
 #[pymodule]
 mod libtouhou {
     #[pymodule_export]
     use super::Loader;
 
+    #[pymodule_export]
+    use crate::audio::Audio;
+
     #[cfg(feature = "glide")]
     #[pymodule_export]
     use super::glide::module;
deleted file mode 100644
--- a/pytouhou/formats/music.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# -*- encoding: utf-8 -*-
-##
-## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-##
-## This program is free software; you can redistribute it and/or modify
-## it under the terms of the GNU General Public License as published
-## by the Free Software Foundation; version 3 only.
-##
-## This program is distributed in the hope that it will be useful,
-## but WITHOUT ANY WARRANTY; without even the implied warranty of
-## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-## GNU General Public License for more details.
-##
-
-
-from struct import unpack
-
-
-class Track:
-    def __init__(self):
-        self.start = 0
-        self.end = 0
-
-
-    @classmethod
-    def read(cls, file):
-        self = cls()
-        self.start, self.end = unpack('<II', file.read(8))
-        return self
--- a/pytouhou/game/game.pxd
+++ b/pytouhou/game/game.pxd
@@ -1,7 +1,6 @@
 from pytouhou.game.effect cimport Effect
 from pytouhou.game.player cimport Player
 from pytouhou.game.text cimport Text, NativeText
-from pytouhou.game.music cimport MusicPlayer
 from pytouhou.utils.random cimport Random
 
 cdef class Game:
@@ -9,7 +8,7 @@
     cdef public list bullet_types, laser_types, item_types, players, enemies, effects, bullets, lasers, cancelled_bullets, players_bullets, players_lasers, items, labels, faces, hints, bonus_list
     cdef public object interface, boss, msg_runner
     cdef public dict texts
-    cdef public MusicPlayer sfx_player
+    cdef public object sfx_player, music
     cdef public Random prng
     cdef public double continues
     cdef public Effect spellcard_effect
deleted file mode 100644
--- a/pytouhou/game/music.pxd
+++ /dev/null
@@ -1,3 +0,0 @@
-cdef class MusicPlayer:
-    cpdef play(self, name)
-    cpdef set_volume(self, name, float volume)
deleted file mode 100644
--- a/pytouhou/game/music.py
+++ /dev/null
@@ -1,6 +0,0 @@
-class MusicPlayer:
-    def play(self, name):
-        pass
-
-    def set_volume(self, name, volume):
-        pass
--- a/pytouhou/games/eosd/interface.py
+++ b/pytouhou/games/eosd/interface.py
@@ -150,7 +150,6 @@
                 else:
                     timeout_label.set_color('red')
                 if (boss.timeout - boss.frame) % 60 == 0 and boss.timeout != 0:
-                    self.game.sfx_player.set_volume('timeout.wav', 1.)
                     self.game.sfx_player.play('timeout.wav')
             timeout_label.set_text('%02d' % (timeout if timeout >= 0 else 0))
             timeout_label.changed = True
--- a/pytouhou/lib/_sdl.pxd
+++ b/pytouhou/lib/_sdl.pxd
@@ -165,40 +165,6 @@
     SDL_Surface *IMG_LoadPNG_RW(SDL_RWops *src)
 
 
-cdef extern from "SDL_mixer.h" nogil:
-    ctypedef enum:
-        MIX_DEFAULT_FORMAT
-
-    ctypedef struct Mix_Music:
-        pass
-
-    ctypedef struct Mix_Chunk:
-        pass
-
-    int Mix_Init(int flags)
-    void Mix_Quit()
-
-    int Mix_OpenAudio(int frequency, Uint16 format_, int channels, int chunksize)
-    void Mix_CloseAudio()
-
-    int Mix_AllocateChannels(int numchans)
-
-    Mix_Music *Mix_LoadMUS(const char *filename)
-    Mix_Chunk *Mix_LoadWAV_RW(SDL_RWops *src, int freesrc)
-
-    void Mix_FreeMusic(Mix_Music *music)
-    void Mix_FreeChunk(Mix_Chunk *chunk)
-
-    int Mix_PlayMusic(Mix_Music *music, int loops)
-    #int Mix_SetLoopPoints(Mix_Music *music, double start, double end)
-
-    int Mix_Volume(int channel, int volume)
-    int Mix_VolumeChunk(Mix_Chunk *chunk, int volume)
-    int Mix_VolumeMusic(int volume)
-
-    int Mix_PlayChannel(int channel, Mix_Chunk *chunk, int loops)
-
-
 cdef extern from "SDL_pixels.h" nogil:
     ctypedef struct SDL_Color:
         Uint8 r, g, b, a
--- a/pytouhou/lib/sdl.pxd
+++ b/pytouhou/lib/sdl.pxd
@@ -95,20 +95,6 @@
     cdef void set_alpha(self, Surface alpha_surface) nogil
 
 
-cdef class Music:
-    cdef Mix_Music *music
-
-    cdef void play(self, int loops) nogil
-    cdef void set_loop_points(self, double start, double end) nogil
-
-
-cdef class Chunk:
-    cdef Mix_Chunk *chunk
-
-    cdef void play(self, int channel, int loops) nogil
-    cdef void set_volume(self, float volume) nogil
-
-
 cdef class Font:
     cdef TTF_Font *font
 
@@ -117,17 +103,10 @@
 
 cdef bint init(Uint32 flags) except True
 cdef bint img_init(int flags) except True
-cdef bint mix_init(int flags) except True
 cdef bint ttf_init() except True
 cdef bint gl_set_attribute(SDL_GLattr attr, int value) except True
 cdef Surface load_png(file_)
 cdef Surface create_rgb_surface(int width, int height, int depth, Uint32 rmask=*, Uint32 gmask=*, Uint32 bmask=*, Uint32 amask=*)
-cdef bint mix_open_audio(int frequency, Uint16 format_, int channels, int chunksize) except True
-cdef bint mix_allocate_channels(int numchans) except True
-cdef int mix_volume(int channel, float volume) nogil
-cdef int mix_volume_music(float volume) nogil
-cdef Music load_music(str filename)
-cdef Chunk load_chunk(file_)
 cdef Uint32 get_ticks() nogil
 cdef void delay(Uint32 ms) nogil
 cpdef bint show_simple_message_box(unicode message) except True
--- a/pytouhou/lib/sdl.pyx
+++ b/pytouhou/lib/sdl.pyx
@@ -62,8 +62,7 @@
 
 
 class SDL:
-    def __init__(self, *, video=True, sound=True):
-        self.sound = sound
+    def __init__(self, *, video=True):
         self.video = video
 
     def __enter__(self):
@@ -77,23 +76,7 @@
 
         keyboard_state = SDL_GetKeyboardState(NULL)
 
-        if self.sound:
-            mix_init(0)
-            try:
-                mix_open_audio(44100, MIX_DEFAULT_FORMAT, 2, 4096)
-            except SDLError as error:
-                logger.error(u'Impossible to set up audio subsystem: %s', error)
-                self.sound = False
-            else:
-                # TODO: make it dependent on the number of sound files in the
-                # archives.
-                mix_allocate_channels(MAX_SOUNDS)
-
     def __exit__(self, *args):
-        if self.sound:
-            Mix_CloseAudio()
-            Mix_Quit()
-
         TTF_Quit()
         IMG_Quit()
         SDL_Quit()
@@ -278,31 +261,6 @@
             image[3+4*i] = alpha[3*i]
 
 
-cdef class Music:
-    def __dealloc__(self):
-        if self.music != NULL:
-            Mix_FreeMusic(self.music)
-
-    cdef void play(self, int loops) nogil:
-        Mix_PlayMusic(self.music, loops)
-
-    cdef void set_loop_points(self, double start, double end) nogil:
-        #Mix_SetLoopPoints(self.music, start, end)
-        pass
-
-
-cdef class Chunk:
-    def __dealloc__(self):
-        if self.chunk != NULL:
-            Mix_FreeChunk(self.chunk)
-
-    cdef void play(self, int channel, int loops) nogil:
-        Mix_PlayChannel(channel, self.chunk, loops)
-
-    cdef void set_volume(self, float volume) nogil:
-        Mix_VolumeChunk(self.chunk, int(volume * 128))
-
-
 cdef class Font:
     def __init__(self, str filename, int ptsize):
         path = filename.encode()
@@ -335,11 +293,6 @@
         raise SDLError()
 
 
-cdef bint mix_init(int flags) except True:
-    if Mix_Init(flags) != flags:
-        raise SDLError()
-
-
 cdef bint ttf_init() except True:
     if TTF_Init() < 0:
         raise SDLError()
@@ -369,44 +322,6 @@
     return surface
 
 
-cdef bint mix_open_audio(int frequency, Uint16 format_, int channels, int chunksize) except True:
-    if Mix_OpenAudio(frequency, format_, channels, chunksize) < 0:
-        raise SDLError()
-
-
-cdef bint mix_allocate_channels(int numchans) except True:
-    if Mix_AllocateChannels(numchans) != numchans:
-        raise SDLError()
-
-
-cdef int mix_volume(int channel, float volume) nogil:
-    return Mix_Volume(channel, int(volume * 128))
-
-
-cdef int mix_volume_music(float volume) nogil:
-    return Mix_VolumeMusic(int(volume * 128))
-
-
-cdef Music load_music(str filename):
-    music = Music()
-    path = filename.encode()
-    music.music = Mix_LoadMUS(path)
-    if music.music == NULL:
-        raise SDLError()
-    return music
-
-
-cdef Chunk load_chunk(file_):
-    cdef SDL_RWops *rwops
-    chunk = Chunk()
-    data = file_.read()
-    rwops = SDL_RWFromConstMem(<char*>data, len(data))
-    chunk.chunk = Mix_LoadWAV_RW(rwops, 1)
-    if chunk.chunk == NULL:
-        raise SDLError()
-    return chunk
-
-
 cdef Uint32 get_ticks() nogil:
     return SDL_GetTicks()
 
--- a/pytouhou/resource/loader.py
+++ b/pytouhou/resource/loader.py
@@ -18,7 +18,6 @@
 from pytouhou.formats.msg import MSG
 from pytouhou.formats.sht import SHT
 from pytouhou.formats.exe import SHT as EoSDSHT, InvalidExeException
-from pytouhou.formats.music import Track
 from pytouhou.formats.fmt import FMT
 
 from pytouhou.utils.helpers import get_logger
@@ -65,12 +64,6 @@
         logger.error("Required game exe not found!")
 
 
-    def get_track(self, name):
-        posname = name.replace('bgm/', '').replace('.mid', '.pos')
-        file = self.get_file(posname)
-        return Track.read(file) #TODO: modular
-
-
     def get_fmt(self, name):
         file = self.get_file(name)
         return FMT.read(file) #TODO: modular
--- a/pytouhou/ui/gamerunner.pyx
+++ b/pytouhou/ui/gamerunner.pyx
@@ -17,9 +17,16 @@
 from pytouhou.lib.gui cimport EXIT, PAUSE, SCREENSHOT, RESIZE, FULLSCREEN
 
 from .window cimport Window, Runner
-from .music import BGMPlayer, SFXPlayer
+from libtouhou import Audio
 from pytouhou.game.game cimport Game
-from pytouhou.game.music cimport MusicPlayer
+
+
+class DummyAudio:
+    def play(self, name):
+        pass
+
+    def play_bgm(self, track):
+        pass
 
 
 cdef class GameRunner(Runner):
@@ -27,14 +34,14 @@
     cdef Game game
     cdef Window window
     cdef list save_keystates
-    cdef bint skip
+    cdef bint skip, audio
 
     # Since we want to support multiple renderers, don’t specify its type.
     #TODO: find a way to still specify its interface.
     cdef object renderer
 
     def __init__(self, Window window, renderer, common, resource_loader,
-                 bint skip=False, con=None):
+                 bint skip=False, con=None, bint audio=True):
         self.renderer = renderer
         self.common = common
         self.resource_loader = resource_loader
@@ -43,6 +50,7 @@
         self.replay_level = None
         self.skip = skip
         self.con = con
+        self.audio = audio
 
         self.width = common.interface.width
         self.height = common.interface.height
@@ -66,14 +74,14 @@
 
         self.save_keystates = save_keystates
 
-        null_player = MusicPlayer()
+        if self.audio:
+            game.music = Audio(self.resource_loader, bgms)
+        else:
+            game.music = DummyAudio()
         if bgms is not None:
-            game.music = BGMPlayer(self.resource_loader, bgms)
-            game.music.play(0)
-        else:
-            game.music = null_player
+            game.music.play_bgm(0)
 
-        game.sfx_player = SFXPlayer(self.resource_loader) if not self.skip else null_player
+        game.sfx_player = game.music if not self.skip else DummyAudio()
 
 
     cdef bint set_input(self, replay=None) except True:
@@ -144,7 +152,7 @@
                 if self.skip:
                     self.set_input()
                     self.skip = False
-                    self.game.sfx_player = SFXPlayer(self.resource_loader)
+                    self.game.sfx_player = self.game.music
 
         if self.save_keystates is not None:
             self.save_keystates.append(keystate)
deleted file mode 100644
--- a/pytouhou/ui/music.pyx
+++ /dev/null
@@ -1,109 +0,0 @@
-# -*- encoding: utf-8 -*-
-##
-## Copyright (C) 2012 Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
-##
-## This program is free software; you can redistribute it and/or modify
-## it under the terms of the GNU General Public License as published
-## by the Free Software Foundation; version 3 only.
-##
-## This program is distributed in the hope that it will be useful,
-## but WITHOUT ANY WARRANTY; without even the implied warranty of
-## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-## GNU General Public License for more details.
-##
-
-
-from os.path import join
-from glob import glob
-from pytouhou.lib import sdl
-from pytouhou.lib.sdl cimport load_music, Music, load_chunk, Chunk
-from pytouhou.utils.helpers import get_logger
-from pytouhou.game.music cimport MusicPlayer
-
-logger = get_logger(__name__)
-
-
-cdef class BGMPlayer(MusicPlayer):
-    cdef list bgms
-
-    def __init__(self, resource_loader, bgms):
-        self.bgms = []
-        for bgm in bgms:
-            if not bgm:
-                self.bgms.append(None)
-                continue
-            posname = bgm[1].replace('bgm/', '').replace('.mid', '.pos')
-            try:
-                track = resource_loader.get_track(posname)
-            except KeyError:
-                track = None
-                logger.warning('Music description “%s” not found, continuing without looping data.', posname)
-            globname = join(resource_loader.game_dir, bgm[1]).replace('.mid', '.*')
-            filenames = glob(globname)
-            for filename in reversed(filenames):
-                try:
-                    source = load_music(filename)
-                except sdl.SDLError as error:
-                    logger.debug('Music file “%s” unreadable: %s', filename, error)
-                    continue
-                else:
-                    if track is not None:
-                        source.set_loop_points(track.start / 44100., track.end / 44100.) #TODO: retrieve the sample rate from the actual track.
-                    self.bgms.append(source)
-                    logger.debug('Music file “%s” opened.', filename)
-                    break
-            else:
-                self.bgms.append(None)
-                logger.warning('No working music file for “%s”, disabling bgm.', globname)
-
-    cpdef play(self, index):
-        cdef Music bgm
-        bgm = self.bgms[index]
-        if bgm is not None:
-            bgm.play(-1)
-
-
-cdef class SFXPlayer(MusicPlayer):
-    cdef object loader
-    cdef dict channels, sounds
-    cdef float volume
-    cdef int next_channel
-
-    def __init__(self, loader, volume=.42):
-        self.loader = loader
-        self.channels = {}
-        self.sounds = {}
-        self.volume = volume
-        self.next_channel = 0
-
-    cdef int get_channel(self, name) except -1:
-        if name not in self.channels:
-            self.channels[name] = self.next_channel
-            self.next_channel += 1
-        return self.channels[name]
-
-    cdef Chunk get_sound(self, name):
-        cdef Chunk chunk
-        if name not in self.sounds:
-            try:
-                wave_file = self.loader.get_file(name)
-                chunk = load_chunk(wave_file)
-            except (KeyError, sdl.SDLError) as error:
-                logger.warning('Sound “%s” not found: %s', name, error)
-                chunk = None
-            else:
-                chunk.set_volume(self.volume)
-            self.sounds[name] = chunk
-        return self.sounds[name]
-
-    cpdef play(self, name):
-        sound = self.get_sound(name)
-        if sound is None:
-            return
-        channel = self.get_channel(name)
-        sound.play(channel, 0)
-
-    cpdef set_volume(self, name, float volume):
-        sound = self.get_sound(name)
-        if sound is not None:
-            sound.set_volume(volume)
--- a/pytouhou/vm/msgrunner.py
+++ b/pytouhou/vm/msgrunner.py
@@ -143,7 +143,7 @@
 
     @instruction(7)
     def change_music(self, track):
-        self._game.music.play(track)
+        self._game.music.play_bgm(track)
 
 
     @instruction(8)
--- a/scripts/pytouhou
+++ b/scripts/pytouhou
@@ -200,7 +200,7 @@
     interface = Interface(resource_loader, common.players[0]) #XXX
     common.interface = interface #XXX
     renderer = GameRenderer(resource_loader, window) if GameRenderer is not None else None
-    runner = GameRunner(window, renderer, common, resource_loader, skip_replay, con)
+    runner = GameRunner(window, renderer, common, resource_loader, skip_replay, con, args.no_sound)
     window.set_runner(runner)
 
     while True:
@@ -279,7 +279,7 @@
 
 
 is_sdl = (args.frontend == 'sdl')
-with SDL(video=is_sdl, sound=args.no_sound):
+with SDL(video=is_sdl):
     window = Window(backend, Interface.width, Interface.height,
                     fps_limit=args.fps_limit, frameskip=args.frameskip)
 
--- a/setup.py
+++ b/setup.py
@@ -17,7 +17,7 @@
 
 COMMAND = 'pkg-config'
 GLFW_LIBRARIES = ['glfw3']
-SDL_LIBRARIES = ['sdl2', 'SDL2_image', 'SDL2_mixer', 'SDL2_ttf']
+SDL_LIBRARIES = ['sdl2', 'SDL2_image', 'SDL2_ttf']
 GL_LIBRARIES = ['epoxy']
 
 debug = False  # True to generate HTML annotations and display infered types.
@@ -68,7 +68,6 @@
     'glfw3': '-lglfw',
     'sdl2': '-lSDL2',
     'SDL2_image': '-lSDL2_image',
-    'SDL2_mixer': '-lSDL2_mixer',
     'SDL2_ttf': '-lSDL2_ttf',
     'epoxy': '-lepoxy'
 }