morrolinux / mpradio

Morrolinux's Pirate radio (PiFmRDS / PiFmAdv implementation with Bluetooth and mp3 support) - Stream music to your car's FM radio or use it as a Bluetooth speaker via headphone jack
GNU General Public License v3.0
108 stars 17 forks source link

Bluetooth Media Control and display #61

Open DavidM42 opened 5 years ago

DavidM42 commented 5 years ago

So first of all thanks for this really cool project. The only two things I'm still missing from this is the possibility to skip songs/pause on a bt client player device (like a phone) and the forwarding of the currently playing track into rds. I've done some reasearch because I'd like to help integrating them via PR but I don't understand the project enough for now. The thing needed for this is probably the AVCRP protocol to control media playback and get more info from apps integrating into this protocol on phones. I found out that you can access this information/send info to bluez via the dbus but I could not get it done myself (partly because of bluez documentation beeing sparse). Does anybody know more about this topic?

DavidM42 commented 5 years ago

Update

I found some resources for what to do with the bluetooth stack:

  1. This SO question with example code to get currently playing music metadata
  2. This reddit question with the interesting part in edit 2 and following

Getting data about playback

import dbus
#XX_XX_XX_XX_XX_XX is a placeholder for bt mac adress of connected device

#probs dict should yield same results as terminal command but in more structured format
#qdbus --system org.bluez /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/player0 

bus  = dbus.SystemBus()

player = bus.get_object("org.bluez", "/org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/player0")
BT_Media_iface = dbus.Interface(player, dbus_interface="org.bluez.MediaPlayer1")

BT_Media_probs = dbus.Interface(player, "org.freedesktop.DBus.Properties")

probs = BT_Media_probs.GetAll("org.bluez.MediaPlayer1")
print(probs)

Control Playback

And this command for skipping the current song

dbus-send --system --type=method_call --dest=org.bluez /org/bluez/hci0/dev_XX_XX_XX_XX_XX_XX/player0 org.bluez.MediaPlayer1.Next

The last word in the value can be:

as found in the reddit question.

I'm not that familiar with dbus so I couldn't figure out yet how to send the commands with the python library.

Now the thing I'm unsure with is how to integrate the metadata into the extisting rds services and the commands into the button service to use when BT is connected.

DavidM42 commented 5 years ago

also just saw #29 . info here should help with this issue

morrolinux commented 5 years ago

Hello! First of all, thanks for your interest, second, there are so many good informations here, I'll have to read through them carefully when I get a slot of free time. Also, I wouldn't worry too much about integrations if I were in you, because I've got a good news as well: I'm thinking about re-writing the whole project in python, it would be much easier to integrate, and right now the project suffers from a variety of reliability inconsistencies when it comes to bluetooth audio playback (for some works, for others don't) probably due to it relying on system and bash scripts for detecting new devices upon connection. You want to help, you're very welcome! I'd suggest you to improve your python3 because we'll probably need it soon :)

DavidM42 commented 5 years ago

That's some good news as python is my preferred language. I'd like to help as far as I can. Do you have any plan when you want to start the rewrite?

DavidM42 commented 5 years ago

I got a working script to control BT media playback. It does not do anything for local playback as I only use bluetooth but it seems to work. I replaced the mpradio-pushbutton-skip.py in my local /bin/ folder with mine to utilize the existing service.

How to use

Code

import RPi.GPIO as GPIO
import time
from subprocess import check_output, call
import re

GPIO.setmode(GPIO.BCM)
GPIO.setup(18, GPIO.IN, pull_up_down=GPIO.PUD_UP)

def get_mac():
    device_cmd = "bt-device -l"
    result = check_output(device_cmd, shell=True)
    regex = "[(].*[)]"
    mac = re.findall(regex, result)[0]
    #print(mac)
    return mac.replace(":","_").replace("(","").replace(")","")

#TODO refactor for less code repetition to just alter print and last command

last_play = True

