From efd5edb285e5ae5dc1ba3d158f91b3a5b9ab24a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20M=C3=A9ndez?= Date: Tue, 17 Dec 2024 22:57:00 -0600 Subject: [PATCH] Bluetooth source support (#14) Support using Linamp as a Bluetooth audio receiver --- README.md | 3 +- debian/changelog | 6 + debian/control | 3 +- debian/rules | 2 + python/linamp/__init__.py | 3 +- python/linamp/baseplayer/__init__.py | 1 + python/linamp/baseplayer/baseplayer.py | 85 +++ python/linamp/btplayer/__init__.py | 1 + python/linamp/btplayer/btadapter.py | 283 ++++++++ python/linamp/btplayer/btplayer.py | 150 ++++ python/linamp/cdplayer.py | 2 +- python/requirements.txt | 1 + .../audiosourcebluetooth.cpp | 660 ++++++++++++------ .../audiosourcebluetooth.h | 115 +-- src/audiosourcecd/audiosourcecd.cpp | 8 +- src/shared/util.cpp | 2 +- src/view-basewindow/mainwindow.cpp | 12 +- src/view-player/playerview.cpp | 22 +- 18 files changed, 1054 insertions(+), 305 deletions(-) create mode 100644 python/linamp/baseplayer/__init__.py create mode 100644 python/linamp/baseplayer/baseplayer.py create mode 100644 python/linamp/btplayer/__init__.py create mode 100644 python/linamp/btplayer/btadapter.py create mode 100644 python/linamp/btplayer/btplayer.py diff --git a/README.md b/README.md index fd2939d..cfb37d9 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Music player app for Linamp - Your favorite music player of the 90s, but in real - python3-pip - python3-full - python3-dev +- python3-dbus-next - libdiscid0 ## Development @@ -34,7 +35,7 @@ sudo apt-get install build-essential qt6-base-dev qt6-base-dev-tools qt6-multime sudo apt-get install libtag1-dev libasound2-dev libpulse-dev libpipewire-0.3-dev libdbus-1-dev -y # Install dependencies for Python (for CD player functionality) -sudo apt-get install vlc libiso9660-dev libcdio-dev libcdio-utils swig python3-pip python3-full python3-dev libdiscid0 libdiscid-dev -y +sudo apt-get install vlc libiso9660-dev libcdio-dev libcdio-utils swig python3-pip python3-full python3-dev python3-dbus-next libdiscid0 libdiscid-dev -y # Create Python venv and install Python dependencies (for CD player functionality) ## IMPORTANT: Make sure you are on the folder where you cloned this repo before running the following commands: diff --git a/debian/changelog b/debian/changelog index cbda2f9..c7c522f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +linamp (1.3.0) bookworm; urgency=medium + + * Feature: Bluetooth Source, use Linamp as a Bluetooth audio receiver + + -- Rodrigo Méndez Tue, 17 Dec 2024 22:37:00 -0600 + linamp (1.2.1) bookworm; urgency=medium * Bugfix: Fixed glitches on spectrum analyzer for views that use audiospectrumcatpure class (CD player, Bluetooth player) diff --git a/debian/control b/debian/control index 909d679..379175a 100644 --- a/debian/control +++ b/debian/control @@ -37,7 +37,8 @@ Depends: python3-libdiscid, python3-musicbrainzngs, python3-cdio, - python3-vlc + python3-vlc, + python3-dbus-next Description: Your favorite music player of the 90s, but in real life Music player app for Linamp - Your favorite music player of the 90s, but in real life. diff --git a/debian/rules b/debian/rules index 562edb2..c7fad36 100755 --- a/debian/rules +++ b/debian/rules @@ -10,6 +10,8 @@ override_dh_auto_install: mkdir -p debian/linamp/usr/lib/python3/dist-packages/linamp cp python/linamp/__init__.py debian/linamp/usr/lib/python3/dist-packages/linamp/__init__.py cp python/linamp/cdplayer.py debian/linamp/usr/lib/python3/dist-packages/linamp/cdplayer.py + cp -r python/linamp/baseplayer debian/linamp/usr/lib/python3/dist-packages/linamp/baseplayer + cp -r python/linamp/btplayer debian/linamp/usr/lib/python3/dist-packages/linamp/btplayer mkdir -p debian/linamp/usr/share/applications/ cp linamp.desktop debian/linamp/usr/share/applications/ diff --git a/python/linamp/__init__.py b/python/linamp/__init__.py index 2916948..14ae616 100644 --- a/python/linamp/__init__.py +++ b/python/linamp/__init__.py @@ -1 +1,2 @@ -from .cdplayer import * +from linamp.cdplayer import * +from linamp.btplayer import * diff --git a/python/linamp/baseplayer/__init__.py b/python/linamp/baseplayer/__init__.py new file mode 100644 index 0000000..ca8ffee --- /dev/null +++ b/python/linamp/baseplayer/__init__.py @@ -0,0 +1 @@ +from linamp.baseplayer.baseplayer import * diff --git a/python/linamp/baseplayer/baseplayer.py b/python/linamp/baseplayer/baseplayer.py new file mode 100644 index 0000000..1c1e5d0 --- /dev/null +++ b/python/linamp/baseplayer/baseplayer.py @@ -0,0 +1,85 @@ +from enum import Enum + +class PlayerStatus(Enum): + Idle = 'idle' + Playing = 'playing' + Stopped = 'stopped' + Paused = 'paused' + Error = 'error' + Loading = 'loading' + + +# Base Player abstract class +class BasePlayer: + # Called when the source is selected in the UI or whenever the full track info needs to be refreshed + def load(self) -> None: + pass + + # Called when the source is deselected in the UI + def unload(self) -> None: + pass + + # Basic playback control funcions called on button press + def play(self) -> None: + pass + + def stop(self) -> None: + pass + + def pause(self) -> None: + pass + + def next(self) -> None: + pass + + def prev(self) -> None: + pass + + # Go to a specific time in a track while playing + def seek(self, ms: int) -> None: + pass + + # Playback mode toggles + def set_shuffle(self, enabled: bool) -> None: + pass + + def set_repeat(self, enabled: bool) -> None: + pass + + # Eject button + def eject(self) -> None: + pass + + # -------- Status Functions -------- + + # Called constantly during playback to update the time progress + def get_postition(self) -> int: + return 0 + + def get_shuffle(self) -> bool: + return False + + def get_repeat(self) -> bool: + return False + + # Returns the str representation of PlayerStatus enum + def get_status(self) -> str: + status = PlayerStatus.Idle + return status.value + + # tuple with format (tracknumber: int, artist, album, title, duration_ms: int, codec: str, bitrate_bps: int, samplerate_hz: int) + def get_track_info(self) -> tuple[int, str, str, str, int, str, int, int]: + return (1, '', '', '', 1, 'AAC', 256000, 44100) + + # Return any message you want to show to the user. tuple with format: (show_message: bool, message: str, message_timeout_ms: int) + def get_message(self) -> tuple[bool, str, int]: + return (False, '', 0) + + # Called whenever the UI wants to force clear the message + def clear_message(self) -> None: + pass + + # -------- Polling functions to be called by a timer -------- + + def poll_events(self) -> bool: + return False diff --git a/python/linamp/btplayer/__init__.py b/python/linamp/btplayer/__init__.py new file mode 100644 index 0000000..8748892 --- /dev/null +++ b/python/linamp/btplayer/__init__.py @@ -0,0 +1 @@ +from linamp.btplayer.btplayer import * diff --git a/python/linamp/btplayer/btadapter.py b/python/linamp/btplayer/btadapter.py new file mode 100644 index 0000000..d8f407f --- /dev/null +++ b/python/linamp/btplayer/btadapter.py @@ -0,0 +1,283 @@ +import asyncio +from enum import Enum + +from dbus_next.aio import MessageBus +from dbus_next import BusType + +loop = asyncio.get_event_loop() + +SERVICE_NAME = 'org.bluez' +DEVICE_IFACE = SERVICE_NAME + '.Device1' +PLAYER_IFACE = SERVICE_NAME + '.MediaPlayer1' +TRANSPORT_IFACE = SERVICE_NAME + '.MediaTransport1' + +# Useful docs: +# https://manpages.ubuntu.com/manpages/oracular/man5/org.bluez.MediaPlayer.5.html +# https://manpages.ubuntu.com/manpages/oracular/man5/org.bluez.MediaTransport.5.html + +# Possible values for player status: +# - playing +# - stopped +# - paused +# - forward-seek +# - reverse-seek +# - error + +# Possible values for transport state: +# - idle +# - pending +# - active + +# Possible values for repeat: +# - off +# - singletrack +# - alltracks +# - group + +# Possible values for shuffle: +# - off +# - alltracks +# - group + +# Please note: these are unconfirmed +class BTCodec(Enum): + SBC = 0 + MP3 = 1 + AAC = 2 + APTX = 3 + APTXHD = 4 + +class BTTrackInfo(): + def __init__(self, title: str = '', track_number: int = 0, number_of_tracks: int = 0, duration: int = 0, album: str = '', artist: str = ''): + self.title = title + self.track_number = track_number + self.number_of_tracks = number_of_tracks + self.duration = duration + self.album = album + self.artist = artist + + def __str__(self): + repr = '\n' + repr = repr + f' Title: {self.title}\n' + repr = repr + f' Album: {self.album}\n' + repr = repr + f' Artist: {self.artist}\n' + + seconds_total = self.duration/1000 + minutes = int(seconds_total/60) + seconds = int(seconds_total - (minutes * 60)) + + repr = repr + f' Duration: {str(minutes).zfill(2)}:{str(seconds).zfill(2)} ({self.duration})\n' + repr = repr + f' Track Number: {self.track_number}\n' + repr = repr + f' Number of Tracks: {self.number_of_tracks}\n' + + return repr + +def wait_for_loop() -> None: + """Antipattern: waits the asyncio running loop to end""" + while loop.is_running(): + pass + +def is_empty_player_track(track: BTTrackInfo) -> bool: + return track.duration <= 0 + + +class BTPlayerAdapter(): + bus = None + manager = None + + device = None + device_alias = None + player = None + player_interface = None + transport = None + + connected = False + transport_state = None + codec = None + codec_configuration = None + status = None + track = BTTrackInfo() + position = 0 + repeat = 'off' + shuffle = 'off' + + def __init__(self): + pass + + async def _get_dbus_object(self, path): + introspection = await self.bus.introspect(SERVICE_NAME, path) + obj = self.bus.get_proxy_object(SERVICE_NAME, path, introspection) + return obj + + async def _get_manager(self): + obj = await self._get_dbus_object('/') + manager = obj.get_interface('org.freedesktop.DBus.ObjectManager') + return manager + + async def _get_player(self, path): + return await self._get_dbus_object(path) + + async def _get_device(self, path): + return await self._get_dbus_object(path) + + async def _get_transport(self, path): + return await self._get_dbus_object(path) + + def _parse_track_info(self, track_raw): + title = track_raw['Title'].value if 'Title' in track_raw else 'Unknown' + track_number = track_raw['TrackNumber'].value if 'TrackNumber' in track_raw else 0 + number_of_tracks = track_raw['NumberOfTracks'].value if 'NumberOfTracks' in track_raw else 0 + duration = track_raw['Duration'].value if 'Duration' in track_raw else 0 + album = track_raw['Album'].value if 'Album' in track_raw else 'Unknown' + artist = track_raw['Artist'].value if 'Artist' in track_raw else 'Unknown' + track_info = BTTrackInfo(title, track_number, number_of_tracks, duration, album, artist) + return track_info + + async def setup(self): + """Initialize DBus""" + self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() + self.manager = await self._get_manager() + + def print_state(self): + print(f'Connected: {self.connected}') + print(f'State: {self.transport_state}') + print(f'Codec: {self.codec}') + print(f'Configuration: {self.codec_configuration}') + print(f'Status: {self.status}') + print(f'Device Alias: {self.device_alias}') + + seconds_total = self.position/1000 + minutes = int(seconds_total/60) + seconds = int(seconds_total - (minutes * 60)) + + print(f'Position: {str(minutes).zfill(2)}:{str(seconds).zfill(2)} ({self.position})') + print(f'Repeat: {self.repeat}') + print(f'Shuffle: {self.shuffle}') + print(f'Track: {self.track}') + + async def find_player(self): + """Identify current player and device""" + objects = await self.manager.call_get_managed_objects() + + player_path = None + transport_path = None + for path in objects: + interfaces = objects[path] + if PLAYER_IFACE in interfaces: + player_path = path + if TRANSPORT_IFACE in interfaces: + transport_path = path + + if player_path: + #print(f'Found player: {player_path}') + # Setup connected state + self.connected = True + self.player = await self._get_player(player_path) + self.player_interface = self.player.get_interface(PLAYER_IFACE) + player_properties = self.player.get_interface('org.freedesktop.DBus.Properties') + device_path_raw = await player_properties.call_get(PLAYER_IFACE, 'Device') + device_path = device_path_raw.value + self.device = await self._get_device(device_path) + device_properties = self.device.get_interface('org.freedesktop.DBus.Properties') + device_alias_raw = await device_properties.call_get(DEVICE_IFACE, 'Alias') + self.device_alias = device_alias_raw.value + + all_player_properties = await player_properties.call_get_all(PLAYER_IFACE) + if 'Status' in all_player_properties: + self.status = all_player_properties['Status'].value + if 'Track' in all_player_properties: + track_raw = all_player_properties['Track'].value + self.track = self._parse_track_info(track_raw) + if 'Position' in all_player_properties: + self.position = all_player_properties['Position'].value + if 'Repeat' in all_player_properties: + self.repeat = all_player_properties['Repeat'].value + if 'Shuffle' in all_player_properties: + self.shuffle = all_player_properties['Shuffle'].value + + else: + self.connected = False + self.player = None + self.player_interface = None + self.device = None + self.device_alias = None + self.status = None + self.track = BTTrackInfo() + self.position = 0 + self.repeat = 'off' + self.shuffle = 'off' + #print('No Player found') + + if transport_path: + #print(f'Found transport: {transport_path}') + self.transport = await self._get_transport(transport_path) + transport_properties = self.transport.get_interface('org.freedesktop.DBus.Properties') + all_transport_properties = await transport_properties.call_get_all(TRANSPORT_IFACE) + if 'State' in all_transport_properties: + self.transport_state = all_transport_properties['State'].value + if 'Codec' in all_transport_properties: + self.codec = BTCodec(all_transport_properties['Codec'].value) + if 'Configuration' in all_transport_properties: + self.codec_configuration = all_transport_properties['Configuration'].value + else: + self.transport = None + self.transport_state = None + self.codec = None + self.codec_configuration = None + + def setup_sync(self): + wait_for_loop() + loop.run_until_complete(self.setup()) + + def find_player_sync(self): + wait_for_loop() + loop.run_until_complete(self.find_player()) + + def play(self): + if not self.player_interface: + return + wait_for_loop() + loop.run_until_complete(self.player_interface.call_play()) + + def pause(self): + if not self.player_interface: + return + wait_for_loop() + loop.run_until_complete(self.player_interface.call_pause()) + + def stop(self): + if not self.player_interface: + return + wait_for_loop() + loop.run_until_complete(self.player_interface.call_stop()) + + def next(self): + if not self.player_interface: + return + wait_for_loop() + loop.run_until_complete(self.player_interface.call_next()) + + def previous(self): + if not self.player_interface: + return + wait_for_loop() + loop.run_until_complete(self.player_interface.call_previous()) + + def set_shuffle(self, enabled: bool) -> None: + if not self.player_interface: + return + wait_for_loop() + self.shuffle = 'alltracks' if enabled else 'off' + loop.run_until_complete(self.player_interface.set_shuffle(self.shuffle)) + + def set_repeat(self, enabled: bool) -> None: + if not self.player_interface: + return + wait_for_loop() + self.repeat = 'alltracks' if enabled else 'off' + loop.run_until_complete(self.player_interface.set_repeat(self.repeat)) + + def get_codec_str(self) -> str: + if not self.codec: + return '' + return self.codec.name diff --git a/python/linamp/btplayer/btplayer.py b/python/linamp/btplayer/btplayer.py new file mode 100644 index 0000000..220426b --- /dev/null +++ b/python/linamp/btplayer/btplayer.py @@ -0,0 +1,150 @@ +from linamp.baseplayer import BasePlayer, PlayerStatus +from linamp.btplayer.btadapter import BTPlayerAdapter, is_empty_player_track + +EMPTY_TRACK_INFO = ( + 0, + '', + '', + '', + 0, + '', + 0, + 44100 +) + +class BTPlayer(BasePlayer): + + message: str + show_message: bool + message_timeout: int + + player: BTPlayerAdapter + track_info: tuple[int, str, str, str, int, str, int, int] + + def __init__(self) -> None: + self.player = BTPlayerAdapter() + # tuple with format (tracknumber: int, artist, album, title, duration: int, codec: str, bitrate_bps: int, samplerate_hz: int) + self.track_info = EMPTY_TRACK_INFO + + self.clear_message() + + self.player.setup_sync() + + def _display_connection_info(self): + if self.player.connected: + self.message = f'CONNNECTED TO: {self.player.device_alias}' + self.show_message = True + self.message_timeout = 5000 + else: + self.message = 'DISCONNECTED' + self.show_message = True + self.message_timeout = 5000 + + # -------- Control Functions -------- + + def load(self) -> None: + self.player.find_player_sync() + if self.player.connected: + track = self.player.track + if not track or is_empty_player_track(track): + self._display_connection_info() + return + self.track_info = ( + track.track_number, + track.artist, + track.album, + track.title, + track.duration, + self.player.get_codec_str(), + 0, # No simple way to know bitrate from BT + 44100 + ) + else: + self.track_info = EMPTY_TRACK_INFO + self._display_connection_info() + + def unload(self) -> None: + self.track_info = EMPTY_TRACK_INFO + self.clear_message() + + def play(self) -> None: + self.player.play() + + def stop(self) -> None: + self.player.stop() + + def pause(self) -> None: + self.player.pause() + + def next(self) -> None: + self.player.next() + + def prev(self) -> None: + self.player.previous() + + # Go to a specific time in a track while playing + def seek(self, ms: int) -> None: + self.message = "NOT SUPPORTED" + self.show_message = True + self.message_timeout = 3000 + + def set_shuffle(self, enabled: bool) -> None: + self.player.set_shuffle(enabled) + + def set_repeat(self, enabled: bool) -> None: + self.player.set_repeat(enabled) + + def eject(self) -> None: + self.message = "NOT SUPPORTED" + self.show_message = True + self.message_timeout = 3000 + + # -------- Status Functions -------- + + def get_postition(self) -> int: + return self.player.position + + def get_shuffle(self) -> bool: + return self.player.shuffle != "off" + + def get_repeat(self) -> bool: + return self.player.repeat != "off" + + # Returns the str representation of PlayerStatus enum + def get_status(self) -> str: + status = PlayerStatus.Idle + btstatus = self.player.status + if btstatus == "playing": + status = PlayerStatus.Playing + if btstatus == "stopped": + status = PlayerStatus.Stopped + if btstatus == "paused": + status = PlayerStatus.Paused + if btstatus == "error": + status = PlayerStatus.Error + if btstatus == "forward-seek": + status = PlayerStatus.Loading + if btstatus == "reverse-seek": + status = PlayerStatus.Loading + return status.value + + def get_track_info(self) -> tuple[int, str, str, str, int, str, int, int]: + return self.track_info + + # Return any message you want to show to the user. tuple with format: (show_message: bool, message: str, message_timeout_ms: int) + def get_message(self) -> tuple[bool, str, int]: + return (self.show_message, self.message, self.message_timeout) + + def clear_message(self) -> None: + self.show_message = False + self.message = '' + self.message_timeout = 0 + + # -------- Events to be called by a timer -------- + + def poll_events(self) -> bool: + was_connected = self.player.connected + self.load() + + # Should tell UI to refresh if we are connected and were not connected before + return self.player.connected and not was_connected diff --git a/python/linamp/cdplayer.py b/python/linamp/cdplayer.py index 4c25797..02785f4 100644 --- a/python/linamp/cdplayer.py +++ b/python/linamp/cdplayer.py @@ -69,7 +69,7 @@ def fetchdata(): if track.get_format() == "data": is_data_tracks[t - i_first_track] = True - musicbrainzngs.set_useragent("Small_diy_cd_player", "0.1") + musicbrainzngs.set_useragent("linamp_cdplayer", "1.0") disc = libdiscid.read(features=libdiscid.FEATURE_READ) # id read try: result = musicbrainzngs.get_releases_by_discid( diff --git a/python/requirements.txt b/python/requirements.txt index 91b114b..a0e3311 100644 --- a/python/requirements.txt +++ b/python/requirements.txt @@ -2,3 +2,4 @@ python-libdiscid==2.0.3 musicbrainzngs==0.7.1 pycdio==2.1.1 python-vlc==3.0.20123 +dbus-next==0.2.3 diff --git a/src/audiosourcebluetooth/audiosourcebluetooth.cpp b/src/audiosourcebluetooth/audiosourcebluetooth.cpp index 6f858d0..8b622ac 100644 --- a/src/audiosourcebluetooth/audiosourcebluetooth.cpp +++ b/src/audiosourcebluetooth/audiosourcebluetooth.cpp @@ -1,207 +1,198 @@ #include "audiosourcebluetooth.h" +//#define DEBUG_ASPY + +#define ASPY_PROGRESS_INTERPOLATION_TIME 100 + AudioSourceBluetooth::AudioSourceBluetooth(QObject *parent) : AudioSourceWSpectrumCapture{parent} { - setupDbusIface(); + auto state = PyGILState_Ensure(); + + // Import 'linamp' python module, see python folder in the root of this repo + PyObject *pModuleName = PyUnicode_DecodeFSDefault("linamp"); + //PyObject *pModuleName = PyUnicode_DecodeFSDefault("linamp-mock"); + playerModule = PyImport_Import(pModuleName); + Py_DECREF(pModuleName); + + if(playerModule == nullptr) { + qDebug() << "Couldn't load python module"; + PyGILState_Release(state); + return; + } + + PyObject *PlayerClass = PyObject_GetAttrString(playerModule, "BTPlayer"); + + if(!PlayerClass || !PyCallable_Check(PlayerClass)) { + qDebug() << "Error getting Player Class"; + PyGILState_Release(state); + return; + } + + player = PyObject_CallNoArgs(PlayerClass); + if(player == nullptr) { + PyErr_Print(); + } + + // Timer to poll for events and load + pollEventsTimer = new QTimer(this); + pollEventsTimer->setInterval(1000); + connect(pollEventsTimer, &QTimer::timeout, this, &AudioSourceBluetooth::pollEvents); + pollEventsTimer->start(); + + // Watch for async events poll results + connect(&pollResultWatcher, &QFutureWatcher::finished, this, &AudioSourceBluetooth::handlePollResult); + + // Handle load end + connect(&loadWatcher, &QFutureWatcher::finished, this, &AudioSourceBluetooth::handleLoadEnd); + + // Handle finish ejecting + connect(&ejectWatcher, &QFutureWatcher::finished, this, &AudioSourceBluetooth::handleEjectEnd); + // Track progress with timer progressRefreshTimer = new QTimer(this); progressRefreshTimer->setInterval(1000); connect(progressRefreshTimer, &QTimer::timeout, this, &AudioSourceBluetooth::refreshProgress); progressInterpolateTimer = new QTimer(this); - progressInterpolateTimer->setInterval(33); + progressInterpolateTimer->setInterval(ASPY_PROGRESS_INTERPOLATION_TIME); connect(progressInterpolateTimer, &QTimer::timeout, this, &AudioSourceBluetooth::interpolateProgress); + + PyGILState_Release(state); } AudioSourceBluetooth::~AudioSourceBluetooth() { } -void AudioSourceBluetooth::handleBtStatusChange(QString status) +void AudioSourceBluetooth::pollEvents() { - if(status == "playing") { - startSpectrum(); - progressRefreshTimer->start(); - progressInterpolateTimer->start(); - emit this->playbackStateChanged(MediaPlayer::PlayingState); - emit this->requestActivation(); // Request audiosource coordinator to select us - } - if(status == "stopped") { - stopSpectrum(); - progressRefreshTimer->stop(); - progressInterpolateTimer->stop(); - emit this->playbackStateChanged(MediaPlayer::StoppedState); - } - if(status == "paused") { - stopSpectrum(); - progressRefreshTimer->stop(); - progressInterpolateTimer->stop(); - emit this->playbackStateChanged(MediaPlayer::PausedState); - } - if(status == "error") { - stopSpectrum(); - progressRefreshTimer->stop(); - progressInterpolateTimer->stop(); - emit this->playbackStateChanged(MediaPlayer::StoppedState); - emit this->messageSet("Bluetooth Error", 5000); + if(pollResultWatcher.isRunning() || loadWatcher.isRunning()) { + #ifdef DEBUG_ASPY + qDebug() << ">>>>>>>>>>>>>>>POLL Avoided"; + #endif + return; } + if(pollInProgress) return; + pollInProgress = true; + #ifdef DEBUG_ASPY + qDebug() << "pollEvents: polling"; + #endif + QFuture status = QtConcurrent::run(&AudioSourceBluetooth::doPollEvents, this); + pollResultWatcher.setFuture(status); } -void AudioSourceBluetooth::handleBtTrackChange(QVariantMap trackData) +void AudioSourceBluetooth::handlePollResult() { - QMediaMetaData metadata; - for( QString trackKey : trackData.keys()){ - qDebug() << " " << trackKey << ":" << trackData.value(trackKey); - if(trackKey == "Title") { - metadata.insert(QMediaMetaData::Title, trackData.value(trackKey)); - } - if(trackKey == "Artist") { - metadata.insert(QMediaMetaData::AlbumArtist, trackData.value(trackKey)); - } - if(trackKey == "Album") { - metadata.insert(QMediaMetaData::AlbumTitle, trackData.value(trackKey)); - } - if(trackKey == "Genre") { - metadata.insert(QMediaMetaData::Genre, trackData.value(trackKey)); - } - if(trackKey == "TrackNumber") { - metadata.insert(QMediaMetaData::TrackNumber, trackData.value(trackKey)); - } - if(trackKey == "Duration") { - quint32 duration = trackData.value(trackKey).toUInt(); - metadata.insert(QMediaMetaData::Duration, duration); - emit this->durationChanged(duration); + #ifdef DEBUG_ASPY + qDebug() << ">>>>POLL RESULT"; + #endif + + bool changeDetected = pollResultWatcher.result(); + if(changeDetected) { + if(loadWatcher.isRunning()) { + #ifdef DEBUG_ASPY + qDebug() << ">>>>>>>>>>>>>>>LOAD Avoided"; + #endif + return; } + emit this->requestActivation(); // Request audiosource coordinator to select us + QFuture status = QtConcurrent::run(&AudioSourceBluetooth::doLoad, this); + loadWatcher.setFuture(status); + } else { + refreshStatus(); + pollInProgress = false; } - emit this->metadataChanged(metadata); + + // Handle messages + this->refreshMessage(); } -void AudioSourceBluetooth::handleBtShuffleChange(QString shuffleSetting) +bool AudioSourceBluetooth::doPollEvents() { - if(shuffleSetting == "off") { - emit this->shuffleEnabledChanged(false); - this->isShuffleEnabled = false; + bool changeDetected = false; + if(player == nullptr) return changeDetected; + + auto state = PyGILState_Ensure(); + PyObject* pyChangeDetected = PyObject_CallMethod(player, "poll_events", NULL); + + if(PyBool_Check(pyChangeDetected)) { + changeDetected = PyObject_IsTrue(pyChangeDetected); + #ifdef DEBUG_ASPY + qDebug() << ">>>Change detected?:" << changeDetected; + #endif } else { - emit this->shuffleEnabledChanged(true); - this->isShuffleEnabled = true; + #ifdef DEBUG_ASPY + qDebug() << ">>>>pollEvents: Not a bool"; + #endif } + if(pyChangeDetected) Py_DECREF(pyChangeDetected); + PyGILState_Release(state); + return changeDetected; } -void AudioSourceBluetooth::handleBtRepeatChange(QString repeatSetting) +void AudioSourceBluetooth::doLoad() { - if(repeatSetting == "off") { - emit this->repeatEnabledChanged(false); - this->isRepeatEnabled = false; - } else { - emit this->repeatEnabledChanged(true); - this->isRepeatEnabled = true; - } + auto state = PyGILState_Ensure(); + PyObject_CallMethod(player, "load", NULL); + PyGILState_Release(state); } -void AudioSourceBluetooth::handleBtPositionChange(quint32 position) +void AudioSourceBluetooth::handleLoadEnd() { - this->currentProgress = position; - emit this->positionChanged(position); + emit this->messageClear(); + refreshStatus(); + pollInProgress = false; } -void AudioSourceBluetooth::handleBtPropertyChange(QString name, QVariantMap map, QStringList list) +void AudioSourceBluetooth::doEject() { - qDebug() << QString("properties of interface %1 changed").arg(name); - for (QVariantMap::const_iterator it = map.cbegin(), end = map.cend(); it != end; ++it) { - qDebug() << "property: " << it.key() << " value: " << it.value(); - - QString prop = it.key(); - - if(prop == "Status") { - QString status = it.value().toString(); - this->handleBtStatusChange(status); - } - - if(prop == "Track") { - QVariantMap trackData = qdbus_cast(it.value()); - this->handleBtTrackChange(trackData); - } - - if(prop == "Repeat") { - QString repeatSetting = it.value().toString(); - this->handleBtRepeatChange(repeatSetting); - } - - if(prop == "Shuffle") { - QString shuffleSetting = it.value().toString(); - this->handleBtShuffleChange(shuffleSetting); - } - - if(prop == "Position") { - quint32 pos = it.value().toUInt(); - this->handleBtPositionChange(pos); - } - - } - for (const auto& element : list) { - qDebug() << "list element: " << element; - } + auto state = PyGILState_Ensure(); + PyObject_CallMethod(player, "eject", NULL); + PyGILState_Release(state); } -void AudioSourceBluetooth::fetchBtMetadata() +void AudioSourceBluetooth::handleEjectEnd() { - if(!isDbusReady()) { - return; - } - - QVariant status = this->dbusIface->property("Status"); - if(!status.isNull()) { - qDebug() << "<<<handleBtStatusChange(status.toString()); - } - - QVariant repeat = this->dbusIface->property("Repeat"); - if(!repeat.isNull()) { - qDebug() << "<<<handleBtRepeatChange(repeat.toString()); - } - - QVariant shuffle = this->dbusIface->property("Shuffle"); - if(!shuffle.isNull()) { - qDebug() << "<<<handleBtShuffleChange(shuffle.toString()); - } - - QVariant track = this->dbusIface->property("Track"); - if(!track.isNull()) { - qDebug() << "<<<(track); - this->handleBtTrackChange(trackData); - } - - this->refreshProgress(); + emit this->messageClear(); + // Empty metadata + QMediaMetaData metadata; + currentMetadata = metadata; + refreshStatus(); } + void AudioSourceBluetooth::activate() { - QMediaMetaData metadata = QMediaMetaData{}; - metadata.insert(QMediaMetaData::Title, "Bluetooth"); - emit playbackStateChanged(MediaPlayer::StoppedState); emit positionChanged(0); - emit metadataChanged(metadata); emit durationChanged(0); emit eqEnabledChanged(false); emit plEnabledChanged(false); emit shuffleEnabledChanged(false); emit repeatEnabledChanged(false); - this->isShuffleEnabled = false; - this->isRepeatEnabled = false; + refreshStatus(); + refreshTrackInfo(true); + // Poll status + progressRefreshTimer->start(); - fetchBtMetadata(); + isActive = true; } void AudioSourceBluetooth::deactivate() { + isActive = false; + + progressRefreshTimer->stop(); stopSpectrum(); emit playbackStateChanged(MediaPlayer::StoppedState); + emit this->messageClear(); + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + PyObject_CallMethod(player, "stop", NULL); + PyGILState_Release(state); + } void AudioSourceBluetooth::handlePl() @@ -211,127 +202,382 @@ void AudioSourceBluetooth::handlePl() void AudioSourceBluetooth::handlePrevious() { - dbusCall("Previous"); + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + PyObject *pyResult = PyObject_CallMethod(player, "prev", NULL); + if(pyResult == nullptr) { + PyErr_Print(); + } + PyGILState_Release(state); + refreshStatus(); } void AudioSourceBluetooth::handlePlay() { - //startSpectrum(); - progressRefreshTimer->start(); - progressInterpolateTimer->start(); - dbusCall("Play"); - emit playbackStateChanged(MediaPlayer::PlayingState); - fetchBtMetadata(); + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + PyObject *pyResult = PyObject_CallMethod(player, "play", NULL); + if(pyResult == nullptr) { + PyErr_Print(); + } + PyGILState_Release(state); + refreshStatus(); } void AudioSourceBluetooth::handlePause() { - stopSpectrum(); - progressRefreshTimer->stop(); - progressInterpolateTimer->stop(); - dbusCall("Pause"); - emit playbackStateChanged(MediaPlayer::PausedState); + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + PyObject *pyResult = PyObject_CallMethod(player, "pause", NULL); + if(pyResult == nullptr) { + PyErr_Print(); + } + PyGILState_Release(state); + refreshStatus(); } void AudioSourceBluetooth::handleStop() { - stopSpectrum(); - progressRefreshTimer->stop(); - progressInterpolateTimer->stop(); - dbusCall("Stop"); - emit playbackStateChanged(MediaPlayer::StoppedState); + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + PyObject *pyResult = PyObject_CallMethod(player, "stop", NULL); + if(pyResult == nullptr) { + PyErr_Print(); + } + PyGILState_Release(state); + refreshStatus(); } void AudioSourceBluetooth::handleNext() { - dbusCall("Next"); + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + PyObject *pyResult = PyObject_CallMethod(player, "next", NULL); + if(pyResult == nullptr) { + PyErr_Print(); + } + PyGILState_Release(state); + refreshStatus(); } void AudioSourceBluetooth::handleOpen() { - + if(player == nullptr) return; + #ifdef DEBUG_ASPY + qDebug() << "<<<<>>>>>>>>>>>>>>EJECT Avoided"; + #endif + return; + } + QFuture status = QtConcurrent::run(&AudioSourceBluetooth::doEject, this); + ejectWatcher.setFuture(status); } void AudioSourceBluetooth::handleShuffle() { + if(player == nullptr) return; + this->isShuffleEnabled = !this->isShuffleEnabled; - emit shuffleEnabledChanged(this->isShuffleEnabled); - if(isDbusReady()) { - this->dbusIface->setShuffle(this->isShuffleEnabled ? "alltracks" : "off"); - } + + auto state = PyGILState_Ensure(); + PyObject_CallMethod(player, "set_shuffle", "i", this->isShuffleEnabled); + PyGILState_Release(state); + + refreshStatus(false); } void AudioSourceBluetooth::handleRepeat() { + if(player == nullptr) return; + this->isRepeatEnabled = !this->isRepeatEnabled; - emit repeatEnabledChanged(this->isRepeatEnabled); - if(isDbusReady()) { - this->dbusIface->setRepeat(this->isRepeatEnabled ? "alltracks" : "off"); - } + + auto state = PyGILState_Ensure(); + PyObject_CallMethod(player, "set_repeat", "i", this->isRepeatEnabled); + PyGILState_Release(state); + + refreshStatus(false); } void AudioSourceBluetooth::handleSeek(int mseconds) { + if(player == nullptr) return; + + auto state = PyGILState_Ensure(); + PyObject_CallMethod(player, "seek", "l", mseconds); + PyGILState_Release(state); + refreshStatus(false); } -void AudioSourceBluetooth::refreshProgress() +void AudioSourceBluetooth::refreshStatus(bool shouldRefreshTrackInfo) { - QVariant position = this->dbusIface->property("Position"); - if(!position.isNull()) { - this->handleBtPositionChange(position.toUInt()); + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + + // Get shuffle status + PyObject *pyShuffleEnabled = PyObject_CallMethod(player, "get_shuffle", NULL); + if(PyBool_Check(pyShuffleEnabled)) { + this->isShuffleEnabled = PyObject_IsTrue(pyShuffleEnabled); + emit shuffleEnabledChanged(this->isShuffleEnabled); } -} + Py_DECREF(pyShuffleEnabled); -void AudioSourceBluetooth::interpolateProgress() -{ - this->currentProgress += 33; - emit this->positionChanged(this->currentProgress); + // Get repeat status + PyObject *pyRepeatEnabled = PyObject_CallMethod(player, "get_repeat", NULL); + if(PyBool_Check(pyRepeatEnabled)) { + this->isRepeatEnabled = PyObject_IsTrue(pyRepeatEnabled); + emit repeatEnabledChanged(this->isRepeatEnabled); + } + Py_DECREF(pyRepeatEnabled); + + PyObject *pyStatus = PyObject_CallMethod(player, "get_status", NULL); + if(pyStatus == nullptr) { + PyGILState_Release(state); + return; + } + QString status(PyUnicode_AsUTF8(pyStatus)); + Py_DECREF(pyStatus); + + PyGILState_Release(state); + + #ifdef DEBUG_ASPY + qDebug() << ">>>Status" << status; + #endif + + if(status == "idle") { + emit playbackStateChanged(MediaPlayer::StoppedState); + emit positionChanged(0); + emit durationChanged(0); + + if(isActive) { + progressInterpolateTimer->stop(); + stopSpectrum(); + } + } + + if(status == "stopped") { + emit playbackStateChanged(MediaPlayer::StoppedState); + emit positionChanged(0); + + if(isActive) { + progressInterpolateTimer->stop(); + stopSpectrum(); + } + } + + if(status == "playing") { + emit this->messageClear(); + emit playbackStateChanged(MediaPlayer::PlayingState); + + if(isActive) { + progressInterpolateTimer->start(); + startSpectrum(); + } + } + + if(status == "paused") { + emit this->messageClear(); + emit playbackStateChanged(MediaPlayer::PausedState); + + if(isActive) { + progressInterpolateTimer->stop(); + stopSpectrum(); + } + } + + if(status == "loading") { + emit playbackStateChanged(MediaPlayer::StoppedState); + emit positionChanged(0); + + if(isActive) { + progressInterpolateTimer->stop(); + stopSpectrum(); + } + + emit this->messageSet("LOADING...", 3000); + } + + if(status == "error") { + emit playbackStateChanged(MediaPlayer::StoppedState); + emit positionChanged(0); + + if(isActive) { + progressInterpolateTimer->stop(); + stopSpectrum(); + } + + emit this->messageSet("ERROR", 5000); + } + + this->currentStatus = status; + + if(status != "idle" && shouldRefreshTrackInfo) { + refreshTrackInfo(); + } } -QString AudioSourceBluetooth::findDbusMediaObjPath() +void AudioSourceBluetooth::refreshTrackInfo(bool force) { + #ifdef DEBUG_ASPY + qDebug() << ">>>>>>>>>Refresh track info"; + #endif + if(player == nullptr) return; + auto state = PyGILState_Ensure(); + PyObject *pyTrackInfo = PyObject_CallMethod(player, "get_track_info", NULL); + if(pyTrackInfo == nullptr) { + #ifdef DEBUG_ASPY + qDebug() << ">>> Couldn't get track info"; + #endif + PyErr_Print(); + PyGILState_Release(state); + return; + } + // format (tracknumber: int, artist, album, title, duration: int, is_data_track: bool) + PyObject *pyTrackNumber = PyTuple_GetItem(pyTrackInfo, 0); + PyObject *pyArtist = PyTuple_GetItem(pyTrackInfo, 1); + PyObject *pyAlbum = PyTuple_GetItem(pyTrackInfo, 2); + PyObject *pyTitle = PyTuple_GetItem(pyTrackInfo, 3); + PyObject *pyDuration = PyTuple_GetItem(pyTrackInfo, 4); + PyObject *pyCodec = PyTuple_GetItem(pyTrackInfo, 5); + PyObject *pyBitrate = PyTuple_GetItem(pyTrackInfo, 6); + PyObject *pySampleRate = PyTuple_GetItem(pyTrackInfo, 7); + + quint32 trackNumber = PyLong_AsLong(pyTrackNumber); + quint32 duration = PyLong_AsLong(pyDuration); + QString title(PyUnicode_AsUTF8(pyTitle)); + + bool isSameTrack = this->currentMetadata.value(QMediaMetaData::TrackNumber).toInt() == trackNumber && + this->currentMetadata.value(QMediaMetaData::Title).toString() == title && + this->currentMetadata.value(QMediaMetaData::Duration).toInt() == duration; + + if(isSameTrack && !force) { + // No need to refresh + Py_DECREF(pyTrackInfo); + PyGILState_Release(state); + return; + } + + QString artist(PyUnicode_AsUTF8(pyArtist)); + QString album(PyUnicode_AsUTF8(pyAlbum)); + QString codec(PyUnicode_AsUTF8(pyCodec)); + quint32 bitrate = PyLong_AsLong(pyBitrate); + quint32 sampleRate = PyLong_AsLong(pySampleRate); + + QMediaMetaData metadata; + metadata.insert(QMediaMetaData::Title, title); + metadata.insert(QMediaMetaData::AlbumArtist, artist); + metadata.insert(QMediaMetaData::AlbumTitle, album); + metadata.insert(QMediaMetaData::TrackNumber, trackNumber); + metadata.insert(QMediaMetaData::Duration, duration); + metadata.insert(QMediaMetaData::AudioBitRate, bitrate); + metadata.insert(QMediaMetaData::Comment, QString::number(sampleRate)); // Using Comment as sample rate + metadata.insert(QMediaMetaData::Description, codec); // Using Description as codec + + this->currentMetadata = metadata; + emit this->durationChanged(duration); + emit this->metadataChanged(metadata); + + #ifdef DEBUG_ASPY + qDebug() << ">>>>>>>>METADATA changed"; + #endif + + Py_DECREF(pyTrackInfo); + + PyGILState_Release(state); } -bool AudioSourceBluetooth::setupDbusIface() +void AudioSourceBluetooth::refreshProgress() { - // Init dbus - auto dbusConn = QDBusConnection::systemBus(); - if(!dbusConn.isConnected()) { - qDebug() << "Cannot connect to DBuss"; - return false; - } else { - qDebug() << "CONNECTED!"; - } + if(player == nullptr) return; - this->dbusIface = new BluezMediaInterface(SERVICE_NAME, OBJ_PATH, dbusConn, this); - if(!this->dbusIface->isValid()) { - qDebug() << "DBus interface is invalid"; - return false; - } + refreshStatus(); - bool success = dbusConn.connect(SERVICE_NAME, OBJ_PATH, "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(handleBtPropertyChange(QString, QVariantMap, QStringList))); + auto state = PyGILState_Ensure(); - if(success) { - qDebug() << "SUCCESS!"; - } else { - qDebug() << "MEH"; + PyObject *pyPosition = PyObject_CallMethod(player, "get_postition", NULL); + if(pyPosition == nullptr) { + #ifdef DEBUG_ASPY + qDebug() << ">>> Couldn't get track position"; + #endif + PyErr_Print(); + PyGILState_Release(state); + return; } - return success; + if(PyLong_Check(pyPosition)) { + quint32 position = PyLong_AsLong(pyPosition); + + int diff = (int)this->currentProgress - (int)position; + #ifdef DEBUG_ASPY + qDebug() << ">>>>Time diff" << diff; + #endif + + // Avoid small jumps caused by the python method latency + if(abs(diff) > 1000) { + this->currentProgress = position; + emit this->positionChanged(this->currentProgress); + } + } + Py_DECREF(pyPosition); + + PyGILState_Release(state); } -bool AudioSourceBluetooth::isDbusReady() +void AudioSourceBluetooth::interpolateProgress() { - if(this->dbusIface == nullptr) { - return false; + if(!progressInterpolateElapsedTimer.isValid()) { + // Handle first time + progressInterpolateElapsedTimer.start(); + return; } - return this->dbusIface->isValid(); + qint64 elapsed = progressInterpolateElapsedTimer.elapsed(); + if(elapsed > 200) { + // Handle invalid interpolations + progressInterpolateElapsedTimer.start(); + return; + } + this->currentProgress += elapsed; + progressInterpolateElapsedTimer.start(); + emit this->positionChanged(this->currentProgress); } -void AudioSourceBluetooth::dbusCall(QString method) +void AudioSourceBluetooth::refreshMessage() { - if(isDbusReady()) { - this->dbusIface->call(method); + if(player == nullptr){ + qDebug() << "<<>> Couldn't get track message data"; + #endif + PyErr_Print(); + PyGILState_Release(state); + return; } + + // format (show_message: bool, message: str, message_timeout_ms: int) + PyObject *pyShowMessage = PyTuple_GetItem(pyMessageData, 0); + PyObject *pyMessage = PyTuple_GetItem(pyMessageData, 1); + PyObject *pyMessageTimeout = PyTuple_GetItem(pyMessageData, 2); + + quint32 showMessage = PyLong_AsLong(pyShowMessage); + QString message(PyUnicode_AsUTF8(pyMessage)); + quint32 messageTimeout = PyLong_AsLong(pyMessageTimeout); + + if(showMessage) { + PyObject_CallMethod(player, "clear_message", NULL); + emit this->messageSet(message, messageTimeout); + } + + Py_DECREF(pyMessageData); + PyGILState_Release(state); } diff --git a/src/audiosourcebluetooth/audiosourcebluetooth.h b/src/audiosourcebluetooth/audiosourcebluetooth.h index 3cfc271..69008cb 100644 --- a/src/audiosourcebluetooth/audiosourcebluetooth.h +++ b/src/audiosourcebluetooth/audiosourcebluetooth.h @@ -1,76 +1,17 @@ #ifndef AUDIOSOURCEBLUETOOTH_H #define AUDIOSOURCEBLUETOOTH_H +#define PY_SSIZE_T_CLEAN +#undef slots +#include +#define slots Q_SLOTS + #include #include -#include -#include -#include -#include +#include #include "audiosourcewspectrumcapture.h" -#define SERVICE_NAME "org.bluez" -#define OBJ_PATH "/org/bluez/hci0/dev_5C_70_17_02_D7_6E/player2" -#define OBJ_INTERFACE "org.bluez.MediaPlayer1" - -class BluezMediaInterface : public QDBusAbstractInterface -{ - Q_OBJECT - -public: - static inline const char *staticInterfaceName() - { return OBJ_INTERFACE; } - -public: - BluezMediaInterface(const QString &service, const QString &path, const QDBusConnection &connection, - QObject *parent = nullptr): - QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent) {} - - virtual ~BluezMediaInterface() {} - - Q_PROPERTY(QVariantMap Track READ track) - QVariantMap track() const - { - return qvariant_cast(property("Track")); - } - - Q_PROPERTY(QString Status READ status) - QString status() const - { - return qvariant_cast(property("Status")); - } - - Q_PROPERTY(QString Repeat READ repeat WRITE setRepeat) - QString repeat() const - { - return qvariant_cast(property("Repeat")); - } - void setRepeat(QString value) - { - setProperty("Repeat", value); - } - - Q_PROPERTY(QString Shuffle READ shuffle WRITE setShuffle) - QString shuffle() const - { - return qvariant_cast(property("Shuffle")); - } - void setShuffle(QString value) - { - setProperty("Shuffle", value); - } - - Q_PROPERTY(quint32 Position READ position) - quint32 position() const - { - return qvariant_cast(property("Position")); - } - -Q_SIGNALS: - void PropertiesChanged(const QVariantMap &properties); -}; - class AudioSourceBluetooth : public AudioSourceWSpectrumCapture { Q_OBJECT @@ -93,33 +34,45 @@ public slots: void handleSeek(int mseconds); private: - BluezMediaInterface *dbusIface = nullptr; + bool isActive = false; + + PyObject *playerModule; + PyObject *player; QTimer *progressRefreshTimer = nullptr; QTimer *progressInterpolateTimer = nullptr; + QElapsedTimer progressInterpolateElapsedTimer; quint32 currentProgress = 0; void refreshProgress(); void interpolateProgress(); + // Poll events thread + QTimer *pollEventsTimer = nullptr; + bool pollInProgress = false; + void pollEvents(); + bool doPollEvents(); + void handlePollResult(); + QFutureWatcher pollResultWatcher; + + // Load details thread + void doLoad(); + void handleLoadEnd(); + QFutureWatcher loadWatcher; + + // Eject thread + void doEject(); + void handleEjectEnd(); + QFutureWatcher ejectWatcher; + + QString currentStatus; // Status as it comes from python bool isShuffleEnabled = false; bool isRepeatEnabled = false; - void fetchBtMetadata(); - - // DBus connection utils - QString findDbusMediaObjPath(); // returns empty string if not found - bool setupDbusIface(); - bool isDbusReady(); - void dbusCall(QString method); - + void refreshStatus(bool shouldRefreshTrackInfo = true); + void refreshTrackInfo(bool force = false); + void refreshMessage(); -private slots: - void handleBtPropertyChange(QString name, QVariantMap map, QStringList list); - void handleBtStatusChange(QString status); - void handleBtTrackChange(QVariantMap trackData); - void handleBtShuffleChange(QString shuffleSetting); - void handleBtRepeatChange(QString repeatSetting); - void handleBtPositionChange(quint32 position); + QMediaMetaData currentMetadata; }; #endif // AUDIOSOURCEBLUETOOTH_H diff --git a/src/audiosourcecd/audiosourcecd.cpp b/src/audiosourcecd/audiosourcecd.cpp index 6942a17..687aab5 100644 --- a/src/audiosourcecd/audiosourcecd.cpp +++ b/src/audiosourcecd/audiosourcecd.cpp @@ -7,10 +7,6 @@ AudioSourceCD::AudioSourceCD(QObject *parent) : AudioSourceWSpectrumCapture{parent} { - Py_Initialize(); - // PyEval_InitThreads(); - PyEval_SaveThread(); - auto state = PyGILState_Ensure(); // Import 'linamp' python module, see python folder in the root of this repo @@ -64,8 +60,6 @@ AudioSourceCD::AudioSourceCD(QObject *parent) AudioSourceCD::~AudioSourceCD() { - PyGILState_Ensure(); - Py_Finalize(); } void AudioSourceCD::pollDetectDiscInsertion() @@ -452,7 +446,7 @@ void AudioSourceCD::refreshTrackInfo(bool force) metadata.insert(QMediaMetaData::TrackNumber, trackNumber); metadata.insert(QMediaMetaData::Duration, duration); metadata.insert(QMediaMetaData::AudioBitRate, 1411 * 1000); - metadata.insert(QMediaMetaData::AudioCodec, 44100); // Using AudioCodec as sample rate for now + metadata.insert(QMediaMetaData::Comment, "44100"); // Using Comment as sample rate this->currentTrackNumber = trackNumber; emit this->durationChanged(duration); diff --git a/src/shared/util.cpp b/src/shared/util.cpp index 4a8ed3e..727b9c8 100644 --- a/src/shared/util.cpp +++ b/src/shared/util.cpp @@ -48,7 +48,7 @@ QMediaMetaData parseMetaData(const QUrl &url) qint64 sampleRate = properties->sampleRate(); metadata.insert(QMediaMetaData::AudioBitRate, bitrate); - metadata.insert(QMediaMetaData::AudioCodec, sampleRate); // Using AudioCodec as sample rate for now + metadata.insert(QMediaMetaData::Comment, QString::number(sampleRate)); // Using Comment as sample rate metadata.insert(QMediaMetaData::Duration, duration); } diff --git a/src/view-basewindow/mainwindow.cpp b/src/view-basewindow/mainwindow.cpp index 577be2d..9b54b27 100644 --- a/src/view-basewindow/mainwindow.cpp +++ b/src/view-basewindow/mainwindow.cpp @@ -27,9 +27,18 @@ const unsigned int WINDOW_W = 277 * UI_SCALE; const unsigned int WINDOW_H = 117 * UI_SCALE; #endif +#define PY_SSIZE_T_CLEAN +#undef slots +#include +#define slots Q_SLOTS + MainWindow::MainWindow(QWidget *parent) : QMainWindow{parent} { + // Initialize Python interpreter, required by audiosourcecd and audiosourcebt + Py_Initialize(); + PyEval_SaveThread(); + // Setup playlist m_playlistModel = new PlaylistModel(this); m_playlist = m_playlistModel->playlist(); @@ -131,7 +140,8 @@ MainWindow::MainWindow(QWidget *parent) MainWindow::~MainWindow() { - + PyGILState_Ensure(); + Py_Finalize(); } void MainWindow::showPlayer() diff --git a/src/view-player/playerview.cpp b/src/view-player/playerview.cpp index c6abd4f..c765b17 100644 --- a/src/view-player/playerview.cpp +++ b/src/view-player/playerview.cpp @@ -152,7 +152,7 @@ void PlayerView::scale() QRect psiGeo = ui->playStatusIcon->geometry(); ui->playStatusIcon->setGeometry(psiGeo.x()*UI_SCALE, psiGeo.y()*UI_SCALE, psiGeo.width(), psiGeo.height()); - ui->progressTimeLabel->setGeometry(39*UI_SCALE, 3*UI_SCALE, 50*UI_SCALE, 20*UI_SCALE); + ui->progressTimeLabel->setGeometry(35*UI_SCALE, 3*UI_SCALE, 54*UI_SCALE, 20*UI_SCALE); QFont ptlFont = ui->progressTimeLabel->font(); ptlFont.setWordSpacing(-2); ui->progressTimeLabel->setFont(ptlFont); @@ -233,11 +233,25 @@ void PlayerView::setMetadata(QMediaMetaData metadata) setTrackInfo(trackInfo); // Set kbps - int bitrate = metadata.value(QMediaMetaData::AudioBitRate).toInt()/1000; - ui->kbpsValueLabel->setText(bitrate > 0 ? QString::number(bitrate) : ""); + int bitrate = metadata.value(QMediaMetaData::AudioBitRate).toInt(); + QString codec = metadata.value(QMediaMetaData::Description).toString(); // Using Description as codec + if(bitrate > 0) { + bitrate = bitrate/1000; + ui->kbpsLabel->setText("kbps"); + ui->kbpsValueLabel->setText(QString::number(bitrate)); + } else if(codec.length()) { + // No bitrate, try to use codec + ui->kbpsLabel->setText("codec"); + ui->kbpsValueLabel->setText(codec); + } else { + // Keep blank + ui->kbpsLabel->setText("kbps"); + ui->kbpsValueLabel->setText(""); + } + // Set kHz - int khz = metadata.value(QMediaMetaData::AudioCodec).toInt()/1000; + int khz = metadata.value(QMediaMetaData::Comment).toString().toInt()/1000; ui->khzValueLabel->setText(khz > 0 ? QString::number(khz) : ""); }