start making player work - playing a song moves slider now
Stefan Schuermans

Stefan Schuermans commited on 2013-11-22 20:44:32
Showing 4 changed files, with 223 additions and 71 deletions.

... ...
@@ -5,11 +5,31 @@ import re
5 5
 import time_fmt
6 6
 
7 7
 class Playlist:
8
+  """ playlist object, reads a playlist form a file and handles the playlist
9
+  entries
10
+
11
+  playlist file format:
12
+    - each line is an entry or a stop point: <line> = <entry> | <stop point>
13
+    - entries are played one after another
14
+    - playing halts at stop points
15
+    - <entry> = <name> <whitespace> <duration>
16
+    - <stop point> = ""
17
+    - <name> = [A-Za-Z0-9_]+
18
+    - <duration> = ((<hours>:)?<minutes>:)?<seconds>
19
+    - <hours> = [0-9]+
20
+    - <minutes> = [0-9]+
21
+    - <seconds> = [0-9]+(.[0-9]+)"""
22
+
8 23
   def __init__(self):
9
-    self.entries = []
24
+    """create a new, empty playlist object"""
25
+    self.entries = [] # list of entries
26
+                      # entry = dictionary { "type": "normal" or "stop"
27
+                      #                      "name": string, name of entry
28
+                      #                      "durtaion": float, in seconds }
10 29
     self.reEntry = re.compile("^\s*([A-Za-z0-9_]+)\s+([0-9:.]+)\s*$")
11 30
 
12 31
   def read(self, filename):
32
+    """read the playlist from filename, replacing the current playlist"""
13 33
     self.entries = []
14 34
     f = open(filename, "r")
15 35
     for line in f:
... ...
@@ -28,7 +48,10 @@ class Playlist:
28 48
     f.close()
29 49
 
30 50
   def update(self, store):
51
+    """update the contents of a Gtk ListStore with the contents of this
52
+    playlist"""
31 53
     store.clear()
54
+    idx = 0
32 55
     for entry in self.entries:
33 56
       if entry["type"] == "normal":
34 57
         name = entry["name"]
... ...
@@ -36,5 +59,6 @@ class Playlist:
36 59
       else:
37 60
         name = ""
38 61
         duration = "STOP"
39
-      store.append([name, duration])
62
+      store.append([idx, name, duration])
63
+      idx = idx + 1
40 64
 
... ...
@@ -1,28 +1,6 @@
1 1
 <?xml version="1.0" encoding="UTF-8"?>
2 2
 <interface>
3 3
   <!-- interface-requires gtk+ 3.0 -->
4
-  <object class="GtkListStore" id="PlaylistStore">
5
-    <columns>
6
-      <!-- column-name Name -->
7
-      <column type="gchararray"/>
8
-      <!-- column-name Dauer -->
9
-      <column type="gchararray"/>
10
-    </columns>
11
-    <data>
12
-      <row>
13
-        <col id="0" translatable="yes">Erster Akt</col>
14
-        <col id="1" translatable="yes"/>
15
-      </row>
16
-      <row>
17
-        <col id="0" translatable="yes">Zweiter Akt</col>
18
-        <col id="1" translatable="yes"/>
19
-      </row>
20
-      <row>
21
-        <col id="0" translatable="yes">Weltuntergang</col>
22
-        <col id="1" translatable="yes"/>
23
-      </row>
24
-    </data>
25
-  </object>
26 4
   <object class="GtkWindow" id="MainWindow">
27 5
     <property name="visible">True</property>
28 6
     <property name="can_focus">False</property>
... ...
@@ -46,6 +24,8 @@
46 24
                 <property name="visible">True</property>
47 25
                 <property name="can_focus">True</property>
48 26
                 <property name="model">PlaylistStore</property>
27
+                <signal name="cursor-changed" handler="onPlaylistClick" swapped="no"/>
28
+                <signal name="row-activated" handler="onPlaylistDblClick" swapped="no"/>
49 29
                 <child internal-child="selection">
50 30
                   <object class="GtkTreeSelection" id="treeview-selection2"/>
51 31
                 </child>
... ...
@@ -301,6 +281,33 @@
301 281
       </object>
302 282
     </child>
303 283
   </object>
