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