summaryrefslogtreecommitdiff
path: root/scripts/_pong.lua
blob: acf3c010c9d10ae17625b0e91d55b1bd49d3d1b9 (plain)
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
ardour {
	["type"]    = "dsp",
	name        = "a-Pong",
	category    = "Toy",
	license     = "MIT",
	author      = "Ardour Lua Task Force",
	description = [[A console classic for your console]]
}

-- return possible i/o configurations
function dsp_ioconfig ()
	-- -1, -1 = any number of channels as long as input and output count matches
	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


-- 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)
	-- 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
	ping_pitch = 752 / rate
	ball_x = 0.5
	ball_y = 0
	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 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 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 game_time > fps and ctrl[2] <= 0 do
		changed = true
		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

		-- single player (reflect top) -- TODO "stereo" version, 2 ctrls :)
		if ball_y <= 0 then
			dy = - dy y = 0
			queue_beep ()
		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] -- get bar position
			if math.abs (bar - ball_x) < 0.1 then
				-- reflect the ball
				dy = - dy
				ball_y = 1.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, reset game
				lost_sound = 0 -- re-start noise
				ball_y = 0
				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 not ins[c]:sameinstance (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 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
			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
				abufs[c][s] = abufs[c][s] + snd
			end
			-- break out of the loop when the sound finished
			if ping_sound >= fps then goto ping_end end
		end
		::ping_end::
	end

	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
			lost_sound = lost_sound + 1
			-- -12dBFS white noise
			local snd = 0.5 * (math.random () - 0.5)
			for c = 1,#outs do
				abufs[c][s] = abufs[c][s] + snd
			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 () -- 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
	else
		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:move_to (bar_space * ctrl[1], h - 3)
	ctx:rel_line_to (bar_width, 0)
	ctx:stroke ()

	-- display ball
	ctx:move_to (1 + state[1] * (w - 3), state[2] * (h - 5))
	ctx:close_path ()
	ctx:stroke ()

	return {w, h}
end