284
+  <object class="GtkListStore" id="PlaylistStore">
285
+    <columns>
286
+      <!-- column-name EntryIdx -->
287
+      <column type="guint"/>
288
+      <!-- column-name Name -->
289
+      <column type="gchararray"/>
290
+      <!-- column-name Dauer -->
291
+      <column type="gchararray"/>
292
+    </columns>
293
+    <data>
294
+      <row>
295
+        <col id="0">0</col>
296
+        <col id="1" translatable="yes">Erster Akt</col>
297
+        <col id="2" translatable="yes">1:00:00</col>
298
+      </row>
299
+      <row>
300
+        <col id="0">1</col>
301
+        <col id="1" translatable="yes">Zweiter Akt</col>
302
+        <col id="2" translatable="yes">23:42</col>
303
+      </row>
304
+      <row>
305
+        <col id="0">2</col>
306
+        <col id="1" translatable="yes">Weltuntergang</col>
307
+        <col id="2" translatable="yes">0.5</col>
308
+      </row>
309
+    </data>
310
+  </object>
304 311
   <object class="GtkAdjustment" id="Position">
305 312
     <property name="upper">100</property>
306 313
     <property name="value">50</property>
... ...
@@ -2,6 +2,8 @@
2 2
 
3 3
 import os
4 4
 from gi.repository import Gtk
5
+import gobject
6
+import time
5 7
 
6 8
 import playlist
7 9
 import time_fmt
... ...
@@ -9,21 +11,25 @@ import time_fmt
9 11
 scriptdir = os.path.dirname(os.path.abspath(__file__))
10 12
 
11 13
 class SyncGui:
14
+
12 15
   def __init__(self):
16
+    """construct a SyncGui object"""
13 17
     self.builder = Gtk.Builder()
14 18
     self.builder.add_from_file(scriptdir + "/sync_gui.glade")
15
-    self.playlistView = self.builder.get_object("PlaylistView")
16
-    self.playlistStore = self.builder.get_object("PlaylistStore")
17
-    self.position = self.builder.get_object("Position")
18
-    self.positionScale = self.builder.get_object("PositionScale")
19
-    self.positionAt = self.builder.get_object("PositionAt")
20
-    self.positionRemaining = self.builder.get_object("PositionRemaining")
21
-    self.btnPause = self.builder.get_object("Pause")
22
-    self.btnPlay = self.builder.get_object("Play")
23
-    self.status = self.builder.get_object("Status")
19
+    self.widPlaylistView = self.builder.get_object("PlaylistView")
20
+    self.widPlaylistStore = self.builder.get_object("PlaylistStore")
21
+    self.widPosition = self.builder.get_object("Position")
22
+    self.widPositionScale = self.builder.get_object("PositionScale")
23
+    self.widPositionAt = self.builder.get_object("PositionAt")
24
+    self.widPositionRemaining = self.builder.get_object("PositionRemaining")
25
+    self.widBtnPause = self.builder.get_object("Pause")
26
+    self.widBtnPlay = self.builder.get_object("Play")
27
+    self.widStatus = self.builder.get_object("Status")
24 28
     self.configPlaylistColumns()
