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
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?
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)".
Wow, this is great!
Thank you!