class SoundEngine: def __init__( self, samples_to_load: Dict[str, Tuple[Union[str, Sample], int]]) -> None: global samples samples.clear() self.output = Output(mixing="mix") if any(isinstance(smp, str) for smp, _ in samples_to_load.values()): print("Loading sound files...") for name, (filename, max_simultaneously) in samples_to_load.items(): if isinstance(filename, Sample): samples[name] = filename else: data = pkgutil.get_data(__name__, "sounds/" + filename) if data: tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".ogg") try: tmp.write(data) tmp.close() samples[name] = Sample( streaming.AudiofileToWavStream(tmp.name), name).stereo() finally: os.remove(tmp.name) else: raise SystemExit("corrupt package; sound data is missing") self.output.set_sample_play_limit(name, max_simultaneously) print("Sound API initialized:", self.output.audio_api) def play_sample(self, samplename, repeat=False, after=0.0): self.output.play_sample(samples[samplename], repeat, after) def silence(self, sid_or_name=None): if sid_or_name: self.output.stop_sample(sid_or_name) else: self.output.silence() def close(self): self.output.close()
class Player: update_rate = 50 # 50 ms = 20 updates/sec levelmeter_lowest = -40 # dB xfade_duration = 7 def __init__(self, app, trackframes): self.app = app self.trackframes = trackframes self.app.after(self.update_rate, self.tick) self.stopping = False self.mixer = StreamMixer([], endless=True) self.output = Output(self.mixer.samplerate, self.mixer.samplewidth, self.mixer.nchannels, mixing="sequential", queue_size=2) self.mixed_samples = iter(self.mixer) self.levelmeter = LevelMeter(rms_mode=False, lowest=self.levelmeter_lowest) self.output.register_notify_played(self.levelmeter.update) for tf in self.trackframes: tf.player = self player_thread = Thread(target=self._play_sample_in_thread, name="jukebox_sampleplayer") player_thread.daemon = True player_thread.start() def skip(self, trackframe): if trackframe.state != TrackFrame.state_needtrack and trackframe.stream: trackframe.stream.close() trackframe.stream = None trackframe.display_track(None, None, None, "(next track...)") trackframe.state = TrackFrame.state_switching def stop(self): self.stopping = True for tf in self.trackframes: if tf.stream: tf.stream.close() tf.stream = None tf.state = TrackFrame.state_needtrack self.mixer.close() self.output.close() def tick(self): # the actual decoding and sound playing is done in a background thread self._levelmeter() self._load_song() self._play_song() self._crossfade() if not self.stopping: self.app.after(self.update_rate, self.tick) def _play_sample_in_thread(self): """ This is run in a background thread to avoid GUI interactions interfering with audio output. """ while True: if self.stopping: break _, sample = next(self.mixed_samples) if sample and sample.duration > 0: self.output.play_sample(sample) else: self.levelmeter.reset() time.sleep(self.update_rate/1000*2) # avoid hogging the cpu while no samples are played def _levelmeter(self): self.app.update_levels(self.levelmeter.level_left, self.levelmeter.level_right) def _load_song(self): if self.stopping: return # make sure we don't load new songs when the player is shutting down for tf in self.trackframes: if tf.state == TrackFrame.state_needtrack: track = self.app.pop_playlist_track() if track: tf.track = track tf.state = TrackFrame.state_idle def _play_song(self): def start_stream(tf, filename, volume): def _start_from_thread(): # start loading the track from a thread to avoid gui stutters when loading takes a bit of time tf.stream = AudiofileToWavStream(filename, hqresample=hqresample) self.mixer.add_stream(tf.stream, [tf.volumefilter]) tf.playback_started = datetime.datetime.now() tf.state = TrackFrame.state_playing tf.volume = volume tf.state = TrackFrame.state_loading Thread(target=_start_from_thread, name="stream_loader").start() for tf in self.trackframes: if tf.state == TrackFrame.state_playing: remaining = tf.track_duration - (datetime.datetime.now() - tf.playback_started) remaining = remaining.total_seconds() tf.time = datetime.timedelta(seconds=math.ceil(remaining)) if tf.stream.closed and tf.time.total_seconds() <= 0: self.skip(tf) # stream ended! elif tf.state == TrackFrame.state_idle: if tf.xfade_state == TrackFrame.state_xfade_fadingin: # if we're set to fading in, regardless of other tracks, we start playing as well start_stream(tf, tf.track["location"], 0) elif not any(tf for tf in self.trackframes if tf.state in (TrackFrame.state_playing, TrackFrame.state_loading)): # if there is no other track currently playing (or loading), it's our turn! start_stream(tf, tf.track["location"], 100) elif tf.state == TrackFrame.state_switching: tf.state = TrackFrame.state_needtrack def _crossfade(self): for tf in self.trackframes: # nearing the end of the track? then start a fade out if tf.state == TrackFrame.state_playing \ and tf.xfade_state == TrackFrame.state_xfade_nofade \ and tf.time.total_seconds() <= self.xfade_duration: tf.xfade_state = TrackFrame.state_xfade_fadingout tf.xfade_started = datetime.datetime.now() tf.xfade_start_volume = tf.volume # fade in the first other track that is currently idle for other_tf in self.trackframes: if tf is not other_tf and other_tf.state == TrackFrame.state_idle: other_tf.xfade_state = TrackFrame.state_xfade_fadingin other_tf.xfade_started = datetime.datetime.now() other_tf.xfade_start_volume = 0 other_tf.volume = 0 break for tf in self.trackframes: if tf.xfade_state == TrackFrame.state_xfade_fadingin: # fading in, slide volume up from 0 to 100% volume = 100 * (datetime.datetime.now() - tf.xfade_started).total_seconds() / self.xfade_duration tf.volume = min(volume, 100) if volume >= 100: tf.xfade_state = TrackFrame.state_xfade_nofade # fade reached the end elif tf.xfade_state == TrackFrame.state_xfade_fadingout: # fading out, slide volume down from what it was at to 0% fade_progress = (datetime.datetime.now() - tf.xfade_started) fade_progress = (self.xfade_duration - fade_progress.total_seconds()) / self.xfade_duration volume = max(0, tf.xfade_start_volume * fade_progress) tf.volume = max(volume, 0) if volume <= 0: tf.xfade_state = TrackFrame.state_xfade_nofade # fade reached the end def play_sample(self, sample): def unmute(trf, vol): if trf: trf.volume = vol if sample and sample.duration > 0: for tf in self.trackframes: if tf.state == TrackFrame.state_playing: old_volume = tf.mute_volume(40) self.mixer.add_sample(sample, lambda mtf=tf, vol=old_volume: unmute(mtf, vol)) break else: self.mixer.add_sample(sample)
class Repl(cmd.Cmd): """ Interactive command line interface to load/record/save and play samples, patterns and whole tracks. Currently it has no way of defining and loading samples manually. This means you need to initialize it with a track file containing at least the instruments (samples) that you will be using. """ def __init__(self, discard_unused_instruments=False): self.song = Song() self.discard_unused_instruments = discard_unused_instruments self.out = Output(mixing="sequential", queue_size=1) super(Repl, self).__init__() def do_quit(self, args): """quits the session""" print("Bye.", args) self.out.close() return True def do_bpm(self, bpm): """set the playback BPM (such as 174 for some drum'n'bass)""" try: self.song.bpm = int(bpm) except ValueError as x: print("ERROR:", x) def do_ticks(self, ticks): """set the number of pattern ticks per beat (usually 4 or 8)""" try: self.song.ticks = int(ticks) except ValueError as x: print("ERROR:", x) def do_samples(self, args): """show the loaded samples""" print("Samples:") print(", ".join(self.song.instruments)) def do_patterns(self, args): """show the loaded patterns""" print("Patterns:") for name, pattern in sorted(self.song.patterns.items()): self.print_pattern(name, pattern) def print_pattern(self, name, pattern): print("PATTERN {:s}".format(name)) for instrument, bars in pattern.items(): print(" {:>15s} = {:s}".format(instrument, bars)) def do_pattern(self, names): """play the pattern with the given name(s)""" names = names.split() for name in sorted(set(names)): try: pat = self.song.patterns[name] self.print_pattern(name, pat) except KeyError: print("no such pattern '{:s}'".format(name)) return patterns = [self.song.patterns[name] for name in names] try: m = Mixer(patterns, self.song.bpm, self.song.ticks, self.song.instruments) result = m.mix(verbose=len(patterns) > 1).make_16bit() self.out.play_sample(result) self.out.wait_all_played() except ValueError as x: print("ERROR:", x) def do_play(self, args): """play a single sample by giving its name, add a bar (xx..x.. etc) to play it in a bar""" if ' ' in args: instrument, pattern = args.split(maxsplit=1) pattern = pattern.replace(' ', '') else: instrument = args pattern = None instrument = instrument.strip() try: sample = self.song.instruments[instrument] except KeyError: print("unknown sample") return if pattern: self.play_single_bar(sample, pattern) else: sample = sample.copy().make_16bit() self.out.play_sample(sample) self.out.wait_all_played() def play_single_bar(self, sample, pattern): try: m = Mixer([{ "sample": pattern }], self.song.bpm, self.song.ticks, {"sample": sample}) result = m.mix(verbose=False).make_16bit() self.out.play_sample(result) self.out.wait_all_played() except ValueError as x: print("ERROR:", x) def do_mix(self, args): """mix and play all patterns of the song""" if not self.song.pattern_sequence: print("Nothing to be mixed.") return output = "__temp_mix.wav" self.song.mix(output) mix = Sample(wave_file=output) print("Playing sound...") self.out.play_sample(mix) os.remove(output) def do_stream(self, args): """ mix all patterns of the song and stream the output to your speakers in real-time, or to an output file if you give a filename argument. This is the fastest and most efficient way of generating the output mix because it uses very little memory and avoids large buffer copying. """ if not self.song.pattern_sequence: print("Nothing to be mixed.") return if args: filename = args.strip() print("Mixing and streaming to output file '{0}'...".format( filename)) self.out.stream_to_file(filename, self.song.mix_generator()) print("\r ") return print("Mixing and streaming to speakers...") try: samples = self.out.normalized_samples(self.song.mix_generator()) for sample in samples: self.out.play_sample(sample) print("\r ") self.out.wait_all_played() except KeyboardInterrupt: print("Stopped.") def do_rec(self, args): """Record (or overwrite) a new sample (instrument) bar in a pattern. Args: [pattern name] [sample] [bar(s)]. Omit bars to remote the sample from the pattern. If a pattern with the name doesn't exist yet it will be added.""" args = args.split(maxsplit=2) if len(args) not in (2, 3): print("Wrong arguments. Use: patternname sample bar(s)") return if len(args) == 2: args.append(None) # no bars pattern_name, instrument, bars = args if instrument not in self.song.instruments: print("Unknown sample '{:s}'.".format(instrument)) return if pattern_name not in self.song.patterns: self.song.patterns[pattern_name] = {} pattern = self.song.patterns[pattern_name] if bars: bars = bars.replace(' ', '') if len(bars) % self.song.ticks != 0: print("Bar length must be multiple of the number of ticks.") return pattern[instrument] = bars else: if instrument in pattern: del pattern[instrument] if pattern_name in self.song.patterns: if not self.song.patterns[pattern_name]: del self.song.patterns[pattern_name] print("Pattern was empty and has been removed.") else: self.print_pattern(pattern_name, self.song.patterns[pattern_name]) def do_seq(self, names): """ Print the sequence of patterns that form the current track, or if you give a list of names: use that as the new pattern sequence. """ if not names: print(" ".join(self.song.pattern_sequence)) return names = names.split() for name in names: if name not in self.song.patterns: print("Unknown pattern '{:s}'.".format(name)) return self.song.pattern_sequence = names def do_load(self, filename): """Load a new song file""" song = Song() try: song.read(filename, self.discard_unused_instruments) self.song = song except IOError as x: print("ERROR:", x) def do_save(self, filename): """Save current song to file""" if not filename: print("Give filename to save song to.") return if not filename.endswith(".ini"): filename += ".ini" if os.path.exists(filename): if input("File exists: '{:s}'. Overwrite y/n? ".format( filename)) not in ('y', 'yes'): return self.song.write(filename)
class LevelGUI(tk.Frame): def __init__(self, audio_source, master=None): self.lowest_level = -50 super().__init__(master) self.master.title("Levels") self.pbvar_left = tk.IntVar() self.pbvar_right = tk.IntVar() pbstyle = ttk.Style() pbstyle.theme_use("classic") pbstyle.configure("green.Vertical.TProgressbar", troughcolor="gray", background="light green") pbstyle.configure("yellow.Vertical.TProgressbar", troughcolor="gray", background="yellow") pbstyle.configure("red.Vertical.TProgressbar", troughcolor="gray", background="orange") frame = tk.LabelFrame(self, text="Left") frame.pack(side=tk.LEFT) tk.Label(frame, text="dB").pack() self.pb_left = ttk.Progressbar(frame, orient=tk.VERTICAL, length=300, maximum=-self.lowest_level, variable=self.pbvar_left, mode='determinate', style='yellow.Vertical.TProgressbar') self.pb_left.pack() frame = tk.LabelFrame(self, text="Right") frame.pack(side=tk.LEFT) tk.Label(frame, text="dB").pack() self.pb_right = ttk.Progressbar(frame, orient=tk.VERTICAL, length=300, maximum=-self.lowest_level, variable=self.pbvar_right, mode='determinate', style='yellow.Vertical.TProgressbar') self.pb_right.pack() frame = tk.LabelFrame(self, text="Info") self.info = tk.Label(frame, text="", justify=tk.LEFT) frame.pack() self.info.pack(side=tk.TOP) self.pack() self.open_audio_file(audio_source) self.after_idle(self.update) self.after_idle(self.stream_audio) def open_audio_file(self, filename_or_stream): wav = wave.open(filename_or_stream, 'r') self.samplewidth = wav.getsampwidth() self.samplerate = wav.getframerate() self.nchannels = wav.getnchannels() self.samplestream = iter(SampleStream(wav, self.samplerate // 10)) self.levelmeter = LevelMeter(rms_mode=False, lowest=self.lowest_level) self.audio_out = Output(self.samplerate, self.samplewidth, self.nchannels, mixing="sequential", queue_size=3) print("Audio API used:", self.audio_out.audio_api) if not self.audio_out.supports_streaming: raise RuntimeError("need api that supports streaming") self.audio_out.register_notify_played(self.levelmeter.update) filename = filename_or_stream if isinstance(filename_or_stream, str) else "<stream>" info = "Source:\n{}\n\nRate: {:g} Khz\nBits: {}\nChannels: {}"\ .format(filename, self.samplerate/1000, 8*self.samplewidth, self.nchannels) self.info.configure(text=info) def stream_audio(self): try: sample = next(self.samplestream) self.audio_out.play_sample(sample) self.after(20, self.stream_audio) except StopIteration: self.audio_out.close() def update(self): if not self.audio_out.still_playing(): self.pbvar_left.set(0) self.pbvar_right.set(0) print("done!") return left, peak_l = self.levelmeter.level_left, self.levelmeter.peak_left right, peak_r = self.levelmeter.level_right, self.levelmeter.peak_right self.pbvar_left.set(left-self.lowest_level) self.pbvar_right.set(right-self.lowest_level) if left > -3: self.pb_left.configure(style="red.Vertical.TProgressbar") elif left > -6: self.pb_left.configure(style="yellow.Vertical.TProgressbar") else: self.pb_left.configure(style="green.Vertical.TProgressbar") if right > -3: self.pb_right.configure(style="red.Vertical.TProgressbar") elif right > -6: self.pb_right.configure(style="yellow.Vertical.TProgressbar") else: self.pb_right.configure(style="green.Vertical.TProgressbar") self.after(1000//update_rate, self.update)