Skip to content

Commit

Permalink
Merge pull request #30 from 4br3mm0rd/v2.0
Browse files Browse the repository at this point in the history
V2.0 - mpg321 and mpg123 code separation
  • Loading branch information
4br3mm0rd authored Apr 18, 2022
2 parents 3129a0a + 2ce3f05 commit bf444b6
Show file tree
Hide file tree
Showing 10 changed files with 509 additions and 439 deletions.
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,28 @@ mpyg321 is a simple python wrapper for mpg321 and mpg123. It allows you to easil

# Installation

mpyg321 requires the installation of mpg321 (or mpg123 depending on your usage) software for reading mp3. This section describes the installation of the library on MacOS, Linux and Windows. **For now, the library has only been tested on mac, but it should work on any platform.**
mpyg321 requires the installation of mpg123 (or mpg321 depending on your usage) software for reading mp3. This section describes the installation of the library on MacOS, Linux and Windows. **For now, the library has only been tested on mac, but it should work on any platform.**

We recommend using mpg123 since the project is more up to date. However, you can also use this library with mpg321 (using the `MPyg321Player` class)

## MacOS

```
$ brew install mpg321
$ brew install mpg123 # or mpg321
$ pip3 install mpyg321
```

## Linux

```
$ sudo apt-get install mpg321
$ sudo apt-get update
$ sudo apt-get install mpg123 # or mpg321
$ pip3 install mpyg321
```

## Windows

