Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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