class Sequencer: def __init__(self, logger=None): if logger is None: logger = logging.getLogger("Sequencer") self.logger = logger self._params = {"tracks": {}, "buses": {}, "reference_colour": None, "max_colour_distance": 1.0} self._sounds = {} self._tracks = collections.OrderedDict() self._buses = collections.OrderedDict() self._groups = [] self._scheduler = Scheduler() self.control_panels = set() self._setup_colour_receiver() SynthController.kill_potential_engine_from_previous_process() self._synth = SynthController() self._synth.launch_engine() self._synth.connect(self._synth.lang_port) self._setup_websocket_server() def _setup_colour_receiver(self): self._current_colour = None self._colour_receiver = ColourReceiver() self._colour_receiver.received_colour = self.set_current_colour self._colour_receiver.start() def set_current_colour(self, rgb): self._current_colour = rgb if self._params["reference_colour"] is not None: self._estimate_age() self._set_age_dependent_rates() for control_panel in self.control_panels: control_panel.send_params() def _estimate_age(self): distance_to_reference = self._colour_distance( self._current_colour, self._params["reference_colour"]) age = distance_to_reference / self._params["max_colour_distance"] age = min(age, 1.0) self._estimated_age = age # self.log("estimated age: %.2f" % self._estimated_age) def _set_age_dependent_rates(self): for track in self._tracks.values(): params = self._params["tracks"][track["name"]] if params["age_type"] is not None: params["rate"] = self._age_dependent_rate(params["age_type"]) # self.log("rate %.1f for age_type=%s, track %s" % ( # params["rate"], params["age_type"], track["name"])) self._on_track_params_changed(track) def _age_dependent_rate(self, age_type): if age_type == "decay": return RATE_MIN + (RATE_MAX - RATE_MIN) * (1 - self._estimated_age) else: return RATE_MIN + (RATE_MAX - RATE_MIN) * self._estimated_age def _colour_distance(self, colour1, colour2): diffs = [colour1[n] - colour2[n] for n in range(3)] return math.sqrt(sum([diff*diff for diff in diffs])) def calibrate_colour(self): self.log("calibrate_colour %s" % self._current_colour) if self._current_colour is not None: self._params["reference_colour"] = self._current_colour def get_tracks(self): return self._tracks def get_buses(self): return self._buses def get_params(self): return self._params def play(self, sound, looped=0): track_name = self._sounds[sound]["track_name"] track = self._tracks[track_name] params = self._params["tracks"][track_name] self._synth.play( sound, params["pan"], params["fade"], params["gain"] + params["gain_adjustment"], params["rate"], looped, params["send"], params["send_gain"] + params["gain_adjustment"], params["comp_threshold"]) def schedule(self, action, delay): self._scheduler.schedule(action, delay) def is_playing(self, sound): return self._synth.is_playing(sound) def load_sounds(self, pattern): for sound in glob.glob(pattern): self.load_sound(sound) def load_sound(self, sound): self._synth.load_sound(sound) self._sounds[sound] = {} def add_track(self, name, pattern, params_overrides): params = copy.copy(DEFAULT_SOUND_PARAMS) params.update(params_overrides) sounds = glob.glob(pattern) track = {"name": name, "sounds": sounds} self._params["tracks"][name] = params for sound in sounds: self._sounds[sound]["track_name"] = name self._tracks[name] = track def add_group(self, pattern, params): group = Group(self, params) for sound in glob.glob(pattern): group.add(sound) self._groups.append(group) def add_bus(self, name): self._synth.add_bus(name) self._buses[name] = {"name": name} self._params["buses"][name] = DEFAULT_BUS_PARAMS def set_bus_params(self, bus, new_params): params = self._params["buses"][bus] params.update(new_params) self._synth.set_bus_params( bus, params["reverb_mix"], params["reverb_room"], params["reverb_damp"]) def try_to_load_params(self): if os.path.exists(PARAMS_FILENAME): self.load_params() def run_main_loop(self): while True: self._process() time.sleep(.1) def _process(self): self._synth.process() self._scheduler.run_scheduled_events() self._colour_receiver.serve() for group in self._groups: group.process() def _setup_websocket_server(self): self._server = WebsocketServer(ControlPanelHandler, {"sequencer": self}) server_thread = threading.Thread(target=self._server.start) server_thread.daemon = True server_thread.start() def set_global_param(self, param, value): self._params[param] = value def set_track_param(self, track_name, param, value): track = self._tracks[track_name] params = self._params["tracks"][track_name] params[param] = value self._on_track_params_changed(track) def _on_tracks_params_changed(self): for track in self._tracks.values(): self._on_track_params_changed(track) def _on_track_params_changed(self, track): params = self._params["tracks"][track["name"]] for sound in track["sounds"]: if self.is_playing(sound): self._synth.set_param(sound, "gain", params["gain"] + params["gain_adjustment"]) self._synth.set_param(sound, "send_gain", params["send_gain"] + params["gain_adjustment"]) self._synth.set_param(sound, "rate", params["rate"]) def save_params(self): f = open(PARAMS_FILENAME, "w") cPickle.dump(self._params, f) f.close() def load_params(self): f = open(PARAMS_FILENAME, "r") self._params = cPickle.load(f) self._on_tracks_params_changed() f.close() def log(self, string): print string self.logger.debug(string)
class Orchestra: SAMPLE_RATE = 44100 PLAYABLE_FORMATS = ['mp3', 'flac', 'wav', 'm4b'] JACK = "jack" SSR = "ssr" @staticmethod def add_parser_arguments(parser): parser.add_argument("--rt", action="store_true", dest="realtime") parser.add_argument("-t", "--torrent", dest="torrentname", default="") parser.add_argument("-z", "--timefactor", dest="timefactor", type=float, default=1) parser.add_argument("--start", dest="start_time", type=float, default=0) parser.add_argument("-q", "--quiet", action="store_true", dest="quiet") parser.add_argument("--pretend-sequential", action="store_true", dest="pretend_sequential") parser.add_argument("--gui", action="store_true", dest="gui_enabled") parser.add_argument("--predecode", action="store_true", dest="predecode", default=True) parser.add_argument("--file-location", dest="file_location", default=DOWNLOAD_LOCATION) parser.add_argument("--fast-forward", action="store_true", dest="ff") parser.add_argument("--fast-forward-to-start", action="store_true", dest="ff_to_start") parser.add_argument("--quit-at-end", action="store_true", dest="quit_at_end") parser.add_argument("--loop", dest="loop", action="store_true") parser.add_argument("--max-passivity", dest="max_passivity", type=float) parser.add_argument("--max-pause-within-segment", dest="max_pause_within_segment", type=float) parser.add_argument("--looped-duration", dest="looped_duration", type=float) parser.add_argument("-o", "--output", dest="output", type=str, default=Orchestra.JACK) parser.add_argument("--include-non-playable", action="store_true") parser.add_argument("-f", "--file", dest="selected_files", type=int, nargs="+") parser.add_argument("--no-synth", action="store_true") parser.add_argument("--locate-peers", action="store_true") _extension_re = re.compile('\.(\w+)$') def __init__(self, sessiondir, tr_log, options): self.options = options self.sessiondir = sessiondir self.tr_log = tr_log self.realtime = options.realtime self.timefactor = options.timefactor self.quiet = options.quiet self.predecode = options.predecode self.file_location = options.file_location self._loop = options.loop self._max_passivity = options.max_passivity self.looped_duration = options.looped_duration self.output = options.output self.include_non_playable = options.include_non_playable if options.locate_peers: import geo.ip_locator self._peer_location = {} ip_locator = geo.ip_locator.IpLocator() for peeraddr in tr_log.peers: self._peer_location[peeraddr] = ip_locator.locate(peeraddr) if options.predecode: predecoder = Predecoder(tr_log, options.file_location, self.SAMPLE_RATE) predecoder.decode() if options.selected_files: tr_log.select_files(options.selected_files) self.playback_enabled = True self.fast_forwarding = False self._log_time_for_last_handled_event = 0 self.gui = None self._check_which_files_are_audio() if options.no_synth: self.synth = None else: from synth_controller import SynthController self.synth = SynthController() self._create_players() self._prepare_playable_files() self.stopwatch = Stopwatch() self.playable_chunks = self._filter_playable_chunks(tr_log.chunks) if self.include_non_playable: self.chunks = tr_log.chunks self._num_selected_files = len(self.tr_log.files) else: self.chunks = self.playable_chunks self._num_selected_files = self._num_playable_files logger.debug("total num chunks: %s" % len(tr_log.chunks)) logger.debug("num playable chunks: %s" % len(self.playable_chunks)) logger.debug("num selected chunks: %s" % len(self.chunks)) self._interpret_chunks_to_score(options.max_pause_within_segment) self._chunks_by_id = {} self.segments_by_id = {} self._playing = False self._quitting = False self.space = Space() if options.ff_to_start: self._ff_to_time = options.start_time self.set_time_cursor(0) else: self._ff_to_time = None self.set_time_cursor(options.start_time) self.scheduler = sched.scheduler(time.time, time.sleep) self._run_scheduler_thread() if self.output == self.SSR: self.ssr = SsrControl() self._warned_about_max_sources = False else: self.ssr = None def _interpret_chunks_to_score(self, max_pause_within_segment): self.score = Interpreter(max_pause_within_segment).interpret(self.playable_chunks, self.tr_log.files) if self._max_passivity: self._reduce_max_passivity_in_score() for segment in self.score: segment["duration"] /= self.timefactor def _reduce_max_passivity_in_score(self): previous_onset = 0 reduced_time = 0 for i in range(len(self.score)): if (self.score[i]["onset"] - reduced_time - previous_onset) > self._max_passivity: reduced_time += self.score[i]["onset"] - reduced_time - previous_onset - self._max_passivity self.score[i]["onset"] -= reduced_time previous_onset = self.score[i]["onset"] def _filter_playable_chunks(self, chunks): return filter(lambda chunk: (self._chunk_is_playable(chunk)), chunks) def _chunk_is_playable(self, chunk): file_info = self.tr_log.files[chunk["filenum"]] return file_info["playable_file_index"] != -1 def _run_scheduler_thread(self): self._scheduler_thread = threading.Thread(target=self._process_scheduled_events) self._scheduler_thread.daemon = True self._scheduler_thread.start() def _process_scheduled_events(self): while not self._quitting: self.scheduler.run() time.sleep(0.01) def _handle_visualizing_message(self, path, args, types, src, data): segment_id = args[0] segment = self.segments_by_id[segment_id] logger.debug("visualizing segment %s" % segment) if self.output == self.SSR: if segment["sound_source_id"]: channel = segment["sound_source_id"] - 1 self._ask_synth_to_play_segment(segment, channel=channel, pan=None) else: player = self.get_player_for_segment(segment) self._ask_synth_to_play_segment(segment, channel=0, pan=player.spatial_position.pan) def _ask_synth_to_play_segment(self, segment, channel, pan): if self.synth: logger.debug("asking synth to play %s" % segment) file_info = self.tr_log.files[segment["filenum"]] self.synth.play_segment( segment["id"], segment["filenum"], segment["start_time_in_file"] / file_info["duration"], segment["end_time_in_file"] / file_info["duration"], segment["duration"], self.looped_duration, channel, pan) self.scheduler.enter( segment["playback_duration"], 1, self.stopped_playing, [segment]) def _check_which_files_are_audio(self): for file_info in self.tr_log.files: file_info["is_audio"] = self._has_audio_extension(file_info["name"]) @staticmethod def _has_audio_extension(filename): return Orchestra._extension(filename) in Orchestra.PLAYABLE_FORMATS @staticmethod def _extension(filename): m = Orchestra._extension_re.search(filename) if m: return m.group(1).lower() def _create_players(self): self._player_class = WavPlayer self.players = [] self._player_for_peer = dict() def _prepare_playable_files(self): if self.predecode: self._get_wav_files_info() self._load_sounds() else: raise Exception("playing wav without precoding is not supported") def _load_sounds(self): if self.synth: print "loading sounds" for filenum in range(len(self.tr_log.files)): file_info = self.tr_log.files[filenum] if file_info["playable_file_index"] != -1: logger.debug("load_sound(%s)" % file_info["decoded_name"]) result = self.synth.load_sound(filenum, file_info["decoded_name"]) logger.debug("result: %s" % result) print "OK" def _get_wav_files_info(self): playable_file_index = 0 for filenum in range(len(self.tr_log.files)): file_info = self.tr_log.files[filenum] file_info["playable_file_index"] = -1 if "decoded_name" in file_info: file_info["duration"] = self._get_file_duration(file_info) if file_info["duration"] > 0: file_info["num_channels"] = self._get_num_channels(file_info) file_info["playable_file_index"] = playable_file_index logger.debug("duration for %r: %r\n" % (file_info["name"], file_info["duration"])) playable_file_index += 1 if self.include_non_playable: file_info["index"] = filenum else: file_info["index"] = file_info["playable_file_index"] self._num_playable_files = playable_file_index def _get_file_duration(self, file_info): if "decoded_name" in file_info: cmd = 'soxi -D "%s"' % file_info["decoded_name"] try: stdoutdata, stderrdata = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate() return float(stdoutdata) except: logger.debug("failed to get duration for %s" % file_info["decoded_name"]) return 0 def _get_num_channels(self, file_info): if "decoded_name" in file_info: cmd = 'soxi -c "%s"' % file_info["decoded_name"] stdoutdata, stderrdata = subprocess.Popen( cmd, shell=True, stdout=subprocess.PIPE).communicate() return int(stdoutdata) def get_current_log_time(self): if self.fast_forwarding: return self._log_time_for_last_handled_event else: return self.log_time_played_from + self.stopwatch.get_elapsed_time() * self.timefactor def play_non_realtime(self, quit_on_end=False): logger.debug("entering play_non_realtime") if self._loop: while True: self._play_until_end() self.set_time_cursor(0) else: self._play_until_end() if quit_on_end: self._quitting = True logger.debug("leaving play_non_realtime") def _play_until_end(self): logger.debug("entering _play_until_end") self._playing = True self.stopwatch.start() no_more_events = False while self._playing and not no_more_events: event = self._get_next_chunk_or_segment() if event: self._handle_event(event) else: no_more_events = True logger.debug("leaving _play_until_end") def _get_next_chunk_or_segment(self): logger.debug("chunk index = %d, segment index = %d" % ( self.current_chunk_index, self.current_segment_index)) chunk = self._get_next_chunk() segment = self._get_next_segment() logger.debug("next chunk: %s" % chunk) logger.debug("next segment: %s" % segment) if chunk and segment: return self._choose_nearest_chunk_or_segment(chunk, segment) elif chunk: return {"type": "chunk", "chunk": chunk} elif segment: return {"type": "segment", "segment": segment} else: return None def _get_next_chunk(self): if self.current_chunk_index < len(self.chunks): return self.chunks[self.current_chunk_index] def _get_next_segment(self): if len(self.score) == 0: return None elif self.current_segment_index < len(self.score): return self.score[self.current_segment_index] def _handle_event(self, event): if event["type"] == "chunk": self.handle_chunk(event["chunk"]) self.current_chunk_index += 1 elif event["type"] == "segment": self.handle_segment(event["segment"]) self.current_segment_index += 1 else: raise Exception("unexpected event %s" % event) def _choose_nearest_chunk_or_segment(self, chunk, segment): if chunk["t"] < segment["onset"]: return {"type": "chunk", "chunk": chunk} else: return {"type": "segment", "segment": segment} def stop(self): if self.synth: self.synth.stop_all() self._playing = False self.log_time_played_from = self.get_current_log_time() self.stopwatch.stop() def handle_segment(self, segment): logger.debug("handling segment %s" % segment) player = self.get_player_for_segment(segment) if not player: logger.debug("get_player_for_segment returned None - skipping playback") if self.fast_forwarding: self._stop_ff_if_necessary() else: now = self.get_current_log_time() time_margin = segment["onset"] - now logger.debug("time_margin=%f-%f=%f" % (segment["onset"], now, time_margin)) if not self.realtime and time_margin > 0: sleep_time = time_margin logger.debug("sleeping %f" % sleep_time) time.sleep(sleep_time) if player: logger.debug("player.enabled=%s" % player.enabled) if player and player.enabled: player.play(segment, pan=0.5) self._log_time_for_last_handled_event = segment["onset"] def handle_chunk(self, chunk): logger.debug("handling chunk %s" % chunk) player = self.get_player_for_chunk(chunk) logger.debug("get_player_for_chunk returned %s" % player) if self.fast_forwarding: self._stop_ff_if_necessary() else: now = self.get_current_log_time() time_margin = chunk["t"] - now logger.debug("time_margin=%f-%f=%f" % (chunk["t"], now, time_margin)) if not self.realtime and time_margin > 0: sleep_time = time_margin logger.debug("sleeping %f" % sleep_time) time.sleep(sleep_time) if player: logger.debug("player.enabled=%s" % player.enabled) if player and player.enabled: player.visualize(chunk) self._log_time_for_last_handled_event = chunk["t"] def _stop_ff_if_necessary(self): if self._ff_to_time is not None and \ self._log_time_for_last_handled_event >= self._ff_to_time: self._ff_to_time = None self.fast_forwarding = False self.set_time_cursor(self.log_time_played_from) def highlight_segment(self, segment): if self.gui: self.gui.highlight_segment(segment) def visualize_chunk(self, chunk, player): if len(self.visualizers) > 0: file_info = self.tr_log.files[chunk["filenum"]] self._chunks_by_id[chunk["id"]] = chunk self._tell_visualizers( "/chunk", chunk["id"], chunk["begin"], chunk["end"] - chunk["begin"], file_info["index"], player.id, chunk["t"]) def visualize_segment(self, segment, player): if len(self.visualizers) > 0: if self.ssr: segment["sound_source_id"] = self.ssr.allocate_source() if not segment["sound_source_id"] and not self._warned_about_max_sources: print "WARNING: max sources exceeded, skipping segment playback (this warning will not be repeated)" self._warned_about_max_sources = True file_info = self.tr_log.files[segment["filenum"]] self.segments_by_id[segment["id"]] = segment self._tell_visualizers( "/segment", segment["id"], segment["begin"], segment["end"] - segment["begin"], file_info["index"], player.id, segment["t"], segment["playback_duration"]) else: self._ask_synth_to_play_segment(segment, channel=0, pan=0.5) def stopped_playing(self, segment): logger.debug("stopped segment %s" % segment) if self.gui: self.gui.unhighlight_segment(segment) if len(self.visualizers) > 0: if self.ssr and segment["sound_source_id"]: self.ssr.free_source(segment["sound_source_id"]) def play_segment(self, segment, player): self.segments_by_id[segment["id"]] = segment if self.looped_duration: segment["playback_duration"] = self.looped_duration else: segment["playback_duration"] = segment["duration"] self.visualize_segment(segment, player) def _send_torrent_info_to_uninformed_visualizers(self): for visualizer in self.visualizers: if not visualizer.informed_about_torrent: self._send_torrent_info_to_visualizer(visualizer) def _send_torrent_info_to_visualizer(self, visualizer): visualizer.send( "/torrent", self._num_selected_files, self.tr_log.lastchunktime(), self.tr_log.total_file_size(), len(self.chunks), len(self.score)) for filenum in range(len(self.tr_log.files)): file_info = self.tr_log.files[filenum] if self.include_non_playable or file_info["playable_file_index"] != -1: visualizer.send( "/file", file_info["index"], file_info["offset"], file_info["length"]) visualizer.informed_about_torrent = True def get_player_for_chunk(self, chunk): try: return chunk["player"] except KeyError: peer_player = self.get_player_for_peer(chunk["peeraddr"]) chunk["player"] = peer_player return peer_player def get_player_for_segment(self, segment): try: return segment["player"] except KeyError: peer_player = self.get_player_for_peer(segment["peeraddr"]) segment["player"] = peer_player return peer_player def get_player_for_peer(self, peeraddr): peer_player = None try: peer_player = self._player_for_peer[peeraddr] except KeyError: peer_player = self._create_player(peeraddr) self.players.append(peer_player) self._player_for_peer[peeraddr] = peer_player return peer_player def _create_player(self, addr): count = len(self.players) logger.debug("creating player number %d" % count) player = self._player_class(self, count) if self.options.locate_peers and self._peer_location[addr] is not None: x, y = self._peer_location[addr] location_str = "%s,%s" % (x, y) else: location_str = "" self._tell_visualizers( "/peer", player.id, addr, player.spatial_position.bearing, location_str) return player def set_time_cursor(self, log_time): assert not self.realtime logger.debug("setting time cursor at %f" % log_time) self.log_time_played_from = log_time if self._playing: self.stopwatch.restart() self.current_chunk_index = self._get_current_chunk_index() self.current_segment_index = self._get_current_segment_index() def _get_current_chunk_index(self): index = 0 next_to_last_index = len(self.chunks) - 2 while index < next_to_last_index: if self.chunks[index+1]["t"] >= self.log_time_played_from: return index index += 1 return len(self.chunks) - 1 def _get_current_segment_index(self): index = 0 next_to_last_index = len(self.score) - 2 while index < next_to_last_index: if self.score[index+1]["onset"] >= self.log_time_played_from: return index index += 1 return len(self.score) - 1 def _handle_set_listener_position(self, path, args, types, src, data): if self.ssr: x, y = args self.ssr.set_listener_position(x, y) def _handle_set_listener_orientation(self, path, args, types, src, data): if self.ssr: orientation = args[0] self.ssr.set_listener_orientation(orientation) def _handle_place_segment(self, path, args, types, src, data): segment_id, x, y, duration = args if self.ssr: segment = self.segments_by_id[segment_id] sound_source_id = segment["sound_source_id"] if sound_source_id is not None: self.ssr.place_source(sound_source_id, x, y, duration) else: pan = self._spatial_position_to_stereo_pan(x, y) if self.synth: self.synth.pan(segment_id, pan) def _handle_enable_smooth_movement(self, path, args, types, src, data): if self.ssr: self.ssr.enable_smooth_movement() def _handle_start_segment_movement_from_peer(self, path, args, types, src, data): segment_id, duration = args if self.ssr: segment = self.segments_by_id[segment_id] sound_source_id = segment["sound_source_id"] if sound_source_id is not None: player = self.get_player_for_segment(segment) self.ssr.start_source_movement( sound_source_id, player.trajectory, duration) def _spatial_position_to_stereo_pan(self, x, y): # compare rectangular_visualizer.Visualizer.pan_segment # NOTE: assumes default listener position and orientation! return float(x) / 5 + 0.5 def reset(self): self._free_sounds() self._tell_visualizers("/reset") def _free_sounds(self): if self.synth: for filenum in range(len(self.tr_log.files)): file_info = self.tr_log.files[filenum] if file_info["playable_file_index"] != -1: self.synth.free_sound(filenum) def _tell_visualizers(self, *args): self._send_torrent_info_to_uninformed_visualizers() self.server._tell_visualizers(*args)
class Orchestra: SAMPLE_RATE = 44100 BYTES_PER_SAMPLE = 2 # mpg123, used by predecode, outputs 16-bit PCM mono PLAYABLE_FORMATS = ['mp3', 'flac', 'wav', 'm4b'] JACK = "jack" SSR = "ssr" @staticmethod def add_parser_arguments(parser): parser.add_argument("--rt", action="store_true", dest="realtime") parser.add_argument("-t", "--torrent", dest="torrentname", default="") parser.add_argument("-z", "--timefactor", dest="timefactor", type=float, default=1) parser.add_argument("--start", dest="start_time", type=float, default=0) parser.add_argument("-q", "--quiet", action="store_true", dest="quiet") parser.add_argument("--pretend-sequential", action="store_true", dest="pretend_sequential") parser.add_argument("--gui", action="store_true", dest="gui_enabled") parser.add_argument("--fast-forward", action="store_true", dest="ff") parser.add_argument("--fast-forward-to-start", action="store_true", dest="ff_to_start") parser.add_argument("--quit-at-end", action="store_true", dest="quit_at_end") parser.add_argument("--loop", dest="loop", action="store_true") parser.add_argument("--max-pause-within-segment", type=float) parser.add_argument("--max-segment-duration", type=float) parser.add_argument("--looped-duration", dest="looped_duration", type=float) parser.add_argument("-o", "--output", dest="output", type=str, default=Orchestra.JACK) parser.add_argument("--include-non-playable", action="store_true") parser.add_argument("-f", "--file", dest="selected_files", type=int, nargs="+") parser.add_argument("--title", type=str, default="") parser.add_argument("--pretend-audio", dest="pretend_audio_filename") parser.add_argument("--capture-audio") parser.add_argument("--leading-pause", type=float, default=0) _extension_re = re.compile('\.(\w+)$') def __init__(self, server, sessiondir, tr_log, options): self.server = server self.options = options self.sessiondir = sessiondir self.tr_log = tr_log self.realtime = options.realtime self.timefactor = options.timefactor self.quiet = options.quiet self._loop = options.loop self.looped_duration = options.looped_duration self.output = options.output self.include_non_playable = options.include_non_playable self._leading_pause = options.leading_pause if server.options.locate_peers: self._peer_location = {} for peeraddr in tr_log.peers: self._peer_location[peeraddr] = server.ip_locator.locate(peeraddr) self._peers_center_location_x = self._get_peers_center_location_x() if options.pretend_audio_filename: self._pretended_file = self._fileinfo_for_pretended_audio_file() self._pretended_file["duration"] = self._get_file_duration(self._pretended_file) self._pretended_files = [self._pretended_file] self._files_to_play = self._pretended_files else: self._files_to_play = self.tr_log.files self.predecode = server.options.predecode if self.predecode: predecoder = Predecoder( tr_log.files, sample_rate=self.SAMPLE_RATE, location=tr_log.file_location) predecoder.decode(server.options.force_predecode) if options.pretend_audio_filename: predecoder = Predecoder( self._pretended_files, sample_rate=self.SAMPLE_RATE) predecoder.decode(server.options.force_predecode) if options.selected_files: tr_log.select_files(options.selected_files) self.playback_enabled = True self.fast_forwarding = False self.gui = None self._check_which_files_are_audio() self._player_class = WavPlayer self.players = [] self._player_for_peer = dict() self._prepare_playable_files() self.stopwatch = Stopwatch() self.playable_chunks = self._filter_playable_chunks(tr_log, tr_log.chunks) if self.include_non_playable: self.chunks = tr_log.chunks self._num_selected_files = len(self.tr_log.files) else: self.chunks = self.playable_chunks self._num_selected_files = self._num_playable_files logger.debug("total num chunks: %s" % len(tr_log.chunks)) logger.debug("num playable chunks: %s" % len(self.playable_chunks)) logger.debug("num selected chunks: %s" % len(self.chunks)) self.score = self._interpret_chunks_to_score(tr_log, self.playable_chunks, options) self.estimated_duration = self._estimated_playback_duration(self.score, options) print "playback duration: %s" % datetime.timedelta(seconds=self.estimated_duration) self._chunks_by_id = {} self.segments_by_id = {} self._playing = False self._quitting = False self._was_stopped = False self.space = Space() self._scheduler_queue = Queue.Queue() self.scheduler = sched.scheduler(time.time, time.sleep) self._run_scheduler_thread() if self.output == self.SSR: self.ssr = SsrControl() self._warned_about_max_sources = False def init_playback(self): if self.server.options.no_synth: self.synth = None else: from synth_controller import SynthController self.synth = SynthController(logger) self.synth.launch_engine(self.server.options.sc_mode) self.synth.connect(self.synth.lang_port) self.synth.subscribe_to_info() if self.options.capture_audio: self._load_sounds() self._start_capture_audio() self._tell_visualizers("/synth_address", self.synth.lang_port) if self.output == self.SSR: self.ssr.run() if not self.options.capture_audio: self._load_sounds() self._log_time_for_last_handled_event = 0 if self.options.ff_to_start: self._ff_to_time = self.options.start_time self.set_time_cursor(0) else: self._ff_to_time = None self.set_time_cursor(self.options.start_time) def _start_capture_audio(self): self._audio_capture_filename = self.options.capture_audio if os.path.exists(self._audio_capture_filename): os.remove(self._audio_capture_filename) self._audio_capture_process = subprocess.Popen( ["./jack_capture/jack_capture", "-f", self._audio_capture_filename, "-d", "-1", "-B", "65536", "SuperCollider:out_1", "SuperCollider:out_2"], shell=False, stdout=subprocess.PIPE) self._wait_until_audio_capture_started() def _wait_until_audio_capture_started(self): print "waiting for audio capture to start" while True: line = self._audio_capture_process.stdout.readline().rstrip("\r\n") m = re.match('^audio capture started at (.*)$', line) if m: audio_capture_start_time = float(m.group(1)) self._tell_visualizers("/audio_captured_started", str(audio_capture_start_time)) print "audio capture started" return @classmethod def _estimated_playback_duration(cls, score, options): last_segment = score[-1] return last_segment["onset"] / options.timefactor + last_segment["duration"] @classmethod def _interpret_chunks_to_score(cls, tr_log, chunks, options): score = Interpreter(options.max_pause_within_segment, options.max_segment_duration).interpret( chunks, tr_log.files) for segment in score: segment["duration"] /= options.timefactor return score @classmethod def _filter_playable_chunks(cls, tr_log, chunks): return filter(lambda chunk: (cls._chunk_is_playable(tr_log, chunk)), chunks) @classmethod def _chunk_is_playable(cls, tr_log, chunk): file_info = tr_log.files[chunk["filenum"]] return file_info["playable_file_index"] != -1 def _run_scheduler_thread(self): self._scheduler_thread = threading.Thread(target=self._process_scheduled_events) self._scheduler_thread.daemon = True self._scheduler_thread.start() def _process_scheduled_events(self): while not self._quitting: while True: try: delay, priority, action, arguments = self._scheduler_queue.get(True, 0.01) except Queue.Empty: break self.scheduler.enter(delay, priority, action, arguments) self.scheduler.run() def _handle_visualizing_message(self, path, args, types, src, data): segment_id = args[0] segment = self.segments_by_id[segment_id] logger.debug("visualizing segment %s" % segment) player = self.get_player_for_segment(segment) self._ask_synth_to_play_segment(segment, channel=0, pan=player.spatial_position.pan) def _ask_synth_to_play_segment(self, segment, channel, pan): if self.synth: logger.debug("asking synth to play %s" % segment) file_info = self.tr_log.files[segment["filenum"]] if self.output == self.SSR: segment["sound_source_id"] = self.ssr.allocate_source() if segment["sound_source_id"] and not self._warned_about_max_sources: channel = segment["sound_source_id"] - 1 pan = None else: print "WARNING: max sources exceeded, skipping segment playback (this warning will not be repeated)" self._warned_about_max_sources = True return self.synth.play_segment( segment["id"], segment["audio_filenum"], segment["relative_start_time_in_file"], segment["relative_end_time_in_file"], segment["duration"], self.looped_duration, channel, pan) self._scheduler_queue.put( (segment["playback_duration"], 1, self.stopped_playing, [segment])) def _check_which_files_are_audio(self): for file_info in self.tr_log.files: file_info["is_audio"] = self._has_audio_extension(file_info["name"]) @staticmethod def _has_audio_extension(filename): return Orchestra._extension(filename) in Orchestra.PLAYABLE_FORMATS @staticmethod def _extension(filename): m = Orchestra._extension_re.search(filename) if m: return m.group(1).lower() def _prepare_playable_files(self): if self.predecode: self._num_playable_files = self._get_wav_files_info( self.tr_log, self.include_non_playable) else: raise Exception("playing wav without precoding is not supported") def _load_sounds(self): if self.synth: print "loading sounds" for filenum in range(len(self._files_to_play)): file_info = self._files_to_play[filenum] if file_info["playable_file_index"] != -1: logger.info("load_sound(%s)" % file_info["decoded_name"]) result = self._load_sound_stubbornly(filenum, file_info["decoded_name"]) logger.info("load_sound result: %s" % result) print "OK" def _load_sound_stubbornly(self, filenum, filename): while True: result = self.synth.load_sound(filenum, filename) if result > 0: return result else: warn(logger, "synth returned %s - retrying soon" % result) time.sleep(1.0) @classmethod def _get_wav_files_info(cls, tr_log, include_non_playable=False): playable_file_index = 0 for filenum in range(len(tr_log.files)): file_info = tr_log.files[filenum] file_info["playable_file_index"] = -1 if "decoded_name" in file_info: file_info["duration"] = cls._get_file_duration(file_info) if file_info["duration"] > 0: file_info["playable_file_index"] = playable_file_index logger.debug("duration for %r: %r\n" % (file_info["name"], file_info["duration"])) playable_file_index += 1 if include_non_playable: file_info["index"] = filenum else: file_info["index"] = file_info["playable_file_index"] return playable_file_index @classmethod def _get_file_duration(cls, file_info): if "decoded_name" in file_info: statinfo = os.stat(file_info["decoded_name"]) wav_header_size = 44 return float((statinfo.st_size - wav_header_size) / cls.BYTES_PER_SAMPLE) / cls.SAMPLE_RATE def get_current_log_time(self): if self.fast_forwarding: return self._log_time_for_last_handled_event else: return self.log_time_played_from + self.stopwatch.get_elapsed_time() * self.timefactor def play_non_realtime(self, quit_on_end=False): logger.info("entering play_non_realtime") self._was_stopped = False self._num_finished_visualizers = 0 if self._loop: while True: self._play_until_end() if not self._was_stopped: self._wait_for_visualizers_to_finish() self.set_time_cursor(0) else: self._play_until_end() if not self._was_stopped: self._wait_for_visualizers_to_finish() if quit_on_end: self._quit() logger.info("leaving play_non_realtime") def _quit(self): if self.options.capture_audio: self._audio_capture_process.kill() self._quitting = True def _play_until_end(self): logger.info("entering _play_until_end") self._playing = True self.stopwatch.start() time.sleep(self._leading_pause) no_more_events = False while self._playing and not no_more_events: event = self._get_next_chunk_or_segment() if event: self._handle_event(event) else: no_more_events = True logger.info("leaving _play_until_end") def _get_next_chunk_or_segment(self): logger.debug("chunk index = %d, segment index = %d" % ( self.current_chunk_index, self.current_segment_index)) chunk = self._get_next_chunk() segment = self._get_next_segment() logger.debug("next chunk: %s" % chunk) logger.debug("next segment: %s" % segment) if chunk and segment: return self._choose_nearest_chunk_or_segment(chunk, segment) elif chunk: return {"type": "chunk", "chunk": chunk} elif segment: return {"type": "segment", "segment": segment} else: return None def _get_next_chunk(self): if self.current_chunk_index < len(self.chunks): return self.chunks[self.current_chunk_index] def _get_next_segment(self): if len(self.score) == 0: return None elif self.current_segment_index < len(self.score): return self.score[self.current_segment_index] def _handle_event(self, event): if event["type"] == "chunk": self.handle_chunk(event["chunk"]) self.current_chunk_index += 1 elif event["type"] == "segment": self.handle_segment(event["segment"]) self.current_segment_index += 1 else: raise Exception("unexpected event %s" % event) def _choose_nearest_chunk_or_segment(self, chunk, segment): if chunk["t"] < segment["onset"]: return {"type": "chunk", "chunk": chunk} else: return {"type": "segment", "segment": segment} def stop(self): # stop_all disabled as it also deletes ~reverb # if self.synth: # self.synth.stop_all() self._was_stopped = True self._playing = False self.log_time_played_from = self.get_current_log_time() self.stopwatch.stop() def handle_segment(self, segment): logger.debug("handling segment %s" % segment) player = self.get_player_for_segment(segment) if not player: logger.debug("get_player_for_segment returned None - skipping playback") if self.fast_forwarding: self._stop_ff_if_necessary() else: now = self.get_current_log_time() time_margin = segment["onset"] - now logger.debug("time_margin=%f-%f=%f" % (segment["onset"], now, time_margin)) if not self.realtime and time_margin > 0: sleep_time = time_margin logger.debug("sleeping %f" % sleep_time) time.sleep(sleep_time) if player: logger.debug("player.enabled=%s" % player.enabled) if player and player.enabled: player.play(segment, pan=0.5) self._log_time_for_last_handled_event = segment["onset"] def handle_chunk(self, chunk): logger.debug("handling chunk %s" % chunk) player = self.get_player_for_chunk(chunk) logger.debug("get_player_for_chunk returned %s" % player) if self.fast_forwarding: self._stop_ff_if_necessary() else: now = self.get_current_log_time() time_margin = chunk["t"] - now logger.debug("time_margin=%f-%f=%f" % (chunk["t"], now, time_margin)) if not self.realtime and time_margin > 0: sleep_time = time_margin logger.debug("sleeping %f" % sleep_time) time.sleep(sleep_time) if player: logger.debug("player.enabled=%s" % player.enabled) if player and player.enabled: player.visualize(chunk) self._log_time_for_last_handled_event = chunk["t"] def _stop_ff_if_necessary(self): if self._ff_to_time is not None and \ self._log_time_for_last_handled_event >= self._ff_to_time: self._ff_to_time = None self.fast_forwarding = False self.set_time_cursor(self.log_time_played_from) def highlight_segment(self, segment): if self.gui: self.gui.highlight_segment(segment) def visualize_chunk(self, chunk, player): if len(self.visualizers) > 0: self._inform_visualizers_about_peer(player) file_info = self.tr_log.files[chunk["filenum"]] self._chunks_by_id[chunk["id"]] = chunk self._tell_visualizers( "/chunk", chunk["id"], chunk["begin"], chunk["end"] - chunk["begin"], file_info["index"], player.id, chunk["t"]) def visualize_segment(self, segment, player): if len(self.visualizers) > 0: self._inform_visualizers_about_peer(player) file_info = self.tr_log.files[segment["filenum"]] self.segments_by_id[segment["id"]] = segment self._tell_visualizers( "/segment", segment["id"], segment["begin"], segment["end"] - segment["begin"], file_info["index"], player.id, segment["t"], segment["playback_duration"]) else: self._ask_synth_to_play_segment(segment, channel=0, pan=0.5) def stopped_playing(self, segment): logger.debug("stopped segment %s" % segment) if self.gui: self.gui.unhighlight_segment(segment) if self.output == self.SSR and segment["sound_source_id"]: self.ssr.free_source(segment["sound_source_id"]) def play_segment(self, segment, player): self.segments_by_id[segment["id"]] = segment if self.looped_duration: segment["playback_duration"] = self.looped_duration else: segment["playback_duration"] = segment["duration"] self.visualize_segment(segment, player) def _send_torrent_info_to_uninformed_visualizers(self): for visualizer in self.visualizers: if not visualizer.informed_about_torrent: self._send_torrent_info_to_visualizer(visualizer) def _inform_visualizers_about_peer(self, player): for visualizer in self.visualizers: if player.id not in visualizer.informed_about_peer: if visualizer.send( "/peer", player.id, player.addr, player.spatial_position.bearing, player.spatial_position.pan, player.location_str): visualizer.informed_about_peer[player.id] = True def _send_torrent_info_to_visualizer(self, visualizer): if not visualizer.send( "/torrent", self._num_selected_files, self.tr_log.lastchunktime(), self.tr_log.total_file_size(), len(self.chunks), len(self.score), self.options.title): return for filenum in range(len(self.tr_log.files)): file_info = self.tr_log.files[filenum] if self.include_non_playable or file_info["playable_file_index"] != -1: if not visualizer.send( "/file", file_info["index"], file_info["offset"], file_info["length"]): return visualizer.informed_about_torrent = True def get_player_for_chunk(self, chunk): try: return chunk["player"] except KeyError: peer_player = self.get_player_for_peer(chunk["peeraddr"]) chunk["player"] = peer_player return peer_player def get_player_for_segment(self, segment): try: return segment["player"] except KeyError: peer_player = self.get_player_for_peer(segment["peeraddr"]) segment["player"] = peer_player return peer_player def get_player_for_peer(self, peeraddr): peer_player = None try: peer_player = self._player_for_peer[peeraddr] except KeyError: peer_player = self._create_player(peeraddr) self.players.append(peer_player) self._player_for_peer[peeraddr] = peer_player return peer_player def _create_player(self, addr): count = len(self.players) logger.debug("creating player number %d" % count) player = self._player_class(self, count) player.addr = addr if self.server.options.locate_peers and self._peer_location[addr] is not None: x, y, place_name = self._peer_location[addr] if place_name: place_name = place_name.encode("unicode_escape") else: place_name = "" player.location_str = "%s,%s,%s" % (x, y, place_name) if x < self._peers_center_location_x: player.spatial_position.pan = -1.0 else: player.spatial_position.pan = 1.0 else: player.location_str = "" return player def set_time_cursor(self, log_time): assert not self.realtime logger.debug("setting time cursor at %f" % log_time) self.log_time_played_from = log_time if self._playing: self.stopwatch.restart() self.current_chunk_index = self._get_current_chunk_index() self.current_segment_index = self._get_current_segment_index() def _get_current_chunk_index(self): index = 0 next_to_last_index = len(self.chunks) - 2 while index < next_to_last_index: if self.chunks[index+1]["t"] >= self.log_time_played_from: return index index += 1 return len(self.chunks) - 1 def _get_current_segment_index(self): index = 0 next_to_last_index = len(self.score) - 2 while index < next_to_last_index: if self.score[index+1]["onset"] >= self.log_time_played_from: return index index += 1 return len(self.score) - 1 def _handle_set_listener_position(self, path, args, types, src, data): if self.output == self.SSR: x, y = args self.ssr.set_listener_position(x, y) def _handle_set_listener_orientation(self, path, args, types, src, data): if self.output == self.SSR: orientation = args[0] self.ssr.set_listener_orientation(orientation) def _handle_place_segment(self, path, args, types, src, data): segment_id, x, y, duration = args if self.output == self.SSR: segment = self.segments_by_id[segment_id] sound_source_id = segment["sound_source_id"] if sound_source_id is not None: self.ssr.place_source(sound_source_id, x, y, duration) else: pan = self._spatial_position_to_stereo_pan(x, y) if self.synth: self.synth.pan(segment_id, pan) def _handle_enable_smooth_movement(self, path, args, types, src, data): pass # OBSOLETE after smooth movement made default def _handle_start_segment_movement_from_peer(self, path, args, types, src, data): segment_id, duration = args if self.output == self.SSR: segment = self.segments_by_id[segment_id] sound_source_id = segment["sound_source_id"] if sound_source_id is not None: player = self.get_player_for_segment(segment) self.ssr.start_source_movement( sound_source_id, player.trajectory, duration) def _spatial_position_to_stereo_pan(self, x, y): # compare rectangular_visualizer.Visualizer.pan_segment # NOTE: assumes default listener position and orientation! return float(x) / 5 + 0.5 def reset(self): if self.synth: self.synth.stop_engine() self._tell_visualizers("/reset") for visualizer in self.visualizers: visualizer.informed_about_torrent = False visualizer.informed_about_peer = {} def _tell_visualizers(self, *args): self._send_torrent_info_to_uninformed_visualizers() self.server._tell_visualizers(*args) def _fileinfo_for_pretended_audio_file(self): return {"offset": 0, "length": os.stat(self.options.pretend_audio_filename).st_size, "name": self.options.pretend_audio_filename, "playable_file_index": 0} def _handle_finished(self, path, args, types, src, data): self._num_finished_visualizers += 1 def _wait_for_visualizers_to_finish(self): while self._num_finished_visualizers < len(self.visualizers): time.sleep(0.1) def _get_peers_center_location_x(self): if len(self._peer_location) <= 1: return 0 else: sorted_xs = sorted([x for x,y,location_str in self._peer_location.values()]) center_index = int((len(self._peer_location)-1) / 2) return float(sorted_xs[center_index] + sorted_xs[center_index+1]) / 2