Only emit InstrumentInfo::Changed() on actual change
[ardour.git] / scripts / _pong.lua
index 3683209a2b20acb8e4f33e85dfddd5c25c9e0e4c..8eef49f00470f00167593897dfa64f53bf71c097 100644 (file)
@@ -1,10 +1,10 @@
 ardour {
        ["type"]    = "dsp",
        name        = "a-Pong",
-       category    = "Visualization",
+       category    = "Toy",
        license     = "MIT",
        author      = "Ardour Lua Task Force",
-       description = [[classic game of mixer pong]]
+       description = [[A console classic for your console]]
 }
 
 -- return possible i/o configurations
@@ -13,117 +13,200 @@ function dsp_ioconfig ()
        return { [1] = { audio_in = -1, audio_out = -1}, }
 end
 
+-- control port(s)
 function dsp_params ()
        return
        {
                { ["type"] = "input", name = "Bar", min = 0, max = 1, default = 0.5 },
+               { ["type"] = "input", name = "Reset", min = 0, max = 1, default = 0, toggled = true },
+               { ["type"] = "input", name = "Difficulty", min = 1, max = 10, default = 3},
        }
 end
 
-local gametime
-local fps
-local ball_x, ball_y
-local dx, dy
-local pingsound
-local lotsound
-local pingnote
+
+-- Game State (for this instance)
+-- NOTE: these variables are for the DSP part (not shared with the GUI instance)
+local sample_rate -- sample-rate
+local fps -- audio samples per game-step
+local game_time -- counts up to fps
+local game_score
+local ball_x, ball_y -- ball position [0..1]
+local dx, dy -- current ball speed
+local lost_sound -- audio-sample counter for game-over [0..3*fps]
+local ping_sound -- audio-sample counter for ping-sound [0..fps]
+local ping_phase -- ping note phase-difference per sample
+local ping_pitch
 
 function dsp_init (rate)
-       self:shmem ():allocate (4)
+       -- allocate a "shared memory" area to transfer state to the GUI
+       self:shmem ():allocate (3)
        self:shmem ():clear ()
+       -- initialize some variables
+       sample_rate = rate
        fps = rate / 25
-       pingnote = 352 / rate
+       ping_pitch = 752 / rate
        ball_x = 0.5
        ball_y = 0
-       dx = 0.011
-       dy = 0.021
+       dx = 0.00367
+       dy = 0.01063
+       game_score = 0
+       game_time  = fps -- start the ball immediately (notify GUI)
+       ping_sound = fps -- set to end of synth cycle
+       lost_sound = 3 * fps
 end
 
-function dsp_configure (ins, outs)
-       gametime = fps
-       pingsound = fps
-       lostsound = 3 * fps
+function queue_beep ()
+       -- queue 'ping' sound (unless one is already playing to prevent clicks)
+       if (ping_sound >= fps) then
+               -- major scale, 2 octaves
+               local scale = { 0, 2, 4, 5, 7, 9, 11, 12, 14, 16, 17, 19, 21, 23, 24 }
+               local midi_note = 60 + scale[1 + math.floor (math.random () * 14)]
+               ping_pitch = (440 / 32) * 2^((midi_note - 10.0) / 12.0) / sample_rate
+               ping_sound = 0
+               ping_phase = 0
+       end
 end
 
+-- callback: process "n_samples" of audio
+-- ins, outs are http://manual.ardour.org/lua-scripting/class_reference/#C:FloatArray
+-- pointers to the audio buffers
 function dsp_run (ins, outs, n_samples)
        local ctrl = CtrlPorts:array () -- get control port array (read/write)
-       local shmem = self:shmem ()
-       local state = shmem:to_float (0):array () -- "cast" into lua-table
 
-       local changed = false
-       gametime = gametime + n_samples
+       local changed = false -- flag to notify GUI on every game-step
+       game_time = game_time + n_samples
+
+       -- reset (allow to write automation from a given start-point)
+       -- ctrl[2] corresponds to the  "Reset" input control
+       if ctrl[2] > 0 then
+               game_time = 0
+               ball_x = 0.5
+               ball_y = 0
+               dx = 0.00367
+               dy = 0.01063
+               game_score = 0
+       end
 
        -- simple game engine
