I can only get streams to play for between 1 and 10ish frames before the player crashes. I've noticed that every time the player crashes, it has far fewer bytes to work with for that frame. This means there is something wrong with my sync logic. It's finding a sync too early. I don't know how to solve this. I've done some research on the mp3 specs and found examples in forms that are completely unrelated to Python. They seem to agree to do a word_search
in the 12 most significant bits. I've done that, and it even works ... for less than a second. What am I missing?
This is the entire code. z
is tossed in to make sure the loop doesn't run on forever. It's not intended to be permanent.
import requests, io, pygame
pygame.init()
pygame.mixer.init()
def stream(url:str):
s = requests.Session()
d = bytearray()
z = 0
with s.get(url, headers=None, stream=True) as resp:
for b in resp.iter_content():
d.append(int.from_bytes(b)) #append this byte
sync = int.from_bytes(d[-2:]) #get last two bytes
#if we have enough bytes and sync
if (len(d) > 12) and (sync == 0xFFF0):
pygame.mixer.music.load(io.BytesIO(bytes(d)))
pygame.mixer.music.play()
pygame.event.wait()
d = bytearray() #clear for next frame
z+=1
if z>100: return
stream('http://198.27.120.235:8450')
edit
I grabbed the header and inserted it at the beginning of each new frame. It works for longer, but it seems to just quit after a few seconds. It teeters between no error and pygame.error: music_drmp3: corrupt mp3 file (bad stream).
How do I reliably "chunk" the mp3 data?
import requests, io, pygame
pygame.init()
pygame.mixer.init()
def stream(url:str):
s = requests.Session()
h = bytearray()
d = bytearray()
with s.get(url, headers=None, stream=True) as resp:
for b in resp.iter_content():
if len(h) < 4: h.append(int.from_bytes(b)) #get header
else : d.append(int.from_bytes(b)) #append this byte
sync = int.from_bytes(d[-2:]) #get last two bytes
#if we have enough bytes and sync
if (L:=len(d)) and (sync == 0xFFF0):
#length, first 4 bytes, last 4 bytes (looking for patterns)
print(L, hex(int.from_bytes(d[:4])), hex(int.from_bytes(d[-4:])))
pygame.mixer.music.load(io.BytesIO(h+d))
pygame.mixer.music.play()
d = bytearray()
stream('http://198.27.120.235:8450')
As you can see from a print, the size of the data (left column) fluctuates wildly, but all of those lines, up to the last one, played just fine. However, the last one is always significantly less bytes than the rest of the frames. This can only mean that I am not finding the sync bits properly. Apparently, the signature of the sync bits can exist in the audio data. How do I differentiate between the 2?
7175 0x7f0a747d 0x83fffff0
20035 0x41dffe4d 0xfffffff0
1114 0xcde3fff3 0x1016fff0
5039 0x7f29ff68 0x7ffffff0
9337 0x28358c14 0x71dffff0
2262 0x8bc3caa1 0xfffffff0
18097 0x8894f5a0 0x659ffff0
19282 0xd09ababd 0xfffffff0
8136 0xe75fa462 0xfc41fff0
1336 0xc7ffa8f9 0x77fffff0
485 0x683a5b88 0xcbbbfff0
Traceback (most recent call last):
File "C:\Users\Michael\Documents\SlimPad_ftk\scratch.py", line 25, in <module>
stream('http://198.27.120.235:8450')
File "C:\Users\Michael\Documents\SlimPad_ftk\scratch.py", line 21, in stream
pygame.mixer.music.load(io.BytesIO(h+d))
pygame.error: music_drmp3: corrupt mp3 file (bad stream).
I did not realize that play
is non-blocking. The solution was to take more control over when a frame is loaded. Also, stream data needs to be an even number of bytes when comparing sync.
This refactor works flawlessly with one caveat, loading the next frame is somewhat determined by how fast the loop can catch it. In those microseconds where we are in a lull, a small 'click' is created. Refactoring this again where play
is waiting for itself (instead of the loop), should eliminate the clicks. However, the clicks and pops are far less than you would hear on an old record, and quite similar in dynamics. I could just not refactor anything and call it a feature.
"Listen to all of today's hottest streams in record player quality!"
import requests, io, os, pygame, time
#create and call - clear terminal
(clear := lambda: os.system(('clear','cls')[os.name=='nt']))()
pygame.init()
pygame.mixer.init()
def streamer(rip:bool=False) -> None:
sess = requests.Session()
head = bytearray()
frame = bytearray()
stream = bytearray()
sync = b'\xff\xf0'
filename = f'episode_{int(time.time()*100000)}.mp3'
#create file if we need it and it doesn't exist
if rip and not os.path.isfile(filename):
with open(filename, 'w'): ...
try:
url = input("\nstream address: http://")
with sess.get(f'http://{url}', headers=None, stream=True) as resp:
#fail harder - so we can catch it and reset
if not resp.ok: raise Exception
for byte in resp.iter_content():
if stream and not pygame.mixer.music.get_busy():
#play stream or fail
try:
pygame.mixer.music.load(io.BytesIO(head + stream + sync))
pygame.mixer.music.play()
except:
print(f'ignoring corrupt stream')
#append stream to file
if rip:
with open(filename, 'r+b') as file:
file.seek(4, 2)
file.write(stream+sync)
#clear stream
stream = bytearray()
#header and current frame
if len(head) == 4: frame.extend(byte)
else : head.extend(byte)
#must have an even number of bytes to compare sync
if (L := len(frame)) % 2: continue
#compare sync frame
if L and (frame[-2:] == sync):
stream.extend(frame[:-2]) #append to stream
frame = bytearray() #clear current frame
except:
clear()
print('failures have occured. maybe try a different stream address')
streamer(rip)
#test streams
#kathy.torontocast.com:3330
#www.partyviberadio.com:8020
#198.27.120.235:8450
streamer(True)