def pause():
    cmd_arr = ["dbus-send", "--system" , "--type=method_call", "--dest=org.bluez", "/org/bluez/hci0/dev_" + get_mac() + "/player0", "org.bluez.MediaPlayer1.Pause"]
    global last_play
    if last_play == False:
        print("Play")
        cmd_arr[5] = "org.bluez.MediaPlayer1.Play"
    else:
        print("Pause")
    #inverse last play after command
    last_play = not last_play
    call(cmd_arr)

def skip():
    print("skip")
    global last_play
    last_play = True
    cmd_arr = ["dbus-send", "--system" , "--type=method_call", "--dest=org.bluez", "/org/bluez/hci0/dev_" + get_mac() + "/player0", "org.bluez.MediaPlayer1.Next"]
    call(cmd_arr)

def previous():
    print("previous")
    #spotify start playing on previous command so removed
    #global last_play
    #last_play = True
    cmd_arr = ["dbus-send", "--system" , "--type=method_call", "--dest=org.bluez", "/org/bluez/hci0/dev_" + get_mac() + "/player0", "org.bluez.MediaPlayer1.Previous"]
    call(cmd_arr)

def shutdown():
    print('shutdown')
    call(["killall", "mpradio"])
    call(["killall", "sox"])
    call(["shutdown", "-h", "now"])
    time.sleep(10)

state = True
max_delay = 1
shutdown_push_time = 5
last_time = time.time()
pulse_count = 0

while True:
    new_state = GPIO.input(18)

    if new_state == False and state == False and (time.time() - last_time) > shutdown_push_time:
        #if button is still pressed after 5 seconds shutdown device
        shutdown()

    if new_state == False and state == True: #going from off to on
        pulse_count += 1
        state = new_state
        last_time = time.time()

    elif new_state and state == False: #on to off
        state = True

    if time.time() > (last_time + max_delay) and pulse_count > 0: #too long a delay so after end of sequence
        if pulse_count == 1:
            pause()
        elif pulse_count == 2:
            skip()
        elif pulse_count == 3:
            previous()
        else:
            pause()
        pulse_count = 0
morrolinux commented 5 years ago

@DavidM42 I can't make precise schedule at the moment because everything's so "dynamic" in this period, but I probably would start with the bluetooth a2dp part to see if it makes sense.

morrolinux commented 5 years ago

So I've been laying down some "projectual ideas" and I think I'm going for this logic image In python we can easily popen processes and attach to their stdin/stdout so I think it will also be possible to hotswap sources (players) and sinks (outputs) without having to restart

DavidM42 commented 5 years ago

Nice. Seems good. pi_fm_adv should be the go to. From my testing it produces much better quality than the other driver. I still have a question regarding the "old" current implementation. How is the rds input handled? I'd like to write a test script to pipe Bluetooth song info into rds but I couldn't figure out the correct way.

morrolinux commented 5 years ago

@DavidM42 have a look at mpradio-legacyRDS.sh , but basically it's a matter of piping your text like so: echo "PS $legacytitle" >/home/pi/rds_ctl and pi_fm_adv reads polls the control pipe. more info about that in its repo

Hurricos commented 5 years ago

It's been a while since I've poked around on this, but I think I'll throw my hat into the ring this weekend. I am not the strongest Python coder, but I love the ideas you put together in that diagram @morrolinux, so I should be able to help cobble something together if you want to cooperate on a python branch.

Python also means we can rely on stronger id3 libraries. I'm quite excited to figure out how to interface with stdin/stdout of processes via popen.

Hurricos commented 5 years ago

@DavidM42 nice discoveries RE: AVRCP! I'd tried but failed to scratch that itch a while ago :( I'll see what I can do to help now :)

morrolinux commented 5 years ago

@Hurricos Cool! since the last schematic I've changed the design just a bit on the remote controller part, but overall that's the thing. I'll update the schematic soon. This afternoon I'm going to open a new repo on github to share the current progress. (spoiler: core functionalities are already working!) :)

morrolinux commented 5 years ago