-       while gametime > fps do
+       while game_time > fps and ctrl[2] <= 0 do
                changed = true
-               gametime = gametime - fps
+               game_time = game_time - fps
+
+               -- move the ball
+               ball_x = ball_x + dx * ctrl[3]
+               ball_y = ball_y + dy * ctrl[3]
+
+               -- reflect left/right
+               if ball_x >= 1 or ball_x <= 0 then
+                       dx = -dx
+                       queue_beep ()
+               end
 
-               ball_x = ball_x + dx
-               ball_y = ball_y + dy
+               -- single player (reflect top) -- TODO "stereo" version, 2 ctrls :)
+               if ball_y <= 0 then
+                       dy = - dy y = 0
+                       queue_beep ()
+               end
 
-               if ball_x >= 1 or ball_x <= 0 then dx = -dx end
-               if ball_y <= 0 then dy = - dy end
+               -- keep the ball in the field at all times
+               if ball_x >= 1 then ball_x = 1 end
+               if ball_x <= 0 then ball_x = 0 end
 
+               -- bottom edge
                if ball_y > 1 then
-                       local bar = ctrl[1]
+                       local bar = ctrl[1] -- get bar position
                        if math.abs (bar - ball_x) < 0.1 then
+                               -- reflect the ball
                                dy = - dy
                                ball_y = 1.0