For windows installation, download mpg321 on the website: [mpg321's website](https://www.mpg123.de/download.shtml). Make sure to rename the command to mpg321, and then run:
For windows installation, download mpg123 on the website: [mpg123's website](https://www.mpg123.de/download.shtml), and then run:

```
$ pip install mpyg321
Expand All @@ -34,21 +37,33 @@ $ pip install mpyg321
# Usage

Usage is pretty straight forward, and all the functionnalities are easily shown in the examples folder.

```
from mpyg321.mpyg321 import MPyg321Player
player = MPyg321Player()
from mpyg321.MPyg123Player import MPyg123Player # or MPyg321Player if you installed mpg321
player = MPyg123Player()
player.play_song("/path/to/some_mp3.mp3")
```

## Calbacks

You can implement callbacks for several events such as: end of song, user paused the music, ...
All the callbacks can be found inside the code of the `BasePlayer` class and the `MPyg123Player` class.
Most of the callbacks are implemented in the `callbacks.py` example file.

## Loops

In order to loop (replay the song when it ended), you can either set the loop mode when calling the `play_song` function:

```
player.play_song("/path/to/sample.mp3", loop=True)
```

or programmatically set the loop mode anywhere in the code:

```
player.play_song("/path/to/sample.mp3)
// Do some stuff ...
player.set_loop(True)
```
```

**Note:** when calling `player.set_loop(True)`, the loop mode will only be taken into account at the end of a song. If nothing is playing, this call will not replay the previous song. In order to replay the previous song, you should call: `player.play()`
5 changes: 4 additions & 1 deletion examples/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@
MPyG321 basic example
Playing and pausing some music
You need to add a "sample.mp3" file in the working directory
In this example, you can replace MPyg321Player by MPyg123Player
according to the player you installed on your machine (mpg321/mpg123)
"""
from mpyg321.mpyg321 import MPyg321Player
from mpyg321.MPyg321Player import MPyg321Player
from time import sleep


Expand Down
25 changes: 22 additions & 3 deletions examples/callbacks.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"""
MPyG321 callbacks example
MPyg321 callbacks example
Playing and pausing some music, triggering callbacks
You need to add a "sample.mp3" file in the working directory
In this example, you can replace MPyg321Player by MPyg123Player
according to the player you installed on your machine (mpg321/mpg123)
"""
from mpyg321.mpyg321 import MPyg321Player
from mpyg321.MPyg123Player import MPyg123Player

from time import sleep


class MyPlayer(MPyg321Player):
class MyPlayer(MPyg123Player):
"""We create a class extending the basic player to implement callbacks"""

def on_any_stop(self):
Expand All @@ -30,6 +34,14 @@ def on_music_end(self):
"""Callback when music ends"""
print("The music has ended")

def on_user_mute(self):
"""Callback when music is muted"""
print("The music has been muted (continues playing)")

def on_user_unmute(self):
"""Callback when music is unmuted"""
print("Music has been unmuted")


def do_some_play_pause(player):
"""Does some play and pause"""
Expand All @@ -40,6 +52,13 @@ def do_some_play_pause(player):
player.resume()
sleep(5)
player.stop()
sleep(2)
player.play()
sleep(2)
player.mute()
sleep(1)
player.unmute()
sleep(20)
player.quit()


Expand Down
210 changes: 210 additions & 0 deletions mpyg321/BasePlayer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
"""
Mpyg BasePlayer base class
This class contains all the functions that are common
both to mpg321 and mpg123.
All the players implement this base class and add their
specific feature.
"""
import pexpect
import subprocess
from threading import Thread
from .MpygError import *
from .consts import *


class BasePlayer:
"""Base class for players"""
player = None
status = None
output_processor = None
song_path = ""
loop = False
performance_mode = True
suitable_versions = [] # mpg123 and/or mpg321 - set inside subclass
default_player = None # mpg123 or mpg321 - set inside subclass
player_version = None # defined inside check_player
mpg_outs = []

def __init__(self, player=None, audiodevice=None, performance_mode=True):
"""Builds the player and creates the callbacks"""
self.set_player(player, audiodevice)
self.output_processor = Thread(target=self.process_output)
self.output_processor.daemon = True
self.performance_mode = performance_mode
self.output_processor.start()

def check_player(self, player):
"""Gets the player"""
try:
cmd = str(player)
output = subprocess.check_output([cmd, "--version"])
for version in self.suitable_versions:
if version in str(output):
self.player_version = version
if self.player_version is None:
raise MPygPlayerNotFoundError(
"""No suitable player found: you might be using the wrong \
player (Mpyg321Player or Mpyg123Player)""")
except subprocess.SubprocessError:
raise MPygPlayerNotFoundError(
"""No suitable player found: you might need to install
mpg123""")

def set_player(self, player, audiodevice):
"""Sets the player"""
if player is None:
player = self.default_player
self.check_player(player)
args = " --audiodevice " + audiodevice if audiodevice else ""
args += "-R mpyg"
self.player = pexpect.spawn(str(player) + " " + args)
self.player.delaybeforesend = None
self.status = PlayerStatus.INSTANCIATED
# Setting extended mpg_outs for version specific behaviors
self.mpg_outs = mpg_outs.copy()
self.mpg_outs.extend(mpg_outs_ext[self.player_version])

def process_output(self):
"""Parses the output"""
mpg_codes = [v["mpg_code"] for v in self.mpg_outs]
while True:
index = self.player.expect(mpg_codes)
action = self.mpg_outs[index]["action"]
if action == "music_stop":
self.on_music_stop_int()
elif action == "user_pause":
self.on_user_pause_int()
elif action == "user_start_or_resume":
self.on_user_start_or_resume_int()
elif action == "end_of_song":
self.on_end_of_song_int()
elif action == "error":
self.on_error()
else:
self.process_output_ext(action)

def process_output_ext(self, action):
"""Processes the output for version specific behavior"""
pass

def play_song(self, path, loop=False):
"""Plays the song"""
self.loop = loop
self.set_song(path)
self.play()

def play(self):
"""Starts playing the song"""
self.player.sendline("LOAD " + self.song_path)
self.status = PlayerStatus.PLAYING

def pause(self):
"""Pauses the player"""
if self.status == PlayerStatus.PLAYING:
self.player.sendline("PAUSE")
self.status = PlayerStatus.PAUSED

def resume(self):
"""Resume the player"""
if self.status == PlayerStatus.PAUSED:
self.player.sendline("PAUSE")
self.on_user_resume()

def stop(self):
"""Stops the player"""
self.player.sendline("STOP")
self.status = PlayerStatus.STOPPING

def quit(self):
"""Quits the player"""
self.player.sendline("QUIT")
self.status = PlayerStatus.QUITTED

def jump(self, pos):
"""Jump to position"""
self.player.sendline("JUMP " + str(pos))

def on_error(self):
"""Process errors encountered by the player"""
output = self.player.readline().decode("utf-8")

# Check error in list of errors
for mpg_error in mpg_errors:
if mpg_error["message"] in output:
action = mpg_error["action"]
if action == "generic_error":
raise MPygError(output)
if action == "file_error":
raise MPygFileError(output)
if action == "command_error":
raise MPygCommandError(output)
if action == "argument_error":
raise MPygArgumentError(output)
if action == "eq_error":
raise MPygEQError
if action == "seek_error":
raise MPygSeekError

# Some other error occurred
raise MPygError(output)

def set_song(self, path):
"""song_path setter"""
self.song_path = path

def set_loop(self, loop):
""""loop setter"""
self.loop = loop

# # # Internal Callbacks # # #
def on_music_stop_int(self):
"""Internal callback when the music is stopped"""
if self.status == PlayerStatus.STOPPING:
self.on_user_stop_int()
self.status = PlayerStatus.STOPPED
else:
self.on_end_of_song_int()

def on_user_stop_int(self):
"""Internal callback when the user stops the music."""
self.on_any_stop()
self.on_user_stop()

def on_user_pause_int(self):
"""Internal callback when user pauses the music"""
self.on_any_stop()
self.on_user_pause()

def on_user_start_or_resume_int(self):
"""Internal callback when user resumes the music"""
self.status = PlayerStatus.PLAYING

def on_end_of_song_int(self):
"""Internal callback when the song ends"""
if(self.loop):
self.play()
else:
# The music doesn't stop if it is looped
self.on_any_stop()
self.on_music_end()

# # # Public Callbacks # # #
def on_any_stop(self):
"""Callback when the music stops for any reason"""
pass

def on_user_pause(self):
"""Callback when user pauses the music"""
pass

def on_user_resume(self):
"""Callback when user resumes the music"""
pass

def on_user_stop(self):
"""Callback when user stops music"""
pass

def on_music_end(self):
"""Callback when music ends"""
pass
54 changes: 54 additions & 0 deletions mpyg321/MPyg123Player.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from .BasePlayer import BasePlayer
from .consts import PlayerStatus


class MPyg123Player(BasePlayer):
"""Player for mpg123"""
def __init__(self, player=None, audiodevice=None, performance_mode=True):
self.suitable_versions = ["mpg123"]
self.default_player = "mpg123"
super().__init__(player, audiodevice, performance_mode)
if performance_mode:
self.silence_mpyg_output()

def process_output_ext(self, action):
"""Processes specific output for mpg123 player"""
if action == "user_mute":
self.on_user_mute()
elif action == "user_unmute":
self.on_user_unmute()

def load_list(self, entry, filepath):
"""Load an entry in a list
Parameters:
entry (int): index of the song in the list - first is 0
filepath: URL/Path to the list
"""
if self.player_version == "mpg123":
self.player.sendline("LOADLIST {} {}".format(entry, filepath))
self.status = PlayerStatus.PLAYING

def silence_mpyg_output(self):
"""Improves performance by silencing the mpg123 process frame output"""
self.player.sendline("SILENCE")

def mute(self):
"""Mutes the player"""
self.player.sendline("MUTE")

def unmute(self):
"""Unmutes the player"""
self.player.sendline("UNMUTE")

def volume(self, percent):
"""Adjust player's volume"""
self.player.sendline("VOLUME {}".format(percent))

# # # Public Callbacks # # #
def on_user_mute(self):
"""Callback when user mutes player"""
pass

def on_user_unmute(self):
"""Callback when user unmutes player"""
pass
Loading

0 comments on commit bf444b6

Please sign in to comment.