class Sound: """Loads a sound from the specified URL. Supports whatever audio formats that PySide2 supports (depending on locally-installed codecs). No error is raised if the file isn't found or is of an unsupported format. Sounds are tracked :param url: the URL to load the sound from, can be a local file """ def __init__(self, url): # type: (str) -> None self.__url = url # Only used for debugging # Tell the app not to quit while this sound is loading, since it is plausible that a user is using sounds # without using a frame or any timers: TheApp.add_tracked(self) self.__player = QMediaPlayer(TheApp, flags=QMediaPlayer.LowLatency) self.__player.setAudioRole(QAudio.GameRole) self.__player.mediaStatusChanged.connect(self.__on_status_changed) self.__player.error.connect(self.__on_status_changed) req = request(url) content = QMediaContent(req) self.__player.setMedia(content) self.__sound_loaded = False self.__play_requested = False def __on_status_changed(self, _): # type: (QMediaPlayer.MediaStatus) -> None """Checks if the sound is loaded. If the sound has loaded without error, and the user has already told it to start playing, this will start the sound playing. If there was any failure in loading the sound, this is recorded, and the sound will never be able to be played. TheApp will also be notified that it can close if this sound is all it is waiting for. This event is also triggered by other status changes. The only other one that matters is the EndOfMedia status. In this case, TheApp is also told that it can close. :param _: (unused) media status object """ error = self.__player.error() status = self.__player.mediaStatus() if status < QMediaPlayer.LoadedMedia: return if error == QMediaPlayer.NoError and QMediaPlayer.LoadedMedia <= status < QMediaPlayer.InvalidMedia: # Check if the media is actually an audio file that is playable if self.__player.isAudioAvailable(): self.__sound_loaded = True if self.__play_requested and status != QMediaPlayer.EndOfMedia: # Play and don't do anything else now self.play() return else: self.__sound_loaded = False self.__play_requested = False TheApp.remove_tracked(self) def play(self): """Starts playing a sound, or restarts playing it at the point it was paused.""" if self.__sound_loaded: self.__player.play() TheApp.add_tracked(self) self.__play_requested = True def pause(self): """Stops the playing of the sound. Playing can be restarted at the stopped point with :meth:`play`.""" if self.__sound_loaded: self.__player.pause() TheApp.remove_tracked(self) self.__play_requested = False def rewind(self): """Stops playing the sound, makes it so the next :meth:`play` will start playing the sound at the beginning.""" if self.__sound_loaded: self.__player.stop() TheApp.remove_tracked(self) self.__play_requested = False def set_volume(self, volume): # type: (float) -> None """Changes the volume for the sound to be the given level on a 0 (silent) – 1.0 (maximum) scale. Default is 1. :param volume: the volume to set """ assert 0.0 <= volume <= 1.0, "volume must be given in range 0-1 inclusive" self.__player.setVolume(int(100 * volume))
class Channel(QObject): def __init__(self, name: T.Optional[str], parent: T.Optional[QObject] = None) -> None: super().__init__(parent) self.name = name self.slider_volume: Number = 100 self.threshold = PlaybackThreshold.Everything self._loop_sound: T.Optional[Sound] = None self._loop_player = QMediaPlayer(self) self._loop_volume_adjustment: Number = 0 self._loop_player.setAudioRole(QAudio.GameRole) self._loop_playlist = QMediaPlaylist(self) self._loop_playlist.setPlaybackMode(QMediaPlaylist.CurrentItemOnce) self._loop_player.setPlaylist(self._loop_playlist) self._loop_player.stateChanged.connect( self._on_loop_player_state_changed) self._one_shot_player = QMediaPlayer(self) self._one_shot_volume_adjustment: Number = 0 self._one_shot_player.setAudioRole(QAudio.GameRole) self._one_shot_player.stateChanged.connect( self._on_one_shot_player_state_changed) @property def is_playing(self): return (self._loop_player.state() == QMediaPlayer.PlayingState or self._one_shot_player.state() == QMediaPlayer.PlayingState) def _on_loop_player_state_changed(self, state: QMediaPlayer.State) -> None: logger.trace("Loop player state changed: {!r}", state) if state != QMediaPlayer.StoppedState: return decibel = -self._loop_volume_adjustment logger.trace("Readjusting loop player volume by {}db", decibel) self.adjust_player_volume_by_decibel(self._loop_player, decibel) self._loop_volume_adjustment = 0 # Loop playlist is empty if not self._loop_playlist.mediaCount(): logger.trace("Loop playlist is empty, not queueing a new file") return # This shouldn't ever happen, it's just here to make mypy happy if not self._loop_sound: return file = random.choices(self._loop_sound.files, [file.weight for file in self._loop_sound.files])[0] index = self._loop_sound.files.index(file) logger.trace( "Loop player playing file: {!r} at playlist index: {}", file, index, ) self._loop_playlist.setCurrentIndex(index) self._loop_player.play() def _on_one_shot_player_state_changed(self, state: QMediaPlayer.State) -> None: logger.trace("One-shot player state changed: {!r}", state) if state != QMediaPlayer.StoppedState: return decibel = -self._one_shot_volume_adjustment logger.trace("Readjusting one-shot player volume by {}db", decibel) self.adjust_player_volume_by_decibel(self._one_shot_player, decibel) self._one_shot_volume_adjustment = 0 logger.trace("One-shot player stopped, resuming loop player") self._loop_player.play() def play_sound(self, sound: Sound) -> None: if sound.playback_threshold > self.threshold: logger.trace("Ignoring sound {!r} because of threshold", sound) return if sound.loop is Loop.Start: self._loop_sound = sound # New looping sound, rebuild playlist self._loop_playlist.clear() for file in sound.files: media = QUrl.fromLocalFile(file.file_name) self._loop_playlist.addMedia(media) # Select file based on weight and set the matching playlist index weights = [file.weight for file in sound.files] file = random.choices(sound.files, weights)[0] index = sound.files.index(file) self._loop_playlist.setCurrentIndex(index) logger.trace("Adjusting loop player volume by {}db", file.volume_adjustment) self._loop_volume_adjustment = self.adjust_player_volume_by_decibel( self._loop_player, file.volume_adjustment) logger.trace("Adjusted One-shot player volume by {}db", self._loop_volume_adjustment) self._loop_player.play() logger.trace( "Loop player playing file: {!r} at playlist index: {}", file, index, ) return if sound.loop is Loop.Stop: logger.trace("Stopping loop player") self._loop_sound = None self._loop_playlist.clear() self._loop_player.stop() else: logger.trace("Pausing loop player for one-shot sound") self._loop_player.pause() file = random.choices(sound.files, [file.weight for file in sound.files])[0] media = QUrl.fromLocalFile(file.file_name) self._one_shot_player.setMedia(media) self._one_shot_volume_adjustment = self.adjust_player_volume_by_decibel( self._one_shot_player, file.volume_adjustment) logger.trace("Adjusted one-shot player volume by {}db", self._one_shot_volume_adjustment) self._one_shot_player.play() logger.trace("One-shot player playing file: {!r}", file) def set_player_volumes(self, volume: Number) -> None: volume = round(volume) self._loop_player.setVolume(volume) self._one_shot_player.setVolume(volume) # noinspection PyMethodMayBeStatic def adjust_player_volume_by_decibel(self, player: QMediaPlayer, decibel: Number) -> Number: original_volume = player.volume() target_volume = round( add_decibel_to_linear_volume(original_volume, decibel)) player.setVolume(target_volume) # Return clamped volume difference, so increasing linear volume 100 by n > 1 db # returns 0 return player.volume() - original_volume def set_threshold(self, threshold: PlaybackThreshold) -> None: logger.trace("Setting channel threshold: {!r}", threshold) self.threshold = threshold if not self._loop_sound: return if self._loop_sound.playback_threshold > threshold: logger.trace("Stopping loop player, new threshold too low") self._loop_playlist.clear() self._loop_player.stop() return logger.trace("Loop player state: {!r}", self._loop_player.state()) if (self._loop_sound.playback_threshold <= threshold and self._loop_player.state() == QMediaPlayer.StoppedState): logger.trace( "Replaying sound: {!r} in loop player from stopped state") self.play_sound(self._loop_sound)