@Hurricos I've published a new repo for the python version here I'm currently putting the basis for a media indexer to feed to the playlist manager, I'll create an ad-hoc data structure and I'd like you to take care of the id3 stuff and filling-in the data structure with those data. just tell me if/when you start working on something, so that we can coordinate better

morrolinux commented 5 years ago

@DavidM42 same for you of course :)

DavidM42 commented 5 years ago

I've got a few questions about the rewrite.

And concerning bluetooth rds I'm facing a bit of a non technical problem. I couldn't find any radio which has rds info besides the car but I won't be debugging on a pi in a car :D

DavidM42 commented 5 years ago

I created a rough draft for the id3 tag reading. Was easy to implement thanks to the nice library mutagen it's in my fork here I would also suggest some kind of dependancy managment (a requirements.txt or setup.py to make it a module or something)

DavidM42 commented 5 years ago

And regarding my pushbutton script. I tested it a few times in the last days and the recognition of pushes does not quite work as wanted. Double clicking leads to a skip and then pause -> the timing probably needs to probably be edited for it to be usable. Two other things I notices in my testing: There still is a noticable boot up time (not ideal for car use). Nothing game breaking but maybe we should evalutate a build with a more lightweight faster booting os or even docker container/resin os kind of OS. And lastly when I try to connect, the device often asks my phone for repairing which is kind of odd and annoying (but maybe an effect of the read only setting?)

morrolinux commented 5 years ago
  • Which python version is the minimum supported?

Well I haven't set a requirement as of yet, but of course it must run on Raspbian 9 which comes with python 3.5 so if it runs on it, it's fine to me. Portability on other platofrms is not really a need because of hardware constraints in FM transmission with PiFmAdv.

  • Any requirements concerning additional python libraries? License wise restricted, maybe as few dependancies as possible or whatever?

I'm not a big expert on licenses, so I guess anything different from GPL will have to be checked for requirements for integration in the project. But I guess it should generally be fine if we link it in the README. As few dependencies as possible is always a nice goal to acheive, unless it results in the code being utterly convoluted and unnecessarely complicated... then I would say who cares, let's not make it an unreadable mess.

  • Could you explain how to setup a usable dev environment. So linux of course but any other reccomendations?

I'm not really sure what you mean by that. As of now, no non-standard python libraries are being used and I'm not using a python environment. If you run mpradio.py on your computer, it will play "storage" files on speakers instead of attempting to stream on FM

And concerning bluetooth rds I'm facing a bit of a non technical problem. I couldn't find any radio which has rds info besides the car but I won't be debugging on a pi in a car :D

Oh but don't worry about that, it will probably won't be needed. I'll put the basis for the RDS updater module and running it on something that's not a Raspberry Pi will just print out RDS info to the screen so that we'll all be able to work comfortably

morrolinux commented 5 years ago

I created a rough draft for the id3 tag reading. Was easy to implement thanks to the nice library mutagen it's in my fork here I would also suggest some kind of dependancy managment (a requirements.txt or setup.py to make it a module or something)

Cool. I would start with a requirements.txt just because we're in a really early stage of the work, then make it a module with the very first release, but really, if you start to work on the setup.py I won't say no

morrolinux commented 5 years ago

There still is a noticable boot up time (not ideal for car use). Nothing game breaking but maybe we should evalutate a build with a more lightweight faster booting os or even docker container/resin os kind of OS

Are you using a pre-built mpradio image? or maybe you're running on raspbian 9 with the "classic" mpradio installed? because it has lots of systemd unit dependencies and it doesn't really work out to be fast. With mpradio-py I would only check for the bluetooth interface to be available and let mpradio.py start all the needed services

And lastly when I try to connect, the device often asks my phone for repairing which is kind of odd and annoying (but maybe an effect of the read only setting?)

Most likely yes. But again, now I think you're working on the "classic" mpradio. Here we should start from scratch

morrolinux commented 5 years ago

@DavidM42 I'm waiting for your PR about id3 data the new repo, which has now a skeleton to be placed to