Playlist for playing multiple Songs (version which does not use buggy completion listener)

3 replies [Last post]
OderWat
User offline. Last seen 28 weeks 1 day ago. Offline
Joined: 4 Jun 2010

Ok ... I just finished some stuff to support a playlist in my app.

This is for INTERNAL mp3 Songs.. not for accessing the Music Library!

I am going to have about 10 songs in my final app and want the user to be able to choose what he wants to listen too by himself.

Because this implementation uses a lot of special things I thought it may be of interest for some of you.

As it is presented here it has:

- duration based "next" (workaround to the crash bug with playSound() listeners)
- pause / continue / restart / next / previous / restart same
- support for suspend/resume while maintaining duration based songswitching
- uses custom events on songchange... nice for animated info's
- module encapsulation (clear interface and selfcontained)
- export / import of state (for storing state on app exit)
- usage of "silence.mp3" and "pausing" instead of stop to work around the ringer volume problem when app starts with music disabled

Currently there is a "deleteSong()" missing :).. left over as exercise for the reader :-P

You may implement some "shuffle" and whatever you need by yourself

I am still creating my interface for building the playlist...

The module "playlist.lua"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
-- Playlist functionality
--
-- this module uses song durations in seconds to
-- advance in the playlist as the "completion" listener
-- of "playSound()" does crash the app on exit!
--
-- written by hans raaf 2010 ... use it as you want :) 
 
-- import just what we need
local string=string
local system=system
local media=media
local Runtime=Runtime
local tonumber=tonumber
local tostring=tostring
local timer=timer
 
--local print=print
local print = function() end
 
-- create the module "separation"
module(...)
 
