Ejemplo n.º 1
0
    def _play_cb_enter(self):
        """
        Callback for pressing enter on audio list.
        """

        logger.debug(f"State: {self.player_state}")
        return self.player_state.on_audio_list_enter_press(self)
Ejemplo n.º 2
0
    def _on_previous_track_click(self):
        """
        Callback for clicking previous button.
        """

        logger.debug(f"State: {self.player_state}")
        return self.player_state.on_previous_track_click(self)
Ejemplo n.º 3
0
    def _on_reload_click(self):
        """
        Callback for clicking reload button.
        """

        logger.debug(f"State: {self.player_state}")
        return self.player_state.on_reload_click(self)
Ejemplo n.º 4
0
    def _on_next_track_click(self):
        """
        Callback for clicking next track button.
        """

        logger.debug(f"State: {self.player_state}")
        return self.player_state.on_next_track_click(self)
Ejemplo n.º 5
0
    def _adjust_playback_right(self):
        """
        Move audio playback cursor to right
        """

        logger.debug(f"State: {self.player_state}")
        return self.player_state.adjust_playback_right(self)
Ejemplo n.º 6
0
    def play_stream(self,
                    audio_file: Union[None, pathlib.Path] = None,
                    next_=False) -> int:
        """
        Load audio and starts audio stream. Returns True if successful.

        :param audio_file: If not None, will play given index of track instead
        :param next_: Used to not refresh playlist when playing next.
        """

        if not audio_file:
            audio_file = self.selected_idx_path

        try:
            self.stream.load_stream(audio_file)
        except IndexError:
            logger.debug(
                f"Invalid idx: {audio_file} / {len(self.path_wrapper)}")
            return False

        except RuntimeError as err:
            msg = f"ERR: {str(err).split(':')[-1]}"
            logger.warning(msg)
            self.write_info(msg)
            return False

        self.current_playing_file = audio_file
        self.refresh_list(search_files=False)
        self.stream.start_stream()

        if not next_:
            self.init_playlist()

        return True
Ejemplo n.º 7
0
    def _play_cb_space_bar(self):
        """
        Callback for pressing space bar on audio list.
        """

        logger.debug(f"State: {self.player_state}")
        return self.player_state.on_audio_list_space_press(self)
Ejemplo n.º 8
0
 def start_stream(stream_manager: StreamManager):
     logger.debug("Starting Stream.")
     try:
         stream_manager.stream.start()
     except Exception as err:
         logger.critical(f"Got {type(err)}")
         raise
     else:
         stream_manager.new_state(StreamPlayingState)
Ejemplo n.º 9
0
    def init_playlist(self: AudioPlayer):
        """
        Create itertools.cycle generator that acts as a playlist
        """

        # Shuffling is harder than imagined!
        # https://engineering.atspotify.com/2014/02/28/how-to-shuffle-songs/
        copy = [i for i in self.path_wrapper.audio_file_list]
        cycle_gen = itertools.cycle(copy)

        while next(cycle_gen) != self.current_playing_file:
            pass

        self._current_play_generator = cycle_gen
        logger.debug(
            f"Initialized playlist generator from directory '{self.path_wrapper.current_path.as_posix()}'"
        )
Ejemplo n.º 10
0
    def adjust_playback_left(audio_player: AudioPlayer):
        """
        Moves audio playback cursor to the left. Moves by 5% of total playback
        """

        reference = audio_player.stream.audio_info.loaded_data

        current_frame = reference.tell()
        total_frame = reference.frames
        offset = total_frame // 20

        reference.seek(0 if offset > current_frame else (current_frame -
                                                         offset))

        logger.debug(
            f"Adjusted left from {current_frame} to {reference.tell()}")

        # Trigger playback callback to display new values
        callback = audio_player.show_progress_wrapper(paused=True)
        callback(audio_player.stream.audio_info, reference.tell())
Ejemplo n.º 11
0
    def play_next(self):
        """
        Play next track. Called by finished callback of sounddevice when conditions are met.
        """

        logger.debug(f"Stop Flag: {self.stream.stop_flag}")

        if not self.stream.stop_flag:
            next_ = self.playlist_next()

            logger.debug(f"Playing Next - {next_}")

            with self.maintain_current_view():
                if not self.play_stream(next_, True):
                    # TODO: add mark_as_error
                    # TODO: add total player length with playlist gen so it can find infinite fail loop
                    logger.warning("Error playing next track. Moving on.")
                    self.play_next()
                else:
                    # update state
                    self.player_state = AudioRunning
                    logger.debug(
                        f"Next track started, state: {self.player_state}")
                    try:
                        self.mark_as_playing(self.current_playing_idx)
                    except IndexError:
                        pass
