musicmuster/app/music.py
2022-03-02 09:27:10 +00:00

181 lines
4.6 KiB
Python

import os
import threading
import vlc
from config import Config
from datetime import datetime
from time import sleep
from log import DEBUG, ERROR
lock = threading.Lock()
class Music:
"""
Manage the playing of music tracks
"""
def __init__(self):
self.current_track_start_time = None
self.fading = 0
self.VLC = vlc.Instance()
self.player = None
self.track_path = None
self.max_volume = Config.VOLUME_VLC_DEFAULT
def fade(self):
"""
Fade the currently playing track.
The actual management of fading runs in its own thread so as not
to hold up the UI during the fade.
"""
DEBUG("music.fade()", True)
if not self.player:
return
if not self.player.get_position() > 0 and self.player.is_playing():
return
self.fading += 1
thread = threading.Thread(target=self._fade)
thread.start()
def _fade(self):
"""
Implementation of fading the current track in a separate thread.
"""
# Take a copy of current player to allow another track to be
# started without interfering here
DEBUG(f"music._fade(), {self.player=}", True)
with lock:
p = self.player
self.player = None
DEBUG("music._fade() post-lock", True)
fade_time = Config.FADE_TIME / 1000
steps = Config.FADE_STEPS
sleep_time = fade_time / steps
# We reduce volume by one mesure first, then by two measures,
# then three, and so on.
# The sum of the arithmetic sequence 1, 2, 3, ..n is
# (n**2 + n) / 2
total_measures_count = (steps**2 + steps) / 2
measures_to_reduce_by = 0
for i in range(1, steps + 1):
measures_to_reduce_by += i
volume_factor = 1 - (
measures_to_reduce_by / total_measures_count)
p.audio_set_volume(int(self.max_volume * volume_factor))
sleep(sleep_time)
with lock:
DEBUG(f"music._facde(), stopping {p=}", True)
p.stop()
DEBUG(f"Releasing player {p=}", True)
p.release()
# Ensure we don't reference player after release
p = None
self.fading -= 1
def get_playtime(self):
"""Return elapsed play time"""
with lock:
if not self.player:
return None
return self.player.get_time()
def get_position(self):
"""Return current position"""
with lock:
DEBUG("music.get_position", True)
return self.player.get_position()
def play(self, path):
"""
Start playing the track at path.
Log and return if path not found.
"""
DEBUG(f"music.play({path=})", True)
if not os.access(path, os.R_OK):
ERROR(f"play({path}): path not found")
return
self.track_path = path
self.player = self.VLC.media_player_new(path)
self.player.audio_set_volume(self.max_volume)
DEBUG(f"music.play({path=}), {self.player}", True)
self.player.play()
self.current_track_start_time = datetime.now()
def playing(self):
"""
Return True if currently playing a track, else False
vlc.is_playing() returns True if track was faded out.
get_position seems more reliable.
"""
with lock:
if self.player:
if self.player.get_position() > 0 and self.player.is_playing():
return True
# We take a copy of the player when fading, so we could be
# playing in a fade nowFalse
return self.fading > 0
def set_position(self, ms):
"""Set current play time in milliseconds from start"""
with lock:
return self.player.set_time(ms)
def set_volume(self, volume):
"""Set maximum volume used for player"""
with lock:
if not self.player:
return
self.max_volume = volume
self.player.audio_set_volume(volume)
def stop(self):
"""Immediately stop playing"""
DEBUG(f"music.stop(), {self.player=}", True)
with lock:
if not self.player:
return
position = self.player.get_position()
self.player.stop()
DEBUG(f"music.stop(): Releasing player {self.player=}", True)
self.player.release()
# Ensure we don't reference player after release
self.player = None
return position