one shot and other fixes
[swamp-bikeopera:code.git] / lib / engine.lua
1 -- Swamp Bike Opera embedded system for Kaffe Matthews
2 -- Copyright (C) 2012 Wolfgang Hauptfleisch, Dave Griffiths
3 --
4 -- This program is free software: you can redistribute it and/or modify
5 -- it under the terms of the GNU General Public License as published by
6 -- the Free Software Foundation, either version 3 of the License, or
7 -- (at your option) any later version.
8 --
9 -- This program is distributed in the hope that it will be useful,
10 -- but WITHOUT ANY WARRANTY; without even the implied warranty of
11 -- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 -- GNU General Public License for more details.
13 --
14 -- You should have received a copy of the GNU General Public License
15 -- along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17 module("engine", package.seeall)
18
19 require 'gps'
20 require 'posix'
21 require 'poly'
22 require 'map'
23 require 'direction'
24 require 'random'
25 require 'audioc'
26 require 'socket'
27
28 ----------------------------------------------------------------
29 -- override for zones
30
31 overrides = {
32 --   ["big"]={
33 --      colour="blue",
34 --      name="directional",
35 --      dir="e",
36 --   },
37 }
38
39 ----------------------------------------------------------------
40
41 function gpslog(lat, lon)
42    local  file = io.open(CONFIG.install_path.."log/gps.log", "a")
43    local logmessage = os.date().." "..lat.." "..lon.."\n"
44    file:write(logmessage)
45    file:close()
46 end
47
48 function log(logmessage)
49    print(os.date().." "..logmessage)
50 end
51
52 function say(txt)
53     if txt~="" then
54         print("saying:"..txt)
55         --os.execute("espeak \""..txt.."\"")
56     end
57 end
58
59 ----------------------------------------------------------------
60
61 function read_events(events)
62     local txt="";
63
64     for k,layer in pairs(events) do
65         for k,event in pairs(layer) do
66             txt=txt.." "..event.type.." ".." "..event.zone_colour.." "..
67                      event.zone_name.." in "..event.layer_name.."."
68         end
69     end
70     say(txt)
71 end
72
73 ----------------------------------------------------------------
74
75 function dispatch_override(event,pos_state,override)
76     if override.name=="directional" then
77         play_directional(event,pos_state,override)
78     end
79 end
80
81 ----------------------------------------------------------------
82
83 function play_directional(event,pos_state,override)
84     --utils.table_print(override)
85
86     --print(direction.pan_from(pos_state.dir,override.dir))
87
88     if event.type=="entered-zone" then
89 --       print(direction.resolve(poly.angle(pos_state.dir)))
90 --       utils.table_print(pos_state.dir)
91 --       print(override.dir)
92        audioc.loop(event.zone_name,
93                    direction.pan_from(pos_state.dir,override.dir))
94     end
95     if event.type=="left-zone" then
96        audioc.stop(event.zone_name)
97     end
98 end
99
100 --
101 --          n
102 --        nw ne
103 --       w     e
104 --        sw se
105 --          s
106 --
107
108 -- return true if facing in same direction (with 90 deg tolerance)
109 -- this is stupid and dumb version
110 function direction_compare(one,two)
111     if (one == "n") then
112        return utils.find_value(two,{"n","nw","n","ne","e"});
113     end
114     if (one == "ne") then
115        return utils.find_value(two,{"nw","n","ne","e","se"});
116     end
117     if (one == "e") then
118        return utils.find_value(two,{"n","ne","e","se","s"});
119     end
120     if (one == "se") then
121        return utils.find_value(two,{"ne","e","se","s","sw"});
122     end
123     if (one == "s") then
124        return utils.find_value(two,{"e","se","s","sw","w"});
125     end
126     if (one == "sw") then
127        return utils.find_value(two,{"se","s","sw","w","nw"});
128     end
129     if (one == "w") then
130        return utils.find_value(two,{"s","sw","w","nw","n"});
131     end
132     if (one == "nw") then
133        return utils.find_value(two,{"sw","w","nw","n","ne"});
134     end
135     return false
136 end
137
138
139 function cats_to_direction(parent,cats)
140     if (utils.find_value(parent..":North",cats)) then
141         if (utils.find_value(parent..":East",cats)) then
142             return "ne"
143         end
144         if (utils.find_value(parent..":West",cats)) then
145             return "nw"
146         end
147         return "n"
148     end
149     if (utils.find_value(parent..":South",cats)) then
150         if (utils.find_value(parent..":East",cats)) then
151             return "se"
152         end
153         if (utils.find_value(parent..":West",cats)) then
154             return "sw"
155         end
156         return "s"
157     end
158     if (utils.find_value(parent..":West",cats)) then
159         return "w"
160     end
161     if (utils.find_value(parent..":East",cats)) then
162         return "e"
163     end
164     return "centre";
165 end
166
167 ----------------------------------------------------------------
168 -- not sure this is the best place for this!
169 local panned_samples={}
170 local one_shot_samples={}
171
172 function play_events(events,pos_state)
173
174     -- first if the direction has been updated, go through
175     -- all the panned samples
176     if pos_state.new_direction then
177         print("updating direction")
178         for name,dir in pairs(panned_samples) do
179             local pan=direction.pan_from(pos_state.dir,dir)
180             print("shifting "..name.." to "..pan)
181             audioc.shift(name,pan)
182         end
183     end
184
185     -- now look through all the maps for new events
186     for k,layer in pairs(events) do
187         for k,event in pairs(layer) do
188             engine.log(event.type.." "..event.zone_name.." in "..event.layer_name)
189
190             local switch_dir = cats_to_direction("Direction Parameter",event.zone_categories)
191             local heading = direction.resolve(poly.angle(pos_state.dir))
192
193             if (switch_dir~="centre") then
194                 -- if this zone is using a direction to switch, check here
195                 print("DIRECTIONAL--->"..switch_dir.." vs current "..heading)
196                 if (direction_compare(switch_dir,heading)) then
197                     print "ACCEPTED"
198                 else
199                     print "wrong way"
200                 end
201             end
202
203             if switch_dir=="centre" or direction_compare(switch_dir,heading) then
204                 -- defaults
205                 local name=event.zone_name
206                 local loop="no"
207                 if (utils.find_value("Sample Parameters:Loop",event.zone_categories)) then
208                    loop="yes"
209                 end
210                 local dir=cats_to_direction("Pan Parameter",event.zone_categories)
211
212         -- is it a one shot sample?
213                 if (utils.find_value("Sample Parameters:One shot",event.zone_categories)) then
214             one_shot_samples[name]="yes"
215         end
216
217                 -- look for an override
218                 local override=overrides[event.zone_name]
219                 if override then
220                    dispatch_override(event,pos_state,override)
221                 else
222                     -- default behaviour
223                     if event.type=="entered-zone" then
224                        if dir=="centre" then ----- normal -------
225                            if loop=="no" then
226                                audioc.play(name)
227                            else
228                                audioc.loop(name)
229                            end
230                        else ------- directional ---------------
231                            -- add to panned samples so we can update it later
232                            panned_samples[name]=dir
233                            print("added panned "..name.." "..dir)
234                            local pan=direction.pan_from(pos_state.dir,dir)
235                            if loop=="no" then
236                                audioc.play(name,pan)
237                            else
238                                audioc.loop(name,pan)
239                            end
240                        end
241                     end
242                 end
243
244                 if event.type=="left-zone" then
245                     if panned_samples[name] then
246                         panned_samples[name]=nil
247                     end
248
249                 if one_shot_samples[name]~="yes" then
250                     audioc.fadeout(name)
251                 end
252                 end
253             end
254         end
255     end
256 end
257
258 ----------------------------------------------------------------------
259
260 function update_pos_state(pos,state)
261     -- calculate distance since last time
262     local time_diff = os.time() - state.time
263
264     state.new_direction=false
265
266     if time_diff > CONFIG.direction_track_time then
267         state.time = os.time()
268         if state.pos then
269             local distance=poly.distance_km(pos.lat, pos.lng,
270                                             state.pos.lat, state.pos.lng);
271             state.speed=distance/CONFIG.direction_track_time;
272             state.dir={lat=pos.lat-state.pos.lat,
273                        lng=pos.lng-state.pos.lng}
274         end
275         state.pos = pos
276         state.new_direction=true
277
278         log("speed is "..state.speed.."km/h"..
279             " direction is "..direction.resolve(poly.angle(state.dir)))
280     end
281     return state
282 end
283
284 ----------------------------------------------------------------
285
286 function load_events(events,pos_state)
287     for k,layer in pairs(events) do
288         for k,event in pairs(layer) do
289             engine.log("load event: "..event.type.." "..event.zone_name.." in "..event.layer_name)
290
291             -- zone name consists of:
292             -- <name>_<loop>_<direction>_<ghost>
293             local tokens=std.split(event.zone_name,"_")
294
295             -- defaults
296             local name=tokens[1]
297
298             -- default behaviour
299             if event.type=="entered-zone" then
300                 audioc.load(name)
301             end
302             if event.type=="left-zone" then
303                 audioc.unload(name)
304             end
305         end
306     end
307 end