1

I'm wanting to take an internet audio/radio stream (specifically Longplayer, click for direct stream URL) and play it with python.

It's preferable that it's backgrounded, such that the script able to continue running its main loop. (e.g. as game background music or something, though Pyglet, PyGame et al. may provide their own tools for that.)

I've seen some likely out of date examples of recording internet radio using requests and dumping it into a file but this isn't exactly what I want and the answers' comments seemed to have arguments about requests being problematic among other things? (see here)

I'm open to using any packages you can pip so long as it works with Python 3.X. (Currently using 3.6 purely because I haven't gathered the effort to install 3.7 yet)

To reiterate, I don't want to save the stream, just play it immediately (or with buffering if that's needed?) back to the user. This is preferably without blocking the script, which I imagine would need multithreadng/multiprocessing but this is secondary to just getting playback.)

ch4rl1e97
  • 666
  • 1
  • 7
  • 24
  • The `request` approach should work fine. You do not need to write the data obtained from a request to a file as described in the answer you linked. You need to take the binary data you get from the stream in raw "byte chunks" that you obtain from a request. These can be saved in a bytearray or numpy array and be passed as raw sound buffer data to whatever sound playing package you want (some of which you listed above). – CodeSurgeon Aug 10 '19 at 02:28
  • @CodeSurgeon I should ensure you that those are not specific sound playing packages and merely examples of game packages that may contain capacity for setting up background threads, Not sure on their ability to process raw sound data. I'm simply not familiar with audio libraries (and have limited experience of `requests` what you described sounds feasible but I simply lack the knowledge to find what I would need to do :/ I'll certainly give it an attempt though. – ch4rl1e97 Aug 10 '19 at 19:48
  • What kind of processing do you intend to do with the sound data? Will there be a GUI (in which case one of those game packages would make sense) or do you plan on just playing the sound from the terminal? – CodeSurgeon Aug 10 '19 at 20:14
  • Preferably volume control and the capacity to switch it off/on entirely, I see you've written a full answer below but it's 4am so I'll take a proper look in a day or so! Looks great on a quick scan through though – ch4rl1e97 Aug 13 '19 at 02:56

2 Answers2

2

As it always seems to be the case with these kinds of apparently simple questions, the devil is in the details. I ended up writing some code that should solve this question. The pip dependencies can be installed using python3 -m pip install ffmpeg-python PyOpenAL. The workflow of the code can be divided into two steps:

  1. The code must download binary chunks of mp3 file data from an online stream and convert them to raw PCM data (basically signed uint16_t amplitude values) for playback. This is done using the ffmpeg-python library, which is a wrapper for FFmpeg. This wrapper runs FFmpeg in a separate process, so no blocking occurs here.
  2. The code must then queue these chunks for playback. This is done using PyOpenAL, which is a wrapper for OpenAL. After creating a device and context to enable audio playback, a 3d-positioned source is created. This source is continuously queued with buffers (simulating a "ring buffer") that are filled with data piped in from FFmpeg. This runs on a separate thread from the first step, making downloading new audio chunks run independently from audio chunk playback.

Here is what that code looks like (with some commenting). Please let me know if you have any questions about the code or any other part of this answer.

import ctypes
import ffmpeg
import numpy as np
from openal.al import *
from openal.alc import *
from queue import Queue, Empty
from threading import Thread
import time
from urllib.request import urlopen

def init_audio():
    #Create an OpenAL device and context.
    device_name = alcGetString(None, ALC_DEFAULT_DEVICE_SPECIFIER)
    device = alcOpenDevice(device_name)
    context = alcCreateContext(device, None)
    alcMakeContextCurrent(context)
    return (device, context)

def create_audio_source():
    #Create an OpenAL source.
    source = ctypes.c_uint()
    alGenSources(1, ctypes.pointer(source))
    return source

def create_audio_buffers(num_buffers):
    #Create a ctypes array of OpenAL buffers.
    buffers = (ctypes.c_uint * num_buffers)()
    buffers_ptr = ctypes.cast(
        ctypes.pointer(buffers), 
        ctypes.POINTER(ctypes.c_uint),
    )
    alGenBuffers(num_buffers, buffers_ptr)
    return buffers_ptr

def fill_audio_buffer(buffer_id, chunk):
    #Fill an OpenAL buffer with a chunk of PCM data.
    alBufferData(buffer_id, AL_FORMAT_STEREO16, chunk, len(chunk), 44100)

def get_audio_chunk(process, chunk_size):
    #Fetch a chunk of PCM data from the FFMPEG process.
    return process.stdout.read(chunk_size)

def play_audio(process):
    #Queues up PCM chunks for playing through OpenAL
    num_buffers = 4
    chunk_size = 8192
    device, context = init_audio()
    source = create_audio_source()
    buffers = create_audio_buffers(num_buffers)

    #Initialize the OpenAL buffers with some chunks
    for i in range(num_buffers):
        buffer_id = ctypes.c_uint(buffers[i])
        chunk = get_audio_chunk(process, chunk_size)
        fill_audio_buffer(buffer_id, chunk)

    #Queue the OpenAL buffers into the OpenAL source and start playing sound!
    alSourceQueueBuffers(source, num_buffers, buffers)
    alSourcePlay(source)
    num_used_buffers = ctypes.pointer(ctypes.c_int())

    while True:
        #Check if any buffers are used up/processed and refill them with data.
        alGetSourcei(source, AL_BUFFERS_PROCESSED, num_used_buffers)
        if num_used_buffers.contents.value != 0:
            used_buffer_id = ctypes.c_uint()
            used_buffer_ptr = ctypes.pointer(used_buffer_id)
            alSourceUnqueueBuffers(source, 1, used_buffer_ptr)
            chunk = get_audio_chunk(process, chunk_size)
            fill_audio_buffer(used_buffer_id, chunk)
            alSourceQueueBuffers(source, 1, used_buffer_ptr)

if __name__ == "__main__":    
    url = "http://icecast.spc.org:8000/longplayer"

    #Run FFMPEG in a separate process using subprocess, so it is non-blocking
    process = (
        ffmpeg
        .input(url)
        .output("pipe:", format='s16le', acodec='pcm_s16le', ac=2, ar=44100, loglevel="quiet")
        .run_async(pipe_stdout=True)
    )

    #Run audio playing OpenAL code in a separate thread
    thread = Thread(target=play_audio, args=(process,), daemon=True)
    thread.start()

    #Some example code to show that this is not being blocked by the audio.
    start = time.time()
    while True:
        print(time.time() - start)
CodeSurgeon
  • 2,435
  • 2
  • 15
  • 36
1

With pyminiaudio: (it provides an icecast stream source class):

import miniaudio

def title_printer(client: miniaudio.IceCastClient, new_title: str) -> None:
    print("Stream title: ", new_title)

with miniaudio.IceCastClient("http://icecast.spc.org:8000/longplayer",
        update_stream_title=title_printer) as source:
    print("Connected to internet stream, audio format:", source.audio_format.name)
    print("Station name: ", source.station_name)
    print("Station genre: ", source.station_genre)
    print("Press <enter> to quit playing.\n")
    stream = miniaudio.stream_any(source, source.audio_format)
    with miniaudio.PlaybackDevice() as device:
        device.start(stream)
        input()   # wait for user input, stream plays in background
Irmen de Jong
  • 2,739
  • 1
  • 14
  • 26