function new()
        self={}
 
        metatable = { __index = self }
 
        self.list={}
        self.titles={}
        self.durations={}
        self.current=0 -- no song
        self.playing=false
 
        self.timerid=nil
        self.remaining=0
        self.songstart=0
        
        self.version='playlist:1'
 
        function self:system(event)
                -- on Suspend we "reload" the timer
                if event.type=="applicationSuspend" then
                        -- stop timer
                        if self.timerid ~= nil then
                                timer.cancel(self.timerid)
                                self.timerid=nil
                        end
                        -- adjust remaining time .. add 2 seconds for fading out compensation
                        self.remaining=self.remaining-(system.getTimer()-self.songstart+2000)
                elseif event.type=="applicationResume" then
                        -- reset basis for duration calcuations and create new timer
                        self.timerid=timer.performWithDelay(self.remaining, self)
                        self.songstart=system.getTimer()
                end
        end
 
        function self:timer(event)
                -- stub .. faking completion
                self:completion(event)
        end
 
        function self:completion(event)
                self:nextSong()
                -- creating and dispatching an event for advancing to next song
                local event = { name="playlistEvent", type="next", title=self.titles[self.current] }
                Runtime:dispatchEvent( event )
        end
 
        function self:nextSong()
                if #self.list == 0 then
                        -- nothing to play at all
                        return false
                end
                if self.current<#self.list then
                        self.current=self.current+1
                else
                        self.current=1
                end
                self:playhelper(self.current)
                -- returning the title of the now
                -- playing song... just in case
                return self.titles[self.current]
        end
 
        function self:previoustSong()
                if #self.list == 0 then
                        -- nothing to play at all
                        return false
                end
 
                if self.current>1 then
                        self.current=self.current-1
                else
                        self.current=#self.list
                end
                self:playhelper(self.current)
                -- returning the title of the now
                -- playing song... just in case
                return self.titles[self.current]
        end
 
        function self:addSong(filename, title, duration)
                local i=#self.list+1
                
                if duration == nil then
                        duration=5
                end
 
                self.list[i]=filename
                self.titles[i]=title
                -- need to be in milliseconds
                self.durations[i]=duration*1000
        end
 
        function self:playhelper(index,action)
                self.current=index
                self.playing=true
 
                -- stop the timer (if any)
                if self.timerid ~= nil then
                        timer.cancel(self.timerid)
                        self.timerid=nil
                end
 
                -- can not using the listener here because of bug :(
                media.playSound(self.list[index])
 
                -- fake the completion event with a timer!
                self.remaining=self.durations[index]
                self.timerid=timer.performWithDelay(self.remaining, self)
                self.songstart=system.getTimer()
 
                if action ~= nil then
                        -- creating and dispatching an event if one is asked for
                        local event = { name="playlistEvent", type=action, title=self.titles[index] }
                        Runtime:dispatchEvent(event)
                end
        end
 
        function self:playSong(index)
                self:playhelper(index,'play')
        end
 
        function self:stopSong()
                if self.playing then
                        media.pauseSound()
                        self.playing=false
                        local event = { name="playlistEvent", type='stop', title=self.titles[self.current] }
                        Runtime:dispatchEvent(event)
                        -- stop the timer
                        if self.timerid ~= nil then
                                timer.cancel(self.timerid)
                                self.timerid=nil
                        end
                        --  calculate the remaining playting
                        self.remaining=self.remaining-(system.getTimer()-self.songstart)
                end
        end
 
        function self:continueSong()
                if not self.playing then
                        -- just continue the song we stopped before
                        media.playSound()
                        -- restart the timer with the remaining playtime
                        self.timerid=timer.performWithDelay(self.remaining, self)
                        -- remember where we started
                        self.songstart=system.getTimer()
                        self.playing=true
                        Runtime:dispatchEvent(event)
                end
        end
 
        function self:restartSong()
                self:playhelper(index,'restart')
        end
 
        function self:export()
                -- a "magic version" indicator
                local data=self.version.."\n"
                -- number of songs in list
                data=data..#self.list.."\n"
                -- number of filenames and titles
                local n
                for n=1, #self.list do
                        data=data..self.list[n].."\n"
                        data=data..self.titles[n].."\n"
                        data=data..self.durations[n].."\n"
                end
                -- current position in songlist
                data=data..self.current.."\n"
                -- state (playing or not)
                if self.playing then
                        data=data.."y\n"
                else
                        data=data.."n\n"
                end
                -- mark the end of data for a simple sanity check
                data=data.."*\n"
                return data
        end
 
        function self:import(data,start)
                if string.len(data) == 0 then
                        return false
                end
 
                -- stop the timer (if any)
                if self.timerid ~= nil then
                        timer.cancel(self.timerid)
                        self.timerid=nil
                end
 
                -- pause (if any)
                media.pauseSound()
 
                local off=1
                local idx=string.find(data,"\n",off,true)
                local version=string.sub(data,off,idx-1)
                off=idx+1
 
                -- check if we can handle this format
                if version~=self.version then
                        --print("wrong version: "..version)
                        return false
                end
 
                -- count of songs in playlist
                local idx=string.find(data,"\n",off,true)
                local cnt=tonumber(string.sub(data,off,idx-1))
                off=idx+1
 
                -- filenames and titles of the songs
                local n
                for n=1,cnt do
                        idx=string.find(data,"\n",off,true)
                        self.list[n]=string.sub(data,off,idx-1)
                        off=idx+1
                        idx=string.find(data,"\n",off,true)
                        self.titles[n]=string.sub(data,off,idx-1)
                        off=idx+1
                        idx=string.find(data,"\n",off,true)
                        self.durations[n]=tonumber(string.sub(data,off,idx-1))
                        off=idx+1
                end
 
                -- current song playing
                local idx=string.find(data,"\n",off,true)
                self.current=tonumber(string.sub(data,off,idx-1))
                off=idx+1
 
                -- are we playing?
                local idx=string.find(data,"\n",off,true)
                local playing=string.sub(data,off,idx-1)
                off=idx+1
 
                -- sanity check
                local idx=string.find(data,"\n",off,true)
                local tmp=string.sub(data,off,idx-1)
        
                if tmp~='*' then
                        -- something went wrong...
                        return false
                end
 
                -- we "start" the song and pause it immediatly in any case (needed)
                if self.current > 0 then
                        -- can not using the listener here because of bug :(
                        media.playSound(self.list[self.current])
                        self.remaining=self.durations[self.current]
                else
                        -- we play "silence" (see below)
                        media.playSound('silence.mp3')
                        self.remaining=0
                end
                self.songstart=0
                media.pauseSound()
 
                -- you can suppress autostart of the music
                if start~=false then
                        -- restoring the state
                        if playing=='y' then
                                self.playing=true
 
                                -- really start it
                                media.playSound()
 
                                -- fake the completion event with a timer!
                                self.timerid=timer.performWithDelay(self.remaining, self)
                                print("duration left: "..self.remaining/1000)
                                self.songstart=system.getTimer()
 
                                -- creating and dispatching an event for the restored play mode
                                local event = { name="playlistEvent", type='restored', 
                                        title=self.titles[self.current] }
                                Runtime:dispatchEvent( event )
                        else
                                self.playing=false
                        end
                else
                        self.playing=false
                end
 
                return true
        end
 
        -- workaround for volume control reasons!
        -- if you have no media sound active
        -- it will show the volume rocker for the
        -- ringer on iPhone... (bad!)
        media.playSound('silence.mp3')
        media.pauseSound()
 
        -- registering "suspend/resume" handler
        -- so we keep up with the right playtimes if
        -- the app gets suspended in between
        Runtime:addEventListener( "system", self )
 
        return self
 
end

And here some example code on how to use it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
local newPlaylist=require 'playlist'.new
 
 
foo={} -- simulate usage inside an object
 
 
function foo:playlistEvent(event)
        -- this is our custom table listener
        print("playlist says: "..event.type.." "..evemt.title)
 
end
 
function foo:init()
 
        -- for saving application state on exit
        Runtime:addEventListener('system', self)
 
        self.playlist=newPlaylist()
 
        local validplst=false
        local data_file=system.pathForFile('playlist.data', system.DocumentsDirectory)
        local fh=io.open(data_file,"r")
        if fh then
                local data=fh:read('*a')
                io.close()
                validplst=self.playlist:import(data)
        end
 
        if not validplst then
                self.playlist:addSong("song1.mp3","Title A",4*60+15)
                self.playlist:addSong("song2.mp3","Title B",60+56)
                self.playlist:addSong("song3.mp3","Title C", 3*60+47)
                self.playlist:nextSong()
        end
 
        -- event listener for playlist
        Runtime:addEventListener("playlistEvent", self)
 
        -- you may set another song:
 
        self.playlist:playSong(2)
 
        -- you may pause play
 
        self.playlist:pauseSong()
 
        -- you may continue to play it
 
        self.playlist:continueSong()
end
 
-- to keep state between the app usages
 
function foo:system(event)
        if "applicationExit" == event.type then
                print("Prepare for exit!")
                local data_file=system.pathForFile("playlist.data", system.DocumentsDirectory)
                local fh=io.open(data_file,'w')
                local data=self.playlist:export()
                fh:write(data.."\n")
                io.close(fh)
        end
end
 
 
foo:init()

Notice that the "playlist" uses its own applicationSusped/Resume handler and is able to restore the state when the app starts (if you save it in the end)

I am unsure if this helps some of you ... because you need to read and understand the source to adapt it for your own needs probably.

I did not use json for import/export of state but it would be easy to use this instead of the format I am using in this example.

There may be any number of bugs and this is completely unsupported by me... use it as you wish.

Have fun...

Hans

Replies

NuPlayEntertainment's picture
NuPlayEntertainment
User offline. Last seen 7 weeks 6 days ago. Offline
Joined: 2 Jul 2010

Wow, this is great!

Thank you!

tokyodan's picture
tokyodan
User offline. Last seen 1 day 13 hours ago. Offline
Joined: 24 Jun 2009

In the playlist.lua file is there a reason why the module(...) statement is below the local variable declarations instead of at the very top of the file?

Also why are all the functions inside the new function?

OderWat
User offline. Last seen 28 weeks 1 day ago. Offline
Joined: 4 Jun 2010

The module() statement hides all globals and encapsulats the source as module. I omit the "package.seeall" which would copy all globals to local context. So I have to declare what I need manually.

I prefer this because it is less generic and illustrates what is being used in the module. Besides that you should "local"ize all your used global functions anyway. But I am still learning lua ... this technique I found in the json module btw.

Using "functions" inside of the function just places them as functions inside of the created "self" variable as member functions. "function self:test(x)" is equivalent to "function self.text(self,x)" which is equivalent to "self.test = function(self,x)".

Viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.