25 29
     handlers = {
26 30
       "onDestroy":          self.onDestroy,
31
+      "onPlaylistClick":    self.onPlaylistClick,
32
+      "onPlaylistDblClick": self.onPlaylistDblClick,
27 33
       "onNewPosition":      self.onNewPosition,
28 34
       "onPrevious":         self.onPrevious,
29 35
       "onBackward":         self.onBackward,
... ...
@@ -36,68 +42,170 @@ class SyncGui:
36 42
     self.builder.connect_signals(handlers)
37 43
     self.playlist = playlist.Playlist()
38 44
     self.playlist.read("playlist.txt")
39
-    self.playlist.update(self.playlistStore)
40
-    self.status.push(0, "TODO...")
41
-    self.showCurrentPosition()
45
+    self.playlist.update(self.widPlaylistStore)
46
+    self.widStatus.push(0, "TODO...")
47
+    self.stEntryIdx = -1 # no entry selected
48
+    self.stName = "" # no current entry name
49
+    self.stDuration = 0 # current entry has zero size
50
+    self.stPosition = 0 # at begin of current entry
51
+    self.stPlaying = False # not playing
52
+    gobject.timeout_add(10, self.onTimer10ms)
53
+    self.updateDuration()
54
+    self.updateButtonVisibility()
42 55
 
43 56
   def configPlaylistColumns(self):
44
-    i = 0
57
+    """configure the columns of the playlist widget at program start"""
58
+    i = 1 # first column is index (not shown)
45 59
     for title in ["Name", "Dauer"]:
46 60
       column = Gtk.TreeViewColumn(title)
47
-      self.playlistView.append_column(column)
61
+      self.widPlaylistView.append_column(column)
48 62
       cell = Gtk.CellRendererText()
49 63
       column.pack_start(cell, False)
50 64
       column.add_attribute(cell, "text", i)
51 65
       i = i + 1
52 66
 
53
-  def showPosition(self, sec):
54
-    if sec < 0:
55
-      sec = 0
56
-    if sec > self.position.get_upper():
57
-      sec = self.position.get_upper()
58
-    posAt = time_fmt.sec2str(sec)
59
-    posRemaining = time_fmt.sec2str(self.position.get_upper() - sec)
60
-    self.positionAt.set_text(posAt)
61
-    self.positionRemaining.set_text(posRemaining)
62
-
63
-  def showCurrentPosition(self):
64
-    sec = self.positionScale.get_value()
65
-    self.showPosition(sec)
67
+  def showPosition(self):
68
+    """update the position texts next to the position slider"""
69
+    # format current time and remaining time
70
+    posAt = time_fmt.sec2str(self.stPosition)
71
+    posRemaining = time_fmt.sec2str(self.stDuration - self.stPosition)
72
+    self.widPositionAt.set_text(posAt)
73
+    self.widPositionRemaining.set_text(posRemaining)
74
+
75
+  def updatePositionState(self):
76
+    """update the position in the state, but not the slider"""
77
+    # calculate (virtual) start time of playing
78
+    # i.e. the time the playing would have had started to arrive at the
79
+    # current position now if it had played continuosly
80
+    self.stPlayStart = time.time() - self.stPosition
81
+    # update position texts
82
+    self.showPosition()
83
+
84
+  def updatePosition(self):
85
+    """update the position including the position slider"""
86
+    # update GUI slider
87
+    self.widPositionScale.set_value(self.stPosition)
88
+    # update position state
89
+    self.updatePositionState()
90
+
91
+  def updateDuration(self):
92
+    """update the duration (i.e. range for the slider) based on the current
93
+       playlist entry"""
94
+    # get duration of new playlist entry
95
+    self.stDuration = 0
96
+    if self.stEntryIdx >= 0:
97
+      entry = self.playlist.entries[self.stEntryIdx]
98
+      if entry["type"] == "normal":
99
+        self.stDuration = entry["duration"]
100
+    # set position to begin
101
+    self.stPosition = 0
102
+    # update value range
103
+    self.widPosition.set_upper(self.stDuration)
104
+    # update position of slider
105
+    self.updatePosition()
106
+
107
+  def updateButtonVisibility(self):
108
+    """update the visibility of the buttons based on if playing or not"""
109
+    self.widBtnPause.set_visible(self.stPlaying)
110
+    self.widBtnPlay.set_visible(not self.stPlaying)
66 111
 
67 112
   def onDestroy(self, widget):
113
+    """window will be destroyed"""
68 114
     Gtk.main_quit()
69 115
 
116
+  def onPlaylistClick(self, widget):
117
+    """playlist entry has been clicked or selected"""
118
+    # get index of selected entry
119
+    idx = -1
120
+    sel = self.widPlaylistView.get_selection()
121
+    if sel is not None:
122
+      (model, it) = sel.get_selected()
123
+      if it is not None:
124
+        (idx, ) = model.get(it, 0)
125
+    print("DEBUG: playlist click idx=%d" % (idx))
126
+    # update playlist entry
127
+    self.stEntryIdx = idx
128
+    # update duration
129
+    self.updateDuration()
130
+
131
+  def onPlaylistDblClick(self, widget, row, col):
132
+    """playlist entry has been double-clicked"""
133
+    # get index of selected entry
134
+    idx = -1
135
+    sel = self.widPlaylistView.get_selection()
136
+    if sel is not None:
137
+      (model, it) = sel.get_selected()
138
+      if it is not None:
139
+        (idx, ) = model.get(it, 0)
140
+    print("DEBUG: playlist double-click idx=%d" % (idx))
141
+    # update playlist entry
142
+    self.stEntryIdx = idx
143
+    # update duration
144
+    self.updateDuration()
145
+    # start playing
146
+    # TODO
147
+
70 148
   def onNewPosition(self, widget, scroll, value):
71
-    print("new position " + str(value));
72
-    self.showPosition(value)
149
+    """slider has been moved to a new position"""
150
+    print("DEBUG: new position " + str(value));
151
+    # clamp position to valid range
152
+    if value < 0:
153
+      value = 0
154
+    if value > self.stDuration:
155
+      value = self.stDuration
156
+    # update current position - and play start time if playing
157
+    self.stPosition = value
158
+    # update position state (do not touch the slider)
159
+    self.updatePositionState()
73 160
 
74 161
   def onPrevious(self, widget):
75
-    print("previous")
162
+    """previous button as been pressed"""
163
+    print("DEBUG: previous")
76 164
 
77 165
   def onBackward(self, widget):
78
-    print("backward")
166
+    """backward button has been pressed"""
167
+    print("DEBUG: backward")
79 168
 
80 169
   def onStop(self, widget):
81
-    print("stop")
82
-    self.btnPause.set_visible(False)
83
-    self.btnPlay.set_visible(True)
170
+    """stop button has been pressed"""
171
+    print("DEBUG: stop")
172
+    self.stPlaying = False
173
+    self.stPosition = 0 # stop goes back to begin
174
+    self.updatePosition()
175
+    self.updateButtonVisibility()
84 176
 
85 177
   def onPause(self, widget):
86
-    print("pause")
87
-    self.btnPause.set_visible(False)
88
-    self.btnPlay.set_visible(True)
178
+    """pause button has been pressed"""
179
+    print("DEBUG: pause")
180
+    self.stPlaying = False
181
+    self.updateButtonVisibility()
89 182
 
90 183
   def onPlay(self, widget):
91
-    print("play")
92
-    self.btnPause.set_visible(True)
93
-    self.btnPlay.set_visible(False)
184
+    """play button has been pressed"""
185
+    print("DEBUG: play")
186
+    self.stPlaying = True
187
+    self.updatePosition()
188
+    self.updateButtonVisibility()
94 189
 
95 190
   def onForward(self, widget):
96
-    print("forward")
191
+    """forward button has been pressed"""
192
+    print("DEBUG: forward")
97 193
 
98 194
   def onNext(self, widget):
99
-    print("next")
100
-
195
+    """next button has been pressed"""
196
+    print("DEBUG: next")
197
+
198
+  def onTimer10ms(self):
199
+    """timer callback, every 10ms"""
200
+    # update position if playing
201
+    if self.stPlaying:
202
+      self.stPosition = time.time() - self.stPlayStart
203
+      if self.stPosition > self.stDuration:
204
+        self.stPosition = 0 # FIXME: go to next entry
205
+      self.updatePosition()
206
+    return True
207
+
208
+# main application entry point
101 209
 if __name__ == "__main__":
102 210
   app = SyncGui()
103 211
   Gtk.main()
... ...
@@ -1,12 +1,24 @@
1 1
 #! /usr/bin/env python
2 2
 
3
+"""time format converter
4
+
5
+converts between time in seconds as floating point value and time as
6
+human-readable string in hours, minutes and seconds
7
+
8
+<time string> = ((<hours>:)?<minutes>:)?<seconds>
9
+<hours> = [0-9]+
10
+<minutes> = [0-9]+
11
+<seconds> = [0-9]+(.[0-9]+)"""
12
+
3 13
 def sec2str(sec):
14
+  """convert time in seconds to human-readable time string"""
4 15
   sign = ""
5
-  if sec < 0:
16
+  sec100 = round(sec * 100)
17
+  if sec100 < 0:
6 18
     sign = "-";
7
-    sec = -sec;
8
-  sec1 = int(sec)
9
-  sec100 = round((sec - sec1) * 100)
19
+    sec100 = -sec100;
20
+  sec1 = sec100 // 100
21
+  sec100 = sec100 % 100
10 22
   minu = sec1 // 60
11 23
   sec1 = sec1 % 60
12 24
   hour = minu // 60
... ...
@@ -14,6 +26,7 @@ def sec2str(sec):
14 26
   return "%s%u:%02u:%02u.%02u" % (sign, hour, minu, sec1, sec100)
15 27
 
16 28
 def str2sec(str):
29
+  """convert a human readable time string into time in seconds"""
17 30
   total = 0
18 31
   section = 0
19 32
   sign = 1
20 33