Ejemplo n.º 12
0
    def __init__(self, audio_dir: str):
        self.audio_dir = pathlib.Path(audio_dir)
        self.loaded_data = sf.SoundFile(self.audio_dir.as_posix())

        self.total_frame = self.loaded_data.frames
        self.tag_data = TinyTag.get(self.audio_dir.as_posix())

        self.title = self.tag_data.title if self.tag_data.title else self.audio_dir.name

        # saving reference for tiny bit faster access
        try:
            self.duration_tag = round(self.tag_data.duration, 1)
        except TypeError:
            logger.warning(
                f"No tag 'duration' exists in {self.title}, calculating estimate."
            )
            self.duration_tag = round(
                self.loaded_data.frames / self.loaded_data.samplerate, 1)

        logger.debug(
            f"Audio detail - Title: {self.title}, Duration: {self.duration_tag}"
        )
Ejemplo n.º 13
0
 def load_stream(stream_manager: StreamManager, audio_dir: str):
     logger.debug("Stopping and loading new audio.")
     logger.debug("Delegating to: StreamPlayingState.stop_stream")
     StreamPlayingState.stop_stream(stream_manager)
     logger.debug("Delegating to: AudioUnloadedState.load_stream")
     AudioUnloadedState.load_stream(stream_manager, audio_dir)
Ejemplo n.º 14
0
 def pause_stream(stream_manager: StreamManager):
     logger.debug("Resuming Stream")
     stream_manager.new_state(StreamPlayingState)
     stream_manager.stream.start()
Ejemplo n.º 15
0
 def stop_stream(stream_manager: StreamManager):
     logger.debug("Delegating to: StreamPlayingState.stop_stream")
     StreamPlayingState.stop_stream(stream_manager)
Ejemplo n.º 16
0
 def pause_stream(stream_manager: StreamManager):
     logger.debug("Pausing Stream.")
     stream_manager.stream.stop()
     stream_manager.new_state(StreamPausedState)
Ejemplo n.º 17
0
 def stop_stream(stream_manager: StreamManager):
     logger.debug("Stopping Stream and resetting playback progress.")
     stream_manager.stream.stop()
     stream_manager.audio_info.loaded_data.seek(0)
Ejemplo n.º 18
0
    def callback():
        logger.debug(f"Playback finished. Stop flag: {stream_manager.stop_flag}")
        stream_manager.new_state(new_next_state)

        if not stream_manager.stop_flag:
            stream_manager.finished_cb()
Ejemplo n.º 19
0
def stream_callback_closure(stream_manager: StreamManager, raw=False) -> Callable:
    # Collecting names here to reduce call overhead.
    last_frame = -1
    dtype = sd.default.dtype[1]
    audio_ref = stream_manager.audio_info.loaded_data
    channel = audio_ref.channels
    callback = stream_manager.stream_cb
    audio_info = stream_manager.audio_info

    cycle = itertools.cycle((not n for n in range(stream_manager.callback_minimum_cycle)))

    # to reduce load, custom callback will be called every n-th iteration of this generator.
    # 3rd parameter is time but that is for internal use. Replacing with underscore.

    def stream_cb(data_out, frames: int, _, status: sd.CallbackFlags) -> None:
        nonlocal last_frame, stream_manager
        assert not status

        read = audio_ref.read(frames, fill_value=0) * stream_manager.multiplier
        try:
            data_out[:] = read
        except ValueError:
            try:
                data_out[:] = read.reshape(read.shape[0], 1)
            except Exception:
                stream_manager.stop_flag = True
                raise

        # if last_frame == (current_frame := audio_ref.tell()):
        #     raise sd.CallbackAbort

        current_frame = audio_ref.tell()

        if last_frame == current_frame:
            raise sd.CallbackAbort

        last_frame = current_frame

        if next(cycle):
            callback(audio_info, current_frame)
            # Stream callback signature for user-supplied callbacks
            # Providing current_frame and duration to reduce call overhead from user-callback side.

    def stream_cb_raw(data_out, frames: int, _, status: sd.CallbackFlags) -> None:
        nonlocal last_frame

        try:
            assert not status
        except AssertionError:
            logger.critical(str(status))
            raise

        # if (written := audio_ref.buffer_read_into(data_out, dtype)) < frames:
        #     data_out[written:] = [[0.0] * channel for _ in range(frames - written)]
        #     raise sd.CallbackStop
        #
        # if last_frame == (current_frame := audio_ref.tell()):
        #     raise sd.CallbackAbort

        written = audio_ref.buffer_read_into(data_out, dtype)

        if written < frames:
            data_out[written:] = [[0.0] * channel for _ in range(frames - written)]
            raise sd.CallbackStop

        current_frame = audio_ref.tell()

        if last_frame == current_frame:
            raise sd.CallbackAbort

        last_frame = current_frame

        if next(cycle):
            callback(audio_info, current_frame)
            # Stream callback signature for user-supplied callbacks
            # Providing current_frame and duration to reduce call overhead from user-callback side.

    logger.debug(f"Using {'Raw' if raw else 'Numpy'} callback.")
    return stream_cb_raw if raw else stream_cb
