Esempio n. 1
0
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))
Esempio n. 2
0
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)