コード例 #1
0
    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)
コード例 #2
0
ファイル: main.py プロジェクト: ShadowElf37/Channeller
 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)
コード例 #3
0
 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')
コード例 #4
0
 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)
コード例 #5
0
    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()
コード例 #6
0
 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)
コード例 #7
0
    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
コード例 #8
0
                        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}"')
コード例 #9
0
    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()
コード例 #10
0
    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)
コード例 #11
0
ファイル: main.py プロジェクト: ShadowElf37/Channeller
    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'