Ejemplo n.º 20
0
class PathWrapper:
    primary_formats = set("." + key.lower()
                          for key in sf.available_formats().keys())
    secondary_formats = {".m4a", ".mp3"} if PY_DUB_ENABLED else set()
    supported_formats = primary_formats | secondary_formats
    supported_formats = supported_formats | set(key.upper()
                                                for key in supported_formats)

    # re_match_pattern = "$|".join(final_supported_formats)
    # subtypes = soundfile.available_subtypes()
    logger.debug(f"Available formats: {supported_formats}")

    # Considering idx 0 to always be step out!

    def __init__(self, path: str = "./"):
        self.current_path = pathlib.Path(path).absolute()
        self.audio_file_list: List[pathlib.Path] = []
        self.folder_list: List[pathlib.Path] = []

    def list_audio(self) -> Generator[pathlib.Path, None, None]:
        yield from (path_obj for path_obj in self.list_file()
                    if path_obj.suffix in self.supported_formats)

    def list_folder(self) -> Generator[pathlib.Path, None, None]:
        """
        First element will be current folder location. either use next() or list()[1:] to skip it.
        """

        yield self.current_path.parent
        yield from (path_ for path_ in self.current_path.iterdir()
                    if path_.is_dir())

    def list_file(self) -> Generator[pathlib.Path, None, None]:
        """
        Can't use glob as it match folders such as .git, using pathlib.Path object instead.
        """

        yield from (item for item in self.current_path.iterdir()
                    if item.is_file())

    def step_in(self, directory_idx: int):
        """
        Relative / Absolute paths supported.
        """

        try:
            self.current_path = self.folder_list[directory_idx]
        except IndexError as err:
            raise NotADirectoryError(
                f"Directory index {directory_idx} does not exist!") from err

        self.refresh_list()
        return self.current_path

    def step_out(self):

        if self.current_path == self.current_path.parent:
            return self.current_path

        self.current_path = self.current_path.parent

        self.refresh_list()
        return self.current_path

    def refresh_list(self):
        self.audio_file_list.clear()
        self.folder_list.clear()

        self.audio_file_list.extend(self.list_audio())
        self.folder_list.extend(self.list_folder())

    def fetch_meta(self):
        # This might have to deal the cases such as path changing before generator fires up.
        for file_dir in self.list_audio():
            yield TinyTag.get(file_dir)

    def fetch_tag_data(self):
        for file_dir in self.list_audio():
            yield TinyTag.get(file_dir)

    def __len__(self):
        return len(self.folder_list) + len(self.audio_file_list)

    def __getitem__(self, item: int):
        # logger.debug(f"idx: {item}, len_f: {len(self.folder_list)}, len_a: {len(self.audio_file_list)}")
        try:
            return self.folder_list[item]
        except IndexError as err:
            if len(self) == 0:
                raise IndexError(
                    "No file or folder to index in current directory."
                ) from err

            return self.audio_file_list[item - len(self.folder_list)]

    def index(self, target: Union[str, pathlib.Path]):
        path_ = pathlib.Path(target)
        try:
            return len(self.folder_list) + self.audio_file_list.index(path_)
        except ValueError as err:
            raise IndexError(
                f"Cannot find given target '{path_.as_posix()}'!") from err
Ejemplo n.º 21
0
 def load_stream(stream_manager: StreamManager, audio_dir: str):
     logger.debug("Loading new file.")
     logger.debug("Delegating to: StreamPlayingState.load_stream")
     AudioUnloadedState.load_stream(stream_manager, audio_dir)
Ejemplo n.º 22
0
 def __del__(self):
     self.loaded_data.close()
     logger.debug(f"Dropping loaded file <{self.title}>")
Ejemplo n.º 23
0
from __future__ import annotations
from sys import platform

import py_cui
from LoggingConfigurator import logger
from Player.PlayerLogic import AudioPlayer

try:
    # noinspection PyUnresolvedReferences
    import pretty_errors
    pretty_errors.activate()
except ImportError:
    pass

VERSION_TAG = "0.0.4a - dev"
logger.debug(f"Platform: {platform} Version: {VERSION_TAG}")

# ------------------------------------------------------------------


def draw_player():
    """
    TUI driver
    """

    root = py_cui.PyCUI(5, 7, auto_focus_buttons=True)
    root.set_title(f"CUI Audio Player - v{VERSION_TAG}")
    root.set_widget_border_characters("╔", "╗", "╚", "╝", "═", "║")
    root.set_refresh_timeout(0.1)
    # this don't have to be a second. Might be an example of downside of ABC
Ejemplo n.º 24
0
 def new_state(self, status: Type[StreamState]):
     logger.debug(f"Switching state: {self.stream_state} -> {status}")
     self.stream_state = status