-                               dx = dx + 0.1 * (bar - ball_x)
-                               -- queue sound (unless it's playing)
-                               if (pingsound > fps) then
-                                       pingsound = 0
-                               end
-                               phase = 0
+                               dx = dx - 0.04 * (bar - ball_x)
+                               -- make sure it's moving (not stuck on borders)
+                               if math.abs (dx) < 0.0001 then dx = 0.0001 end
+                               game_score = game_score + 1
+                               queue_beep ()
                        else
-                               -- game over
-                               lostsound = 0
+                               -- game over, reset game
+                               lost_sound = 0 -- re-start noise
                                ball_y = 0
-                               dx = 0.011
+                               game_score = 0
+                               dx = 0.00367
                        end
                end
        end
 
+       -- forward audio if processing is not in-place
+       for c = 1,#outs do
+               -- check if output and input buffers for this channel are identical
+               -- http://manual.ardour.org/lua-scripting/class_reference/#C:FloatArray
+               if ins[c] ~= outs[c] then
+                       -- fast (accelerated) copy
+                       -- http://manual.ardour.org/lua-scripting/class_reference/#ARDOUR:DSP
+                       ARDOUR.DSP.copy_vector (outs[c], ins[c], n_samples)
+               end
+       end
+
        -- simple synth -- TODO Optimize
-       if pingsound <= fps then
+       if ping_sound < fps then
+               -- cache audio data buffers for direct access, later
+               local abufs = {}
+               for c = 1,#outs do
+                       abufs[c] = outs[c]:array()
+               end
+               -- simple sine synth with a sine-envelope
                for s = 1, n_samples do
-                       pingsound = pingsound + 1
-                       if pingsound > fps then goto note_end end
-                       phase = phase + pingnote
-                       local snd = 0.7 * math.sin(6.283185307 * phase) * math.sin (3.141592 * pingsound / fps)
+                       ping_sound = ping_sound + 1
+                       ping_phase = ping_phase + ping_pitch
+                       local snd = 0.7 * math.sin(6.283185307 * ping_phase) * math.sin (3.141592 * ping_sound / fps)
+                       -- add synthesized sound to all channels
                        for c = 1,#outs do
-                               -- don't copy this code, it's quick/dirty and not efficient
-                               outs[c]:array()[s] = outs[c]:array()[s] + snd
+                               abufs[c][s] = abufs[c][s] + snd
                        end
-                       ::note_end::
+                       -- break out of the loop when the sound finished
+                       if ping_sound >= fps then goto ping_end end
                end
+               ::ping_end::
        end
 
-       if lostsound <= 3 * fps then
+       if lost_sound < 3 * fps then
+               local abufs = {}
+               for c = 1,#outs do
+                       abufs[c] = outs[c]:array()
+               end
                for s = 1, n_samples do
-                       lostsound = lostsound + 1
-                       if lostsound > 3 * fps then goto noise_end end
+                       lost_sound = lost_sound + 1
+                       -- -12dBFS white noise
                        local snd = 0.5 * (math.random () - 0.5)
                        for c = 1,#outs do
-                               -- don't copy this code, it's quick/dirty and not efficient
-                               outs[c]:array()[s] = outs[c]:array()[s] + snd
+                               abufs[c][s] = abufs[c][s] + snd
                        end
-                       ::noise_end::
+                       if lost_sound >= 3 * fps then goto noise_end end
                end
+               ::noise_end::
        end
 
        if changed then
+               -- notify the GUI
+               local shmem = self:shmem () -- get the shared memory region
+               local state = shmem:to_float (0):array () -- "cast" into lua-table
+               -- update data..
                state[1] = ball_x
                state[2] = ball_y
+               state[3] = game_score
+               -- ..and wake up the UI
                self:queue_draw ()
        end
 end
 
 
+-------------------------------------------------------------------------------
+--- inline display
+
+local txt = nil -- cache font description (in GUI context)
+
 function render_inline (ctx, w, max_h)
-       local ctrl = CtrlPorts:array () -- get control port array (read/write)
-       local shmem = self:shmem () -- get shared memory region
-       local state = shmem:to_float (0):array () -- "cast" into lua-table
+       local ctrl = CtrlPorts:array () -- control port array
+       local shmem = self:shmem () -- shared memory region (game state from DSP)
+       local state = shmem:to_float (0):array () -- cast to lua-table
 
        if (w > max_h) then
                h = max_h
@@ -131,24 +214,45 @@ function render_inline (ctx, w, max_h)
                h = w
        end
 
+       -- prepare text rendering
+       if not txt then
+               -- allocate PangoLayout and set font
+               --http://manual.ardour.org/lua-scripting/class_reference/#Cairo:PangoLayout
+               txt = Cairo.PangoLayout (ctx, "Mono 10px")
+       end
+
+       -- ctx is-a http://manual.ardour.org/lua-scripting/class_reference/#Cairo:Context
+       -- 2D vector graphics http://cairographics.org/
+
        -- clear background
        ctx:rectangle (0, 0, w, h)
        ctx:set_source_rgba (.2, .2, .2, 1.0)
        ctx:fill ()
 
+       -- print the current score
+       if (state[3] > 0) then
+               txt:set_text (string.format ("%.0f", state[3]));
+               local tw, th = txt:get_pixel_size ()
+               ctx:set_source_rgba (1, 1, 1, 1.0)
+               ctx:move_to (w - tw - 3, 3)
+               txt:show_in_cairo_context (ctx)
+       end
+
+       -- prepare line and dot rendering
+       ctx:set_line_cap (Cairo.LineCap.Round)
+       ctx:set_line_width (3.0)
+       ctx:set_source_rgba (.8, .8, .8, 1.0)
+
        -- display bar
        local bar_width = w * .1
        local bar_space = w - bar_width
 
-       ctx:set_line_cap (Cairo.LineCap.Round)
-       ctx:set_source_rgba (.8, .8, .8, 1.0)
-       ctx:set_line_width (3.0)
        ctx:move_to (bar_space * ctrl[1], h - 3)
        ctx:rel_line_to (bar_width, 0)
        ctx:stroke ()
 
        -- display ball
-       ctx:move_to (state[1] * w, state[2] * (h - 5))
+       ctx:move_to (1 + state[1] * (w - 3), state[2] * (h - 5))
        ctx:close_path ()
        ctx:stroke ()