refurbish a-pong
[ardour.git] / scripts / _pong.lua
1 ardour {
2         ["type"]    = "dsp",
3         name        = "a-Pong",
4         category    = "Toy",
5         license     = "MIT",
6         author      = "Ardour Lua Task Force",
7         description = [[A console classic for your console]]
8 }
9
10 -- return possible i/o configurations
11 function dsp_ioconfig ()
12         -- -1, -1 = any number of channels as long as input and output count matches
13         return { [1] = { audio_in = -1, audio_out = -1}, }
14 end
15
16 -- control port(s)
17 function dsp_params ()
18         return
19         {
20                 { ["type"] = "input", name = "Bar", min = 0, max = 1, default = 0.5 },
21                 { ["type"] = "input", name = "Reset", min = 0, max = 1, default = 0, toggled = true },
22                 { ["type"] = "input", name = "Difficulty", min = 1, max = 10, default = 3},
23         }
24 end
25
26
27 -- NOTE: these variables are for the DSP part (not shared with the GUI instance)
28 local fps -- audio samples per game-step
29 local game_time -- counts up to fps
30 local game_score
31 local ball_x, ball_y -- ball position [0..1]
32 local dx, dy -- current ball speed
33 local lost_sound -- audio-sample counter for game-over [0..3*fps]
34 local ping_sound -- audio-sample counter for ping-sound [0..fps]
35 local ping_phase -- ping note phase
36 local ping_pitch
37
38 function dsp_init (rate)
39         -- allocate a "shared memory" area to transfer state to the GUI
40         self:shmem ():allocate (3)
41         self:shmem ():clear ()
42         -- initialize some variables
43         fps = rate / 25
44         ping_pitch = 752 / rate
45         ball_x = 0.5
46         ball_y = 0
47         dx = 0.00367
48         dy = 0.01063
49         game_score = 0
50         game_time  = fps -- start the ball immediately (notfiy GUI)
51         ping_sound = fps -- set to end of synth cycle
52         lost_sound = 3 * fps
53 end
54
55 function dsp_run (ins, outs, n_samples)
56         local ctrl = CtrlPorts:array () -- get control port array (read/write)
57         local shmem = self:shmem ()
58         local state = shmem:to_float (0):array () -- "cast" into lua-table
59
60         local changed = false -- flag to notify GUI on every game-step
61         game_time = game_time + n_samples
62
63         -- reset (allow to write automation from a given start-point)
64         if ctrl[2] > 0 then
65                 game_time = 0
66                 ball_x = 0.5
67                 ball_y = 0
68                 dx = 0.00367
69                 dy = 0.01063
70                 game_score = 0
71         end
72
73         -- simple game engine
74         while game_time > fps and ctrl[2] <= 0 do
75                 changed = true
76                 game_time = game_time - fps
77
78                 -- move the ball
79                 ball_x = ball_x + dx * ctrl[3]
80                 ball_y = ball_y + dy * ctrl[3]
81
82                 -- reflect left/right
83                 if ball_x >= 1 or ball_x <= 0 then dx = -dx end
84                 -- keep the ball in the field
85                 if ball_x >= 1 then ball_x = 1 end
86                 if ball_x <= 0 then ball_x = 0 end
87
88                 -- single player (reflect top) -- TODO "stereo" version, 2 ctrls :)
89                 if ball_y <= 0 then dy = - dy y = 0 end
90
91                 -- bottom edge
92                 if ball_y > 1 then
93                         local bar = ctrl[1] -- get bar position
94                         if math.abs (bar - ball_x) < 0.1 then
95                                 -- reflect the ball
96                                 dy = - dy
97                                 ball_y = 1.0
98                                 dx = dx - 0.04 * (bar - ball_x)
99                                 -- make sure it's moving (not stuck on borders)
100                                 if math.abs (dx) < 0.0001 then dx = 0.0001 end
101                                 -- queue 'ping' sound (unless it's already playing to prevent clicks)
102                                 if (ping_sound >= fps) then
103                                         ping_sound = 0
104                                         ping_phase = 0
105                                 end
106                                 game_score = game_score + 1
107                         else
108                                 -- game over
109                                 lost_sound = 0
110                                 ball_y = 0
111                                 dx = 0.00367
112                                 game_score = 0
113                         end
114                 end
115         end
116
117         -- forward audio if processing is not in-place
118         for c = 1,#outs do
119                 -- check if output and input buffers for this channel are identical
120                 -- http://manual.ardour.org/lua-scripting/class_reference/#C:FloatArray
121                 if not ins[c]:sameinstance (outs[c]) then
122                         -- fast (accellerated) memcpy
123                         -- http://manual.ardour.org/lua-scripting/class_reference/#ARDOUR:DSP
124                         ARDOUR.DSP.copy_vector (out[c], ins[c], n_samples)
125                 end
126         end
127
128         -- simple synth -- TODO Optimize
129         if ping_sound < fps then
130                 -- cache audio data buffers for direct access, later
131                 local abufs = {}
132                 for c = 1,#outs do
133                         abufs[c] = outs[c]:array()
134                 end
135                 -- simple sine synth with a sine-envelope
136                 for s = 1, n_samples do
137                         ping_sound = ping_sound + 1
138                         ping_phase = ping_phase + ping_pitch
139                         local snd = 0.7 * math.sin(6.283185307 * ping_phase) * math.sin (3.141592 * ping_sound / fps)
140                         -- add synthesized sound to all channels
141                         for c = 1,#outs do
142                                 abufs[c][s] = abufs[c][s] + snd
143                         end
144                         -- break out of the loop when the sound finished
145                         if ping_sound >= fps then goto ping_end end
146                 end
147                 ::ping_end::
148         end
149
150         if lost_sound < 3 * fps then
151                 local abufs = {}
152                 for c = 1,#outs do
153                         abufs[c] = outs[c]:array()
154                 end
155                 for s = 1, n_samples do
156                         lost_sound = lost_sound + 1
157                         -- -12dBFS white noise
158                         local snd = 0.5 * (math.random () - 0.5)
159                         for c = 1,#outs do
160                                 abufs[c][s] = abufs[c][s] + snd
161                         end
162                         if lost_sound >= 3 * fps then goto noise_end end
163                 end
164                 ::noise_end::
165         end
166
167         if changed then
168                 state[1] = ball_x
169                 state[2] = ball_y
170                 state[3] = game_score
171                 self:queue_draw ()
172         end
173 end
174
175
176 -------------------------------------------------------------------------------
177 --- inline display
178
179 local txt = nil -- cache pango context (in GUI context)
180
181 function render_inline (ctx, w, max_h)
182         local ctrl = CtrlPorts:array () -- control port array
183         local shmem = self:shmem () -- shared memory region
184         local state = shmem:to_float (0):array () -- cast to lua-table
185
186         if (w > max_h) then
187                 h = max_h
188         else
189                 h = w
190         end
191
192         -- prepare text rendering
193         if not txt then
194                 -- allocate PangoLayout and set font
195                 --http://manual.ardour.org/lua-scripting/class_reference/#Cairo:PangoLayout
196                 txt = Cairo.PangoLayout (ctx, "Mono 10px")
197         end
198
199         -- clear background
200         ctx:rectangle (0, 0, w, h)
201         ctx:set_source_rgba (.2, .2, .2, 1.0)
202         ctx:fill ()
203
204         -- print score
205         if (state[3] > 0) then
206                 txt:set_text (string.format ("%.0f", state[3]));
207                 local tw, th = txt:get_pixel_size ()
208                 ctx:set_source_rgba (1, 1, 1, 1.0)
209                 ctx:move_to (w - tw - 3, 3)
210                 txt:show_in_cairo_context (ctx)
211         end
212
213         -- prepare line and dot rendering
214         ctx:set_line_cap (Cairo.LineCap.Round)
215         ctx:set_line_width (3.0)
216         ctx:set_source_rgba (.8, .8, .8, 1.0)
217
218         -- display bar
219         local bar_width = w * .1
220         local bar_space = w - bar_width
221
222         ctx:move_to (bar_space * ctrl[1], h - 3)
223         ctx:rel_line_to (bar_width, 0)
224         ctx:stroke ()
225
226         -- display ball
227         ctx:move_to (1 + state[1] * (w - 3), state[2] * (h - 5))
228         ctx:close_path ()
229         ctx:stroke ()
230
231         return {w, h}
232 end