├── tests ├── bensound-epic.mp3 ├── bensound-scifi.mp3 └── test_mpg123.py ├── examples ├── simple_player.py ├── mp3_to_wave.py ├── radio_rock.py ├── radio_aiohttp.py ├── pyaudio_player.py └── death_fm_asyncio.py ├── setup.py ├── LICENSE ├── README.md ├── .gitignore └── mpg123.py /tests/bensound-epic.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/20tab/mpg123-python/HEAD/tests/bensound-epic.mp3 -------------------------------------------------------------------------------- /tests/bensound-scifi.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/20tab/mpg123-python/HEAD/tests/bensound-scifi.mp3 -------------------------------------------------------------------------------- /examples/simple_player.py: -------------------------------------------------------------------------------- 1 | from mpg123 import Mpg123, Out123 2 | 3 | mp3 = Mpg123('tests/bensound-scifi.mp3') 4 | 5 | out = Out123() 6 | 7 | for frame in mp3.iter_frames(out.start): 8 | out.play(frame) 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from distutils.core import setup 4 | 5 | setup(name='mpg123', 6 | version='0.4', 7 | description='Python wrapper for libmpg123', 8 | author='20Tab S.r.l.', 9 | author_email='info@20tab.com', 10 | url='https://github.com/20tab/mpg123-python', 11 | py_modules=['mpg123']) 12 | -------------------------------------------------------------------------------- /examples/mp3_to_wave.py: -------------------------------------------------------------------------------- 1 | from mpg123 import Mpg123 2 | import wave 3 | 4 | mp3 = Mpg123('tests/bensound-epic.mp3') 5 | rate, channels, encoding = mp3.get_format() 6 | 7 | wav = wave.open('bensound-epic.wav', 'wb') 8 | wav.setnchannels(channels) 9 | wav.setframerate(rate) 10 | wav.setsampwidth(mp3.get_width_by_encoding(encoding)) 11 | 12 | for frame in mp3.iter_frames(): 13 | wav.writeframes(frame) 14 | 15 | wav.close() 16 | -------------------------------------------------------------------------------- /examples/radio_rock.py: -------------------------------------------------------------------------------- 1 | from mpg123 import Mpg123, Out123 2 | try: 3 | # old python2 4 | from urllib2 import urlopen 5 | except ImportError: 6 | from urllib.request import urlopen 7 | 8 | mp3 = Mpg123() 9 | 10 | out = Out123() 11 | 12 | response = urlopen('http://178.32.136.160:8050') 13 | 14 | while True: 15 | chunk = response.read(4096) 16 | if not chunk: 17 | break 18 | mp3.feed(chunk) 19 | for frame in mp3.iter_frames(out.start): 20 | out.play(frame) 21 | -------------------------------------------------------------------------------- /examples/radio_aiohttp.py: -------------------------------------------------------------------------------- 1 | from mpg123 import Mpg123, Out123 2 | import asyncio 3 | import aiohttp 4 | 5 | mp3 = Mpg123() 6 | 7 | out = Out123() 8 | 9 | async def radio_streaming(mp3, out): 10 | async with aiohttp.ClientSession() as session: 11 | async with session.get('http://stream.radiometal.com:8010') as response: 12 | while True: 13 | chunk = await response.content.read(4096) 14 | if not chunk: 15 | break 16 | mp3.feed(chunk) 17 | for frame in mp3.iter_frames(out.start): 18 | out.play(frame) 19 | 20 | loop = asyncio.get_event_loop() 21 | loop.run_until_complete(radio_streaming(mp3, out)) 22 | -------------------------------------------------------------------------------- /examples/pyaudio_player.py: -------------------------------------------------------------------------------- 1 | import mpg123 2 | import pyaudio 3 | 4 | mp3 = mpg123.Mpg123('tests/bensound-epic.mp3') 5 | 6 | rate, channels, encoding = mp3.get_format() 7 | 8 | p = pyaudio.PyAudio() 9 | 10 | # conversion table between mpg123 and pyaudio 11 | ENCODINGS_TABLE = { 12 | mpg123.ENC_SIGNED_8: pyaudio.paInt8, 13 | mpg123.ENC_SIGNED_16: pyaudio.paInt16, 14 | mpg123.ENC_SIGNED_24: pyaudio.paInt24, 15 | mpg123.ENC_SIGNED_32: pyaudio.paInt32, 16 | mpg123.ENC_FLOAT_32: pyaudio.paFloat32, 17 | mpg123.ENC_UNSIGNED_8: pyaudio.paUInt8, 18 | } 19 | 20 | stream = p.open(channels=channels, format=pyaudio.paInt16, rate=rate, output=True) 21 | 22 | for frame in mp3.iter_frames(): 23 | stream.write(frame) 24 | 25 | p.terminate() 26 | -------------------------------------------------------------------------------- /examples/death_fm_asyncio.py: -------------------------------------------------------------------------------- 1 | from mpg123 import Mpg123, Out123 2 | import asyncio 3 | 4 | """ 5 | This is a really special test as we check if the mp3 parser is robust 6 | enough to survive a blind-tcp session instead of http full parsing 7 | """ 8 | 9 | mp3 = Mpg123() 10 | 11 | out = Out123() 12 | 13 | async def radio_streaming(mp3, out): 14 | reader, writer = await asyncio.open_connection('hi5.death.fm', 80) 15 | writer.write('GET / HTTP/1.0\r\n\r\n'.encode()) 16 | 17 | while True: 18 | chunk = await reader.read(4096) 19 | if not chunk: 20 | break 21 | mp3.feed(chunk) 22 | for frame in mp3.iter_frames(out.start): 23 | out.play(frame) 24 | 25 | loop = asyncio.get_event_loop() 26 | loop.run_until_complete(radio_streaming(mp3, out)) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 20tab srl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mpg123-python 2 | mpg123 python wrapper using ctypes 3 | 4 | mpg123 libraries are available on the official site: https://www.mpg123.de/ 5 | 6 | CPython 2 and 3 are supported as well as PyPy 7 | 8 | ## Quick intro 9 | 10 | this is a simple mp3 player using the first available sound device as the output (libout123 is part of the mpg123 distribution) 11 | 12 | ```python 13 | from mpg123 import Mpg123, Out123 14 | 15 | # load an mp3 file 16 | mp3 = Mpg123('tests/bensound-scifi.mp3') 17 | 18 | # use libout123 to access the sound device 19 | out = Out123() 20 | 21 | # decode mp3 frames and send them to the sound device 22 | for frame in mp3.iter_frames(out.start): 23 | out.play(frame) 24 | ``` 25 | 26 | this is another example showing how to encode an mp3 to a wave file: 27 | 28 | ```python 29 | from mpg123 import Mpg123 30 | import wave 31 | 32 | mp3 = Mpg123('tests/bensound-epic.mp3') 33 | 34 | # get informations about the file 35 | rate, channels, encoding = mp3.get_format() 36 | 37 | # prepare the wave header 38 | wav = wave.open('bensound-epic.wav', 'wb') 39 | wav.setnchannels(channels) 40 | wav.setframerate(rate) 41 | wav.setsampwidth(mp3.get_width_by_encoding(encoding)) 42 | 43 | # fill the wave file 44 | for frame in mp3.iter_frames(): 45 | wav.writeframes(frame) 46 | 47 | wav.close() 48 | ``` 49 | 50 | 51 | more examples, included streaming of webradio are available here: https://github.com/20tab/mpg123-python/tree/master/examples 52 | 53 | ## Test suite 54 | 55 | The example files are "Royalty Free Music from Bensound" (http://www.bensound.com). For all of the test files a ID3v1 record has been added 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | *.un~ 104 | *.swp 105 | MANIFEST 106 | -------------------------------------------------------------------------------- /tests/test_mpg123.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import mpg123 3 | from mpg123 import Mpg123 4 | import sys 5 | 6 | 7 | class TestMpg123(unittest.TestCase): 8 | 9 | def test_feeding_need_more(self): 10 | mp3 = Mpg123() 11 | mp3.feed(b'') 12 | with self.assertRaises(Mpg123.NeedMoreException): 13 | mp3.decode_frame() 14 | 15 | def test_feeding_need_more_not_empty(self): 16 | mp3 = Mpg123() 17 | mp3.feed(b'\0\0\0\0') 18 | with self.assertRaises(Mpg123.NeedMoreException): 19 | mp3.decode_frame() 20 | 21 | def test_get_format_need_more(self): 22 | mp3 = Mpg123() 23 | with self.assertRaises(Mpg123.NeedMoreException): 24 | mp3.get_format() 25 | 26 | def test_feeding_need_more_bytearray(self): 27 | mp3 = Mpg123() 28 | mp3.feed(bytearray(8)) 29 | with self.assertRaises(Mpg123.NeedMoreException): 30 | mp3.decode_frame() 31 | 32 | def test_feeding_need_more_string(self): 33 | mp3 = Mpg123() 34 | mp3.feed('hello') 35 | with self.assertRaises(Mpg123.NeedMoreException): 36 | mp3.decode_frame() 37 | 38 | def test_feeding_file(self): 39 | mp3 = Mpg123() 40 | with open('tests/bensound-scifi.mp3', 'rb') as f: 41 | while True: 42 | data = f.read(4096) 43 | if not data: 44 | break 45 | mp3.feed(data) 46 | rate, channels, encoding = mp3.get_format() 47 | self.assertEqual(rate, 44100) 48 | self.assertEqual(channels, 2) 49 | self.assertEqual(encoding, 208) 50 | self.assertEqual(mp3.get_width_by_encoding(encoding), 2) 51 | self.assertEqual(encoding, mpg123.ENC_SIGNED_16) 52 | frame = mp3.decode_frame() 53 | self.assertEqual(len(frame), 188) 54 | frame = mp3.decode_frame() 55 | self.assertEqual(len(frame), 4608) 56 | 57 | def test_file_format(self): 58 | mp3 = Mpg123('tests/bensound-epic.mp3') 59 | rate, channels, encoding = mp3.get_format() 60 | self.assertEqual(rate, 44100) 61 | self.assertEqual(channels, 2) 62 | self.assertEqual(encoding, 208) 63 | 64 | def test_file_format2(self): 65 | mp3 = Mpg123('tests/bensound-scifi.mp3') 66 | rate, channels, encoding = mp3.get_format() 67 | self.assertEqual(rate, 44100) 68 | self.assertEqual(channels, 2) 69 | self.assertEqual(encoding, 208) 70 | 71 | def test_file_frame(self): 72 | mp3 = Mpg123('tests/bensound-epic.mp3') 73 | frame = mp3.decode_frame() 74 | self.assertEqual(len(frame), 188) 75 | frame = mp3.decode_frame() 76 | self.assertEqual(len(frame), 4608) 77 | 78 | def test_file_id3(self): 79 | mp3 = Mpg123('tests/bensound-epic.mp3') 80 | id3 = mp3.get_id3() 81 | self.assertEqual(id3.artist, 'Bensound'.encode()) 82 | self.assertEqual(id3.year, '2017'.encode()) 83 | self.assertEqual(id3.genre, 0) 84 | 85 | def test_file_id3_song2(self): 86 | mp3 = Mpg123('tests/bensound-scifi.mp3') 87 | id3 = mp3.get_id3() 88 | self.assertEqual(id3.artist, 'http://www.bensound.com'.encode()) 89 | self.assertEqual(id3.year, '2012'.encode()) 90 | self.assertEqual(id3.genre, 1) 91 | 92 | def test_file_all_frames(self): 93 | mp3 = Mpg123('tests/bensound-epic.mp3') 94 | frames = [frame for frame in mp3.iter_frames()] 95 | self.assertEqual(len(frames), 6835) 96 | 97 | def test_file_frame_data(self): 98 | mp3 = Mpg123('tests/bensound-epic.mp3') 99 | frames = [frame for frame in mp3.iter_frames()] 100 | if sys.version_info[0] >= 3: 101 | self.assertEqual(frames[17][22], 30) 102 | else: 103 | self.assertEqual(ord(frames[17][22]), 30) 104 | 105 | def test_file_length(self): 106 | mp3 = Mpg123('tests/bensound-epic.mp3') 107 | self.assertEqual(mp3.length(), 7872625) 108 | 109 | def test_file_frame_length(self): 110 | mp3 = Mpg123('tests/bensound-epic.mp3') 111 | self.assertEqual(mp3.frame_length(), 6835) 112 | 113 | def test_feed_frame_length(self): 114 | mp3 = Mpg123() 115 | with self.assertRaises(Mpg123.NeedMoreException): 116 | mp3.frame_length() 117 | 118 | def test_feed_length(self): 119 | mp3 = Mpg123() 120 | with self.assertRaises(Mpg123.NeedMoreException): 121 | mp3.length() 122 | 123 | if __name__ == '__main__': 124 | unittest.main() 125 | -------------------------------------------------------------------------------- /mpg123.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | from ctypes.util import find_library 3 | import sys 4 | 5 | VERBOSE = 0 6 | 7 | OK = 0 8 | NEED_MORE = -10 9 | NEW_FORMAT = -11 10 | DONE = -12 11 | 12 | MONO = 1 13 | STEREO = 2 14 | 15 | ENC_8 = 0x00f 16 | ENC_16 = 0x040 17 | ENC_24 = 0x4000 18 | ENC_32 = 0x100 19 | ENC_SIGNED = 0x080 20 | ENC_FLOAT = 0xe00 21 | ENC_SIGNED_16 = (ENC_16 | ENC_SIGNED | 0x10) 22 | ENC_UNSIGNED_16 = (ENC_16 | 0x20) 23 | ENC_UNSIGNED_8 = 0x01 24 | ENC_SIGNED_8 = (ENC_SIGNED | 0x02) 25 | ENC_ULAW_8 = 0x04 26 | ENC_ALAW_8 = 0x08 27 | ENC_SIGNED_32 = (ENC_32 | ENC_SIGNED | 0x1000) 28 | ENC_UNSIGNED_32 = (ENC_32 | 0x2000) 29 | ENC_SIGNED_24 = (ENC_24 | ENC_SIGNED | 0x1000) 30 | ENC_UNSIGNED_24 = (ENC_24 | 0x2000) 31 | ENC_FLOAT_32 = 0x200 32 | ENC_FLOAT_64 = 0x400 33 | 34 | 35 | class ID3v1(ctypes.Structure): 36 | _fields_ = [ 37 | ('tag', ctypes.c_char * 3), 38 | ('title', ctypes.c_char * 30), 39 | ('artist', ctypes.c_char * 30), 40 | ('album', ctypes.c_char * 30), 41 | ('year', ctypes.c_char * 4), 42 | ('comment', ctypes.c_char * 30), 43 | ('genre', ctypes.c_ubyte), 44 | ] 45 | 46 | 47 | class Mpg123: 48 | 49 | _lib = None 50 | 51 | class LibInitializationException(Exception): 52 | pass 53 | 54 | class OpenFeedException(Exception): 55 | pass 56 | 57 | class CloseException(Exception): 58 | pass 59 | 60 | class OpenFileException(Exception): 61 | pass 62 | 63 | class NotFeedException(Exception): 64 | pass 65 | 66 | class FeedingException(Exception): 67 | pass 68 | 69 | class FormatException(Exception): 70 | pass 71 | 72 | class DecodeException(Exception): 73 | pass 74 | 75 | class NeedMoreException(Exception): 76 | pass 77 | 78 | class DoneException(Exception): 79 | pass 80 | 81 | class LengthException(Exception): 82 | pass 83 | 84 | class ID3Exception(Exception): 85 | pass 86 | 87 | def plain_strerror(self, errcode): 88 | self._lib.mpg123_plain_strerror.restype = ctypes.c_char_p 89 | return self._lib.mpg123_plain_strerror(errcode).decode() 90 | 91 | def init_library(self, library_path=None): 92 | if not library_path: 93 | library_path = find_library('mpg123') 94 | 95 | if not library_path: 96 | library_path = find_library('libmpg123-0') 97 | 98 | if not library_path: 99 | raise self.LibInitializationException('libmpg123 not found') 100 | 101 | lib = ctypes.CDLL(library_path) 102 | errcode = lib.mpg123_init() 103 | if errcode != OK: 104 | raise self.LibInitializationException(self.plain_strerror(errcode)) 105 | return lib 106 | 107 | def __init__(self, filename=None, library_path=None): 108 | self.handle = None 109 | if not self._lib: 110 | self._lib = self.init_library(library_path) 111 | self._lib.mpg123_new.restype = ctypes.c_void_p 112 | self.c_handle = self._lib.mpg123_new(ctypes.c_char_p(None), None) 113 | self.handle = ctypes.c_void_p(self.c_handle) 114 | self.is_feed = filename is None 115 | self.offset = ctypes.c_size_t(0) 116 | if self.is_feed: 117 | errcode = self._lib.mpg123_open_feed(self.handle) 118 | if errcode != OK: 119 | raise self.OpenFeedException(self.plain_strerror(errcode)) 120 | else: 121 | errcode = self._lib.mpg123_open(self.handle, filename.encode()) 122 | if errcode != OK: 123 | raise self.OpenFileException(self.plain_strerror(errcode)) 124 | 125 | def feed(self, data): 126 | if not self.is_feed: 127 | raise self.NotFeedException('instance is not in feed mode') 128 | # encode string to bytes in modern python 129 | if sys.version_info[0] >= 3 and isinstance(data, str): 130 | data = data.encode() 131 | data = memoryview(data) 132 | errcode = self._lib.mpg123_feed(self.handle, 133 | ctypes.c_char_p(data.tobytes()), 134 | len(data)) 135 | if errcode != OK: 136 | raise self.FeedingException(self.plain_strerror(errcode)) 137 | 138 | def get_id3(self): 139 | v1 = ctypes.c_void_p() 140 | v2 = ctypes.c_void_p() 141 | errcode = self._lib.mpg123_id3(self.handle, 142 | ctypes.pointer(v1), 143 | ctypes.pointer(v2)) 144 | if errcode != OK: 145 | raise self.ID3Exception(self.plain_strerror(errcode)) 146 | if v1.value is None: 147 | raise self.ID3Exception(self.plain_strerror(errcode)) 148 | 149 | return ctypes.cast(v1, ctypes.POINTER(ID3v1)).contents 150 | 151 | def get_format(self): 152 | rate = ctypes.c_int(0) 153 | channels = ctypes.c_int(0) 154 | encoding = ctypes.c_int(0) 155 | 156 | errcode = self._lib.mpg123_getformat(self.handle, 157 | ctypes.pointer(rate), 158 | ctypes.pointer(channels), 159 | ctypes.pointer(encoding)) 160 | if errcode != OK: 161 | if errcode == NEED_MORE: 162 | raise self.NeedMoreException(self.plain_strerror(errcode)) 163 | raise self.FormatException(self.plain_strerror(errcode)) 164 | return (rate.value, channels.value, encoding.value) 165 | 166 | def get_width_by_encoding(self, encoding): 167 | return self._lib.mpg123_encsize(encoding) 168 | 169 | def length(self): 170 | errcode = self._lib.mpg123_length(self.handle) 171 | if errcode <= 0: 172 | if errcode == NEED_MORE: 173 | raise self.NeedMoreException(self.plain_strerror(errcode)) 174 | raise self.LengthException(self.plain_strerror(errcode)) 175 | return errcode 176 | 177 | def frame_length(self): 178 | errcode = self._lib.mpg123_framelength(self.handle) 179 | if errcode <= 0: 180 | if errcode == NEED_MORE: 181 | raise self.NeedMoreException(self.plain_strerror(errcode)) 182 | raise self.LengthException(self.plain_strerror(errcode)) 183 | return errcode 184 | 185 | def decode_frame(self): 186 | audio = ctypes.c_char_p() 187 | done = ctypes.c_size_t(0) 188 | errcode = self._lib.mpg123_decode_frame(self.handle, 189 | ctypes.pointer(self.offset), 190 | ctypes.pointer(audio), 191 | ctypes.pointer(done)) 192 | if errcode == OK: 193 | return ctypes.string_at(audio, done.value) 194 | 195 | if errcode == NEED_MORE: 196 | raise self.NeedMoreException(self.plain_strerror(errcode)) 197 | 198 | if errcode == NEW_FORMAT: 199 | return self.decode_frame() 200 | 201 | if errcode == DONE: 202 | raise self.DoneException(self.plain_strerror(errcode)) 203 | 204 | raise self.DecodeException(self.plain_strerror(errcode)) 205 | 206 | def iter_frames(self, new_format_callback=None): 207 | self.offset = ctypes.c_size_t(0) 208 | audio = ctypes.c_char_p() 209 | done = ctypes.c_size_t(0) 210 | 211 | while True: 212 | errcode = self._lib.mpg123_decode_frame( 213 | self.handle, 214 | ctypes.pointer(self.offset), 215 | ctypes.pointer(audio), 216 | ctypes.pointer(done)) 217 | if errcode == OK: 218 | yield ctypes.string_at(audio, done.value) 219 | else: 220 | if errcode in (NEED_MORE, DONE): 221 | break 222 | if errcode == NEW_FORMAT: 223 | if new_format_callback: 224 | new_format_callback(*self.get_format()) 225 | continue 226 | raise self.DecodeException(self.plain_strerror(errcode)) 227 | 228 | def __del__(self): 229 | if not self.handle: 230 | return 231 | errcode = self._lib.mpg123_close(self.handle) 232 | if errcode != OK: 233 | raise self.CloseException(self.plain_strerror(errcode)) 234 | 235 | 236 | class Out123: 237 | 238 | _lib = None 239 | 240 | class LibInitializationException(Exception): 241 | pass 242 | 243 | class OpenException(Exception): 244 | pass 245 | 246 | class CloseException(Exception): 247 | pass 248 | 249 | class StartException(Exception): 250 | pass 251 | 252 | class PlayingException(Exception): 253 | pass 254 | 255 | def init_library(self, library_path=None): 256 | if not library_path: 257 | library_path = find_library('out123') 258 | 259 | if not library_path: 260 | library_path = find_library('libout123-0') 261 | 262 | if not library_path: 263 | raise self.LibInitializationException('libout123 not found') 264 | 265 | return ctypes.CDLL(library_path) 266 | 267 | def plain_strerror(self, errcode): 268 | self._lib.out123_plain_strerror.restype = ctypes.c_char_p 269 | return self._lib.out123_plain_strerror(errcode).decode() 270 | 271 | def __init__(self, library_path=None): 272 | self.handle = None 273 | if not self._lib: 274 | self._lib = self.init_library(library_path) 275 | self._lib.out123_new.restype = ctypes.c_void_p 276 | self.c_handle = self._lib.out123_new() 277 | self.handle = ctypes.c_void_p(self.c_handle) 278 | errcode = self._lib.out123_open(self.handle, 279 | ctypes.c_char_p(None), 280 | ctypes.c_char_p(None)) 281 | if errcode != OK: 282 | raise self.OpenException(self.plain_strerror(errcode)) 283 | 284 | def start(self, rate, channels, encoding): 285 | errcode = self._lib.out123_start(self.handle, rate, channels, encoding) 286 | if errcode != OK: 287 | raise self.StartException(self.plain_strerror(errcode)) 288 | 289 | def play(self, data): 290 | # encode string to bytes in modern python 291 | if sys.version_info[0] >= 3 and isinstance(data, str): 292 | data = data.encode() 293 | data = memoryview(data) 294 | return self._lib.out123_play(self.handle, 295 | ctypes.c_char_p(data.tobytes()), 296 | len(data)) 297 | 298 | def __del__(self): 299 | if not self.handle: 300 | return 301 | self._lib.out123_close(self.handle) 302 | --------------------------------------------------------------------------------