def queue(self, track, i=-1, _ready_barrier=mp.Barrier(1)): # Pass parent track.channel = self track._ready_barrier = _ready_barrier # To synchronize with main thread # Set mono/stereo if not track.initially_mono and self._channel_count == 1: tracks = track.track.split_to_mono() track.track = tracks[0].overlay(tracks[1]).set_frame_rate( track.track.frame_rate * 2 ) # Number of frames doubles in the unification, and that has some side effects else: track.track = track.track.set_channels(self._channel_count) # COMPRESSION IS TOO EXPENSIVE #print('comp') # Compression #if self.compression: # track.track = compress_dynamic_range(track.track, **self.comp_args) #print('excomp') # Queue if i == -1: self._queue.append(track) else: self._queue.insert(i, track) print('Initializing %s...' % track) track.procinit() self.update() freed = sizemb(track.track._data) del track.track # The only other place this is used should be in subprocesses, which have already copied the data over... therefore this is wasted RAM, and it is LARGE print('Freed %.2f MB of copied RAM.' % freed)
def execute_commands_loop(): while True: string = EXECUTOR_QUEUE.get() # blocks print('Command received:', '[ '+string+' ]') try: exec(string, {**builtins.__dict__, **userfunctions.__dict__}, cm.locals) except Exception as e: print('Timed command failed:', e)
def play(self): if self.empty: return print('releasing restart_lock') # Must call renew() if playing multiple times due to threads being unable to restart # print(self.restart_lock) try: self.restart_lock.release() except ValueError: print('please reset the track before playing it again')
def _renew(self): if self.old: self.old.set(False) self.queue_index.set(0) self.temp_start.set(0) self.temp_end.set(int(self.length * 1000)) self.play_time.set(0) print('resetting track') self.playing.set(False) self.restart_lock.acquire(False)
def procloop(self, exec_queue, stdout): sys.stdout = sys.stderr = stdout print('Subprocess for %s started.' % self) stream = PA.open( format=PA.get_format_from_width(self.sample_width), channels=self.track.channels, rate=self.track.frame_rate, output=True, # Audio out output_device_index=self.channel.device) data_stream = None if self.streaming: print('Freed another %.2f MB by streaming.' % (sizemb(self.track.raw_data))) data_stream = streaming.AudioStream(self.track, self.name) print('STREAMING', self.name) del self.track #print(sys.getsizeof(data_stream.__dict__), sys.getsizeof(data_stream.audio.__dict__), data_stream.audio.__dict__) data_stream.load_ms(0, 2000) stream_queue = Queue(1) self.player_thread = Thread(target=self._write_to_stream, args=(stream, stream_queue), daemon=True) self.player_thread.start() try: print('%s on standby' % self) self._ready_barrier.wait( ) # main should be the last to the barrier, unless we don't care about waiting for proc to be ready extras.testmem() while True: self.restart_lock.acquire(True) self._play(stream, stream_queue, exec_queue, data_stream) self._renew() print('%s on standby' % self) finally: stream_queue.put(None) stream.close()
def generate_views(self): print(self.channels) for name, c in self.channels.items(): self.views[name] = v = views.ChannelView(self.app, c, self._slots[name]) self.app.track(v)
def load_tracks(self, data, loading_notifier=NonceVar()): # loading_notifier is a StringVar, usually, for a loading screen, because loading tracks fresh can take a sec; NonceVar mimics StringVar interface cache = Path(self.app.DIR, 'yt_cache') urls = json.load(open(cache + 'urls.json')) tracks_per_channel = {} for channel in self.channels.values(): if listing := data.get(channel.name): l = len(listing) track_list = [] for i, track in enumerate(listing): YT = track.get('url') cache_fp = None i += 1 if YT: # YOUTUBE DOWNLOADS WORK AHAHAHAHAHA IM SO HAPPY url = track['url'].replace('music.', '') video_id = parse_qs(urlparse(url).query).get( 'v', (url, ) )[0] # this can take youtube music, youtube, youtu.be, http://, and even just the video code that comes after v= url_short = url # Shortened for visual appeal in the loading screen if len(url) > 41: url_short = url[:19] + '...' + url[-19:] loading_notifier.set( f'[{channel.name}] Resolving \n {url_short} \n ({i}/{l})' ) self.app.root.update() print('Resolving', url) vid = None if video_id in urls: # We keep urls stored with their video titles in case we have them; cuts the 2 seconds required for pafy.new() name = urls[video_id] else: # If it's not listed then that's very suspicious, probably not cached, so we resolve it ourselves try: vid = pafy.new(video_id) except: continue urls[video_id] = name = sanitize(vid.title) wave_path = Path(self.app.DIR, 'wave_cache', name + '.wav').path if os.path.exists(wave_path): # Now actually check if we have it cached. # If you clear urls.json but still have the wav hanging around this will save you, # otherwise it's a redundant check track['file'] = wave_path print('Not downloading, found a cached copy.') else: # But if not cached, gotta download! if vid is None: try: vid = pafy.new(video_id) except: continue loading_notifier.set( f'[{channel.name}]\nDownloading "{name}" from YouTube ({i}/{l})' ) self.app.root.update() print('Downloading audio...') print(vid) aud = vid.getbestaudio() # mod this # download it to the yt_cache cache_fp = cache + (sanitize(vid.title) + '.' + aud.extension) aud.download(cache_fp) print('DOWNLOADED:', cache_fp) track['file'] = cache_fp if not track.get('name'): # Give it a name track['name'] = name + ' (YT)' # not a Track() arg, and videos should have been downloaded... if 'url' in track: del track['url'] # Handles general files, as well as the downloaded YouTube audio name = track.get('name', track['file']) loading_notifier.set( f'[{channel.name}]\nLoading {name} ({i}/{l})') self.app.root.update() # Generate track and queue it self.track_dict[name] = t = audio.Track( **track, chunk_override=audio.Track.CHUNK ) # track_dict used for autofollow track_list.append(t) # yt_cache is EVANESCENT if cache_fp: os.remove(cache_fp) # Fix up autofollows and at_times print('Making autofollows...') loading_notifier.set( f'[{channel.name}]\nRendering autofollows...') self.app.root.update() tracks_to_remove = [] for track in track_list: for at_data in track.at_time_mods: # [5.0, "print('hello')"] track.at_time(*at_data) for af_name in track.auto_follow_mods: print('AUTOFOLLOW:', track, '+', af_name) # "track name" if not (af_track := self.track_dict.get(af_name)): print( f'Couldn\'t find track "{af_name}" for autofollow' ) continue #print('AUTOFOLLOW 2:', af_track) track.autofollow(af_track) # inherit timed commands track.at_time_mods += [ (t + af_track.length * 1000, f) for t, f in af_track.at_time_mods ] tracks_to_remove.append(af_track) # Remove auto'd tracks from the queue since they attach to the parent # REMOVE autofollowed etc. for track in tracks_to_remove: track_list.remove(track) # We'll queue in a hot sec tracks_per_channel[channel] = track_list
tracks_to_remove.append(af_track) # Remove auto'd tracks from the queue since they attach to the parent # REMOVE autofollowed etc. for track in tracks_to_remove: track_list.remove(track) # We'll queue in a hot sec tracks_per_channel[channel] = track_list ntracks = sum(len(tracks) for tracks in tracks_per_channel.values()) ready_barrier = mp.Barrier(ntracks + 1) # We need this Barrier so that main won't proceed with running Channeller until all the tracks are on standby for channel, tracks in tracks_per_channel.items(): # Finally queue the fully resolved tracks print(f'Queueing tracks for channel "{channel.name}"') l = len(tracks) loading_notifier.set(f'[{channel.name}]\nSpawning {l} children...') self.app.root.update() # Spawning all the processes at the same time is much faster ThreadPool(processes=l).starmap( channel.queue, zip(tracks, [-1] * l, [ready_barrier] * l) ) # passes: track, -1 (default for i=), the barrier so main knows when they're all standing by #for i, t in enumerate(track_list): # loading_notifier.set(f'Spawning children... ({i+1}/{l})') # self.app.root.update() # channel.queue(t) print(f'Finished track queueing for channel "{channel.name}"')
def _play(self, stream: pyaudio.Stream, stream_queue: Queue, exec_queue: mp.Queue, data_stream: streaming.AudioStream = None): self.playing.set(True) self.old.set(True) print('playing %s' % self) if data_stream is not None: data_stream.seek_chunk(data_stream.chunk_number(self.temp_start)) data_stream.set_eof_at_chunk( data_stream.chunk_number(self.temp_end)) chunks = streaming.audio_stream_blocker(data_stream, self.CHUNK) print(chunks) else: chunks = make_chunks(self.track[self.temp_start:self.temp_end], self.CHUNK) for chunk in chunks: #print('chunked') if self.paused: print('pause_lock acquired') stream.stop_stream( ) # Sometimes audio gets stuck in the pipes and pops when pausing/resuming self.pause_lock.acquire( ) # yay Locks; blocks until released in resume() #print('pause checked') if not self.playing: print('stopping track') break # Kills thread #print('play checked') #print('round') if stream.is_stopped(): stream.start_stream() #print('stop checked') try: if data_stream is not None: data = audioop.mul( chunk, self.sample_width, 10**(float(self.channel.gain + self.gain) / 10)) else: data = ( chunk + self.channel.gain + self.gain )._data # Live gain editing is possible because it's applied to each chunk in real time #print('new segment created') stream_queue.join( ) # this should block until _write_to_stream() is done processing stream_queue.put( data ) # this will also block if there's more than 1 item in the queue, but that's impossible? #stream.write(data) except OSError: # Couldn't write to host device; maybe it unplugged? raise DeviceDisconnected #print('wrote!') self.play_time += self.CHUNK if self.queue_index != len(self.at_time_queue) and abs( self.at_time_queue[self.queue_index.get()][0] - self.play_time) < self.CHUNK: # If within a CHUNK of the execution time for s in self.at_time_queue[self.queue_index.get()][1]: exec_queue.put(s) self.queue_index += 1 # print(self.play_time) stream.stop_stream()
def __init__(self, file, name='', start_sec=0.0, end_sec=None, fade_in=0.0, fade_out=0.0, delay_in=0.0, delay_out=0.0, gain=0.0, repeat=0, repeat_transition_duration=0.1, repeat_transition_is_xf=False, cut_leading_silence=False, autofollow=(), timed_commands=(), chunk_override=CHUNK): # If repeat_transition_xf is False then a delay will be used with repeat_transition_duration; if True, it will crossfade # Setting fade_in and fade_out to 0.0 will crash self.f = file self.name = name or file self.start = int(start_sec * 1000) self.end = int(end_sec * 1000) if end_sec else None self.fade = ( int(fade_in * 1000) or 1, int(fade_out * 1000) or 1 ) # Fades of 0.0 crash for some reason, so 1 ms will be preferred as a safety measure self.delay = (int(delay_in * 1000), int(delay_out * 1000)) self.gain = shared.Float(gain) # applied in real time self.channel = None self.empty = file is None self.repeats = repeat self._ready_barrier = mp.Barrier(1) # To synchronize with main # This should be replaced in Channel.queue(), as multiple tracks are usually queued at once, and all of them need to be waited for # Mods; handled by a manager # BOTH SHOULD BE LISTS self.auto_follow_mods = autofollow self.at_time_mods = timed_commands if not self.empty: # File is passed fname, ext = os.path.splitext(ntpath.basename( file)) # ntpath.basename splits off the file name from a path loaded = None wc = os.path.join(DIR, 'wave_cache') # We need to check if the file is a wav or not because loading wav is fast af if ext != '.wav': for f in os.listdir(wc): if os.path.splitext(f)[0] == fname: # Not a wav but we found a cached wav copy print('WAV CACHE:', os.path.join(wc, f)) loaded = AudioSegment.from_file(os.path.join(wc, f)) break if loaded is None: # We didn't find a cached wav so we need to load it; cache a wav copy loaded = AudioSegment.from_file(file) print('GENERATING WAV:', os.path.join(wc, fname + '.wav')) if self.CACHE_CONVERTED and 'yt_cache' not in file or self.CACHE_DOWNLOADED and 'yt_cache' in file: loaded.export(os.path.join(wc, fname + '.wav'), format='wav') else: # They gave us a wav thank God print('WAV RECEIVED:', file) loaded = AudioSegment.from_file(file) # delay in, start time, end of leading silence, end time, fade in, fade out self.track = AudioSegment.silent(self.delay[0]) +\ (loaded[self.start + (detect_leading_silence(loaded) if cut_leading_silence else 0):self.end].fade_in(self.fade[0]).fade_out(self.fade[1])) # repeats; track + delay + track + ... if repeat_transition_is_xf: # delay if repeat_transition_duration: for _ in range(repeat): self.track += AudioSegment.silent( int(repeat_transition_duration * 1000)) + self.track else: self.track *= repeat else: # xf for _ in range(repeat): self.track = self.track.append( self.track, crossfade=repeat_transition_duration) # delay out self.track += AudioSegment.silent(self.delay[1]) del loaded else: # An empty track is desired for whatever reason; use delay in and delay out self.track = AudioSegment.silent( self.delay[0]) + AudioSegment.silent(self.delay[1]) self.initially_mono = False if self.track.channels > 1 else True self.length = self.track.duration_seconds self.sample_width = self.track.sample_width self.CHUNK = chunk_override self.proc = mp.Process(target=self.procloop, args=(self.EXECUTOR_QUEUE, self.STDOUT), daemon=True) self.restart_lock = mp.Lock() self.restart_lock.acquire(False) # Prevents from autoplaying at launch self.pause_lock = mp.Lock() self.playing = shared.Bool() self.paused = shared.Bool() self.play_time = shared.Int() # ms self.temp_start = shared.Int() # ms self.temp_end = shared.Int(self.length * 1000) self.at_time_queue = [] self.queue_index = shared.Int() self.old = shared.Bool( ) # False until played; False when renewed, to make sure you don't renew it multiple times self.streaming = shared.Bool(True)
newlogpath = os.path.join(os.getcwd(), 'logs', dt.datetime.now().strftime('%Y-%m-%d %H.%M.%S.log').replace('/', '-').replace(':', ';')) # This mp-manager's sole purpose is to proxy the log file ProxyManager.register('open', open) with ProxyManager() as mp_proxy_manager: log = mp_proxy_manager.open(newlogpath, 'w+', encoding='utf-8') sys.stdout = sys.stderr = audio.Track.STDOUT = log # with statement doesn't work with proxy open(); manual try-finally try: #==================== # FILE SELECTION #==================== print('PID', os.getpid()) # FILE SELECTION cfolder = Path(os.getcwd(), 'config') conf = open(cfolder + 'stored.json', 'r+') data = json.load(conf) # Write down available audio devices real quick with open(cfolder + 'audio_devices.txt', 'w') as f: f.write(str(query_devices())) print('Fetched audio devices.') lfchan = data['last_channel_file'] or cfolder + 'channels.json' lftrack = data['last_track_file'] or cfolder + 'tracks.json' lfcue = data['last_cue_file'] or cfolder + 'cues.cfg'