Esempio n. 1
0
class WavForm(gtk.Widget):
    DEFAULT_PALETTE = {
        "chnl_in": gtk.gdk.color_parse("darkblue"),
        "chnl_out": gtk.gdk.color_parse("black"),
        "odd_in": gtk.gdk.color_parse("green"),
        "odd_out": gtk.gdk.color_parse("darkgreen"),
        "even_in": gtk.gdk.color_parse("red"),
        "even_out": gtk.gdk.color_parse("darkred"),
        "boundary": gtk.gdk.color_parse("cyan"),
        "grid": gtk.gdk.color_parse("darkgray"),
        "tick": gtk.gdk.color_parse("lightgray"),
        "center": gtk.gdk.color_parse("lightgray"),
    }
    VIEWPORT_ZOOM_IN = 0
    VIEWPORT_ZOOM_OUT = 1
    VIEWPORT_PAN_LEFT = 2
    VIEWPORT_PAN_RIGHT = 3
    #__gproperties__ = {}
    __gsignals__ = {
        "sample-data-changed":
        (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
        "audio-format-changed": (gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE,
                                 (gobject.TYPE_PYOBJECT, gobject.TYPE_INT)),
    }

    def __init__(self, adjustment=None):
        gtk.Widget.__init__(self)
        self.set_flags(gtk.CAN_FOCUS)

        self.offscreen_pixmap = None
        self.palette = {}
        self.gc = {}

        self.cursor = gtk.gdk.Cursor(gtk.gdk.CROSS)

        context = self.create_pango_context()
        desc = context.get_font_description()
        desc.set_family("monospace")
        desc.set_absolute_size(12 * pango.SCALE)
        self.layout12 = pango.Layout(context)
        desc.set_absolute_size(10 * pango.SCALE)
        self.layout10 = pango.Layout(context)
        desc.set_absolute_size(9 * pango.SCALE)
        self.layout9 = pango.Layout(context)

        self.metrics = Metrics(self.layout10)

        self.adjustment = None
        self.handler_id_changed = -1
        self.handler_id_value_changed = -1

        self.player = Player(self)

        self.audiofile = ""
        self.caps = None
        self.data = array.array("b")
        self.loader_thread_id = -1
        self.fr_cut_in = 0
        self.fr_cut_out = 8000 * 60
        self.x_cue_pos = -1

        self.draw_solid = True
        self.draw_buf = {0: []}

        self.playback_status = (False, 0)

        if not adjustment:
            adjustment = gtk.Adjustment(0, 0, 8000 * 60, page_size=8000 * 60)
        self.set_adjustment(adjustment)

    def do_realize(self):
        self.set_flags(gtk.REALIZED)
        self.window = gtk.gdk.Window(
            self.get_parent_window(),
            width=self.allocation.width,
            height=self.allocation.height,
            window_type=gtk.gdk.WINDOW_CHILD,
            wclass=gtk.gdk.INPUT_OUTPUT,
            event_mask=gtk.gdk.EXPOSURE_MASK | gtk.gdk.BUTTON_PRESS_MASK
            | gtk.gdk.BUTTON_RELEASE_MASK | gtk.gdk.ENTER_NOTIFY_MASK
            | gtk.gdk.LEAVE_NOTIFY_MASK | gtk.gdk.POINTER_MOTION_MASK
            | gtk.gdk.POINTER_MOTION_HINT_MASK)
        self.window.set_user_data(self)
        self.style.attach(self.window)
        self.style.set_background(self.window, gtk.STATE_NORMAL)
        self.window.move_resize(*self.allocation)

        self.update_palette(self.DEFAULT_PALETTE)
        self.create_pixmap()
        self.paint_pixmap()

    def do_unrealize(self):
        self.window.set_user_data(None)
        self.player.dispose()

    def do_size_request(self, requisition):
        requisition.width = 200
        requisition.height = 100

    def do_size_allocate(self, allocation):
        self.allocation = allocation
        self.update_metrics()
        if self.flags() & gtk.REALIZED:
            self.window.move_resize(*allocation)
            self.update_draw_buffer()
            self.create_pixmap()
            self.paint_pixmap()

    def do_state_changed(self, state):
        self.paint_pixmap()
        self.queue_draw()

    def do_key_press_event(self, event):
        if event.keyval == ord(" "):
            self.player.toggle_play_state()

    def do_expose_event(self, event):
        self.window.draw_drawable(self.style.black_gc, self.offscreen_pixmap,
                                  event.area.x, event.area.y, event.area.x,
                                  event.area.y, event.area.width,
                                  event.area.height)

    def do_button_press_event(self, event):
        if event.type == gtk.gdk.BUTTON_PRESS:
            if not self.is_focus():
                self.grab_focus()

    def do_enter_notify_event(self, event):
        if self.get_property("sensitive"):
            self.window.set_cursor(self.cursor)
            self.set_state(gtk.STATE_PRELIGHT)

    def do_leave_notify_event(self, event):
        if self.get_property("sensitive"):
            self.window.set_cursor(None)
            self.set_state(gtk.STATE_NORMAL)

    def do_motion_notify_event(self, event):
        x_cut_in = self.frame_to_x(self.fr_cut_in)
        x_cut_out = self.frame_to_x(self.fr_cut_out)
        if x_cut_in < event.x and event.x < x_cut_out:
            if event.state & gtk.gdk.BUTTON1_MASK:
                self.x_cue_pos = int(event.x)
                self.paint_pixmap()
                self.queue_draw()

    def do_scroll_event(self, event):
        if not self.caps: return
        fr = self.x_to_frame(event.x)
        if event.direction == gtk.gdk.SCROLL_DOWN:
            if event.state & gtk.gdk.CONTROL_MASK:
                self.update_viewport(self.VIEWPORT_PAN_LEFT, fr)
            else:
                self.update_viewport(self.VIEWPORT_ZOOM_OUT, fr)
        elif event.direction == gtk.gdk.SCROLL_UP:
            if event.state & gtk.gdk.CONTROL_MASK:
                self.update_viewport(self.VIEWPORT_PAN_RIGHT, fr)
            else:
                self.update_viewport(self.VIEWPORT_ZOOM_IN, fr)
        elif event.direction == gtk.gdk.SCROLL_LEFT:
            self.update_viewport(self.VIEWPORT_PAN_LEFT, fr)
        elif event.direction == gtk.gdk.SCROLL_RIGHT:
            self.update_viewport(self.VIEWPORT_PAN_RIGHT, fr)

    def update_palette(self, palette):
        if not self.flags() & gtk.REALIZED:
            return
        self.palette.clear()
        self.gc.clear()
        colormap = self.window.get_colormap()
        for key, color in palette.iteritems():
            self.palette[key] = colormap.alloc_color(color)
            self.gc[key] = self.window.new_gc(foreground=self.palette[key])
        self.gc["center"].set_values(line_style=gtk.gdk.LINE_ON_OFF_DASH)

    def create_pixmap(self):
        w, h = self.window.get_size()
        self.offscreen_pixmap = gtk.gdk.Pixmap(self.window, w, h)

    def paint_pixmap(self):
        if not self.offscreen_pixmap:
            return
        canvas = self.offscreen_pixmap

        gc = self.style.bg_gc[self.state]
        w, h = self.window.get_size()
        canvas.draw_rectangle(gc, True, 0, 0, w, h)

        if not self.get_property("sensitive"):
            return

        channels = self.get_channels()

        ch_height = self.metrics.get_channel_height()
        grid_ys = self.metrics.get_grid_lines()
        tick_ys = self.metrics.get_ticks()
        label_ys = self.metrics.get_labels()
        x_array = self.metrics.get_x_array()
        timetick = self.metrics.get_timetick()
        timetag = self.metrics.get_timetag()
        center_y = self.metrics.get_center()
        layout = self.layout10

        cut_in = self.fr_cut_in
        cut_out = self.fr_cut_out
        x_cut_in = self.frame_to_x(cut_in)
        x_cut_out = self.frame_to_x(cut_out)
        draw_in = int(self.adjustment.value)
        draw_out = draw_in + int(self.adjustment.page_size)
        #print "draw_in", draw_in, "x_draw_in", self.frame_to_x(draw_in),
        #print "draw_out", draw_out, "x_draw_out", self.frame_to_x(draw_out)
        #print "cut_in", cut_in, "cut_out", cut_out,
        #print "x_cut_in", x_cut_in, "x_cut_out", x_cut_out

        # draw sound graph
        hh = (ch_height - 1) * .5
        canvas.draw_rectangle(self.gc["grid"], True, 0, 0, w, h)
        for ch in range(channels):
            if ch % 2 == 0:
                gc_in = self.gc["odd_in"]
                gc_out = self.gc["odd_out"]
            else:
                gc_in = self.gc["even_in"]
                gc_out = self.gc["even_out"]
            by = self.metrics.get_channel_base_y(ch)
            # channel background
            canvas.draw_rectangle(self.gc["chnl_out"], True, 0, by, w,
                                  ch_height - 1)
            canvas.draw_rectangle(self.gc["chnl_in"], True, x_cut_in, by,
                                  x_cut_out - x_cut_in, ch_height - 1)
            # volume grid
            if True:
                for y in grid_ys:
                    canvas.draw_line(self.gc["grid"], 0, y + by, w, y + by)
            # time grid
            for x in x_array:
                canvas.draw_line(self.gc["grid"], x, by, x, by + ch_height - 1)
            # wave form
            yy = by + center_y
            if self.draw_solid:
                for x, (high, low) in enumerate(self.draw_buf[ch]):
                    dh = int(yy - hh * high / 128.)
                    dl = int(yy - hh * low / 128.)
                    if x < x_cut_in or x > x_cut_out:
                        canvas.draw_line(gc_out, x, dh, x, dl)
                    else:
                        canvas.draw_line(gc_in, x, dh, x, dl)
            else:
                draw_in = int(self.adjustment.value)
                x0 = -1
                y0 = yy
                for i, v in enumerate(self.draw_buf[ch]):
                    fr = draw_in + i
                    if fr < cut_in or fr >= cut_out:
                        gc = gc_out
                    else:
                        gc = gc_in
                    x = self.frame_to_x(fr + 1) - 1
                    y = int(yy - hh * v / 128.)
                    canvas.draw_line(gc, x0, y0, x0 + 1, y)
                    canvas.draw_line(gc, x0 + 1, y, x, y)
                    x0, y0 = x, y
            # volume tick
            if True:
                for y, x in tick_ys:
                    canvas.draw_line(self.gc["tick"], 0, y + by, x, y + by)
            # volume label
            if True:
                for y, text in label_ys:
                    layout.set_text(text)
                    tw, th = layout.get_pixel_size()
                    canvas.draw_layout(self.gc["tick"], 28 - tw, y + by - 2,
                                       layout)
            # center line
            canvas.draw_line(self.gc["center"], 0, by + center_y, w - 1,
                             by + center_y)

        if self.x_cue_pos >= 0:
            canvas.draw_line(self.style.white_gc, self.x_cue_pos, 0,
                             self.x_cue_pos, h - 1)
            layout = self.layout9
            layout.set_text("%.3f" % self.get_cue_time())
            canvas.draw_layout(self.style.white_gc, self.x_cue_pos + 2, 80,
                               layout)

        # draw boundary
        #print "boundary", "x_cut_in", x_cut_in, "x_cut_out", x_cut_out
        # this is a ugly solution for black out problem
        # each statement works fine without the other when causes problem
        # when put together
        if x_cut_in >= 0 and x_cut_in < w:
            canvas.draw_line(self.gc["boundary"], x_cut_in, 0, x_cut_in, h - 1)
        canvas.draw_line(self.gc["boundary"], x_cut_out, 0, x_cut_out, h - 1)

        if self.playback_status[0]:
            x_is = self.time_to_x(self.playback_status[1])
            canvas.draw_line(self.style.white_gc, x_is, 0, x_is, h - 27)
            canvas.draw_line(self.style.black_gc, x_is - 1, 0, x_is - 1,
                             h - 27)

        # draw cue slot
        canvas.draw_line(self.style.white_gc, 0, h - 26, w - 1, h - 26)
        canvas.draw_rectangle(self.style.black_gc, True, 0, h - 25, w, 9)
        canvas.draw_line(self.gc["grid"], 0, h - 25, w - 1, h - 25)
        for x in x_array:
            canvas.draw_line(self.gc["grid"], x, h - 25, x, h - 17)

        # draw time line
        layout = self.layout10
        canvas.draw_line(self.style.white_gc, 0, h - 16, w - 1, h - 16)
        canvas.draw_rectangle(self.style.bg_gc[gtk.STATE_ACTIVE], True, 0,
                              h - 15, w, 15)
        for x, y in timetick:
            canvas.draw_line(self.style.fg_gc[gtk.STATE_NORMAL], x, h - 15, x,
                             h - 15 + y)
        for x, text in timetag:
            layout.set_text(text)
            canvas.draw_layout(self.style.fg_gc[gtk.STATE_NORMAL], x + 2,
                               h - 15, layout)

    def update_draw_buffer(self):
        w = self.allocation.width
        self.draw_solid = True
        channels = self.get_channels()
        self.draw_buf = dict([(ch, []) for ch in range(channels)])
        if not True:
            for ch in range(channels):
                for x in range(w - 1):
                    v = math.sin(math.pi * 4 * x / w + math.pi * ch * .5) * 128
                    self.draw_buf[ch].append([max(v, 0), min(v, 0)])
            return

        draw_count = int(self.adjustment.page_size)
        draw_in = int(self.adjustment.value)
        draw_out = draw_in + draw_count
        slice = w - 1
        padding = 0
        pace = 1
        max_offset = len(self.data) - 1
        frames_per_pixel = float(draw_count) / slice
        if frames_per_pixel <= 2:
            self.draw_solid = False
            for fr in range(draw_in, draw_out, pace):
                offset = (fr - padding) * channels
                for ch in range(channels):
                    ii = offset + ch
                    if ii > max_offset:
                        value = 0
                    else:
                        value = self.data[ii]
                    self.draw_buf[ch].append(value)
            return
        else:
            for x in range(w):
                for ch in range(channels):
                    self.draw_buf[ch].append([0, 0])
            pace = 1
            if frames_per_pixel > 20:
                pace = int(frames_per_pixel / 20)
            for x in range(w):
                base_fr = self.x_to_frame(x)
                for fr in range(0, int(frames_per_pixel), pace):
                    offset = (fr + base_fr) * channels
                    ch = 0
                    while ch < channels and offset <= max_offset:
                        self.draw_buf[ch][x][0] = max(self.draw_buf[ch][x][0],
                                                      self.data[offset])
                        self.draw_buf[ch][x][1] = min(self.draw_buf[ch][x][1],
                                                      self.data[offset])
                        offset += 1
                        ch += 1

    def update_metrics(self):
        w = max(self.allocation.width, 200)
        h = max(self.allocation.height, 100) - 26
        self.metrics.update(w, h, self.get_rate(), self.get_channels(),
                            self.adjustment.value,
                            self.adjustment.value + self.adjustment.page_size)

    def update_viewport(self, action, fr_hint):
        draw_count = int(self.adjustment.page_size)
        draw_in = int(self.adjustment.value)
        draw_out = draw_in + draw_count
        total_frames = int(self.adjustment.upper)
        if action == self.VIEWPORT_PAN_LEFT:
            new_draw_in = max(0, draw_in - draw_count)
            if draw_in == new_draw_in:
                return
            self.adjustment.set_value(new_draw_in)
        elif action == self.VIEWPORT_PAN_RIGHT:
            new_draw_out = min(total_frames, draw_out + draw_count)
            if draw_out == new_draw_out:
                return
            self.adjustment.set_value(new_draw_out - draw_count)
        elif action == self.VIEWPORT_ZOOM_IN:
            new_draw_count = max(1, draw_count / 2)
            if draw_count == new_draw_count:
                return
            new_draw_in = fr_hint - new_draw_count / 2
            new_draw_out = new_draw_in + new_draw_count
            if new_draw_in < 0:
                new_draw_in = 0
                new_draw_out = new_draw_count
            elif new_draw_out > total_frames:
                new_draw_out = total_frames
                new_draw_in = new_draw_out - new_draw_count
            self.adjustment.set_all(new_draw_in, 0, total_frames,
                                    new_draw_count, new_draw_count,
                                    new_draw_count)
        elif action == self.VIEWPORT_ZOOM_OUT:
            new_draw_count = min(total_frames, draw_count * 2)
            if draw_count == new_draw_count:
                return
            new_draw_in = fr_hint - new_draw_count / 2
            new_draw_out = new_draw_in + new_draw_count
            if new_draw_in < 0:
                new_draw_in = 0
                new_draw_out = new_draw_count
            elif new_draw_out > total_frames:
                new_draw_out = total_frames
                new_draw_in = new_draw_out - new_draw_count
            self.adjustment.set_all(new_draw_in, 0, total_frames,
                                    new_draw_count, new_draw_count,
                                    new_draw_count)

    #####################################

    def new_load_media_file(self, filepath):
        thread_id = self.loader_thread_id
        loader = SampleLoader(self, thread_id, filepath)

    def on_adjustment_changed(self, adjustment):
        self.update_metrics()
        self.update_draw_buffer()
        self.paint_pixmap()
        self.queue_draw()

    def on_adjustment_value_changed(self, adjustment):
        self.update_metrics()
        self.update_draw_buffer()
        self.paint_pixmap()
        self.queue_draw()

    def reset(self):
        self.audiofile = ""
        self.caps = None
        self.adjustment.set_all(0, 0, 0, 0, 0)
        self.data = array.array("b")
        self.load_thread_id = -1
        self.set_tooltip_text(None)

    ##########################################
    # coordinates conversion utility methods #
    ##########################################

    def frame_to_x(self, fr):
        draw_in = self.adjustment.value
        draw_count = self.adjustment.page_size
        slice = self.allocation.width - 1.0
        if fr < draw_in:
            return -1
        elif fr > draw_in + draw_count * (1 + 1. / slice):
            return self.allocation.width
        x = int((fr - draw_in) * slice / draw_count)
        return x

    def x_to_frame(self, x):
        draw_in = self.adjustment.value
        draw_count = self.adjustment.page_size
        slice = self.allocation.width - 1.0
        fr = int(x * draw_count / slice + draw_in)
        return min(fr, int(self.adjustment.upper))

    def time_to_x(self, t):
        draw_in = self.adjustment.value
        draw_count = self.adjustment.page_size
        slice = self.allocation.width - 1.0
        rate = self.get_rate()
        x = int((t * rate - draw_in) * slice / draw_count)
        return x

    def x_to_time(self, x):
        return float(self.x_to_frame(x)) / self.get_rate()

    def time_to_frame(self, timestamp):
        return int(round(timestamp * self.get_rate()))

    ##################################
    # AudioManager interface methods #
    ##################################

    def get_rate(self):
        if not self.caps:
            return 8000
        return self.caps["rate"]

    def get_channels(self):
        if not self.caps:
            return 1
        return self.caps["channels"]

    ###################################
    # DisplayController interface     #
    ###################################

    def set_adjustment(self, adjustment):
        if self.adjustment:
            self.adjustment.disconnect(self.handler_id_changed)
            self.adjustment.disconnect(self.handler_id_value_changed)
            self.adjustment = None
            self.handler_id_changed = -1
            self.handler_id_value_changed = -1
        self.adjustment = adjustment
        self.handler_id_changed = self.adjustment.connect(
            "changed", self.on_adjustment_changed)
        self.handler_id_value_changed = self.adjustment.connect(
            "value-changed", self.on_adjustment_value_changed)
        self.paint_pixmap()
        self.queue_draw()

    ############################
    # INTERFACE CALLBACKS      #
    ############################

    #-----------------------------------+
    # interface visible to SampleLoader |
    #-----------------------------------+

    def format_detected(self, token, caps, total_frames):
        if token != self.loader_thread_id:
            return
        self.caps = caps
        self.fr_cut_in = 0
        self.fr_cut_out = total_frames
        self.adjustment.set_all(0,
                                0,
                                total_frames,
                                step_increment=total_frames,
                                page_increment=total_frames,
                                page_size=total_frames)
        self.emit("audio-format-changed", caps, total_frames)

    def sample_data_loaded(self, token, new_data, has_more):
        if token != self.loader_thread_id:
            return False
        gtk.gdk.threads_enter()
        self.data.extend(new_data)
        if not has_more:
            self.update_draw_buffer()
            self.paint_pixmap()
            self.queue_draw()
        gtk.gdk.threads_leave()
        self.emit("sample-data-changed")
        return has_more

    #-----------------------------+
    # interface visible to Player |
    #-----------------------------+

    def update_playback_status(self, is_playing, pos_is):
        self.playback_status = (is_playing, pos_is)
        if self.flags() & gtk.REALIZED:
            if is_playing:
                if self.time_to_x(pos_is) >= self.allocation.width:
                    self.update_viewport(self.VIEWPORT_PAN_RIGHT, None)
                else:
                    gtk.gdk.threads_enter()
                    self.paint_pixmap()
                    gtk.gdk.threads_leave()
                    self.queue_draw()

    #--------------------------------+
    # interface visible to tdfplayer |
    #--------------------------------+

    def set_media_file(self, filepath):
        self.reset()
        if not filepath:
            return
        if not os.path.exists(filepath):
            raise RuntimeError, os.strerror(errno.ENOENT)
        elif os.path.isdir(filepath):
            raise RuntimeError, os.strerror(errno.EISDIR)

        self.player.dispose()
        self.player = Player(self)
        self.player.set_file(filepath)
        self.loader_thread_id = thread.start_new_thread(
            self.new_load_media_file, (filepath, ))

    def set_selection_by_time(self, cut_in_time, cut_out_time):
        fr_cut_in = self.time_to_frame(cut_in_time)
        fr_cut_out = self.time_to_frame(cut_out_time)
        if fr_cut_in != self.fr_cut_in or fr_cut_out != self.fr_cut_out:
            self.x_cue_pos = -1
            self.fr_cut_in = fr_cut_in
            self.fr_cut_out = fr_cut_out
            self.paint_pixmap()
            self.queue_draw()

    def set_viewport_by_time(self, disp_in_time, disp_out_time):
        fr_disp_in = max(0, self.time_to_frame(disp_in_time))
        fr_disp_out = self.time_to_frame(disp_out_time)
        viewport_size = fr_disp_out - fr_disp_in
        if fr_disp_in != self.adjustment.value or fr_disp_out != (
                self.adjustment.value + self.adjustment.page_size):
            self.adjustment.set_all(fr_disp_in, 0,
                                    max(self.adjustment.upper,
                                        fr_disp_out), viewport_size,
                                    viewport_size, viewport_size)

    def get_cue_time(self):
        return self.x_to_time(self.x_cue_pos)