summaryrefslogtreecommitdiff
path: root/scripts/tomsloop.lua
blob: 4c04d32fbddf333031039cddbfb25d7e8e647292 (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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
ardour { ["type"] = "EditorAction", name = "Tom's Loop",
	license     = "MIT",
	author      = "Ardour Team",
	description = [[Bounce the loop-range of all non muted audio tracks, paste N times at playhead]]
}

-- for minimal configuration in dialogue
function action_params ()
	return { ["times"]   = { title = "Number of copies to add", default = "1"}, }
end

-- main method, every custom (i.e. non-ardour) method must be defined *inside* factory()
function factory (params) return function ()

-- when this script is called as an action, the output will be printed to the ardour log window
	function print_help()
		print("")
		print("---------------------------------------------------------------------")
		print("")
		print("Manual for \"Tom’s Loop\" Ardour Lua Script")
		print("")
		print("---------------------------------------------------------------------")
		print("---------------------------------------------------------------------")
		print("")
		print("Table of Contents")
		print("")
		print("1. The first test")
		print("2. Using mute and solo")
		print("3. Combining region clouds to a defined length")
		print("")
		print("Abstract: This script for Ardour (>=4.7 git) operates on the time")
		print("line. It allows to copy and combine specific portions within the loop")
		print("range to a later point on the time line with one single action")
		print("command. Everything that can be heard within the loop range is")
		print("considered for this process, namely non-muted regions on non-muted or")
		print("soloed tracks that are fully or partially inside the loop range. This")
		print("still sounds a bit abstract and will be more obvious with the")
		print("following example cases of use.")
		print("")
		print("For convenience, it’s recommended to bind the script to a keyboard")
		print("shortcut in order to quickly and easily access the \"Tom’s Loop\"")
		print("scripted action.")
		print("")
		print("-Open dialog \"Script Manager\" via menu Edit/Scripted Actions/Script")
		print("Manager")
		print("")
		print("-In tab \"Action Scripts\", select a line and press button \"Add/Set\"")
		print("")
		print("-In dialog \"Add Lua Action\", select \"Tom’s Loop\" from the drop down")
		print("menu and hit \"Add\"")
		print("")
		print("-In dialog \"Set Script Parameter\" just hit \"Add\" again")
		print("")
		print("-Close dialog \"Script Manager\"")
		print("")
		print("-Open dialog \"Bindings Editor\" via menu Window/Bindings Editor")
		print("")
		print("-In tab \"Editor\", expand \"Editor\", look for entry \"Tom’s loop\",")
		print("select it")
		print("")
		print("-Hit the keyboard shortcut to assign to this scripted action")
		print("")
		print("-Close dialog \"Key Bindings\"")
		print("")
		print("An alternative way to quickly access a scripted action is to enable")
		print("\"Action Script Button Visibility\" in \"Preferences/GUI\".")
		print("")
		print("---------------------------------------------------------------------")
		print("")
		print("1. The first test")
		print("")
		print("---------------------------------------------------------------------")
		print("")
		print("-Record a short sequence of audio input or import a wave file to a")
		print("track to get a region")
		print("")
		print("-Set a loop range inside that one region")
		print("")
		print("-Place the playhead after the loop range, possibly after the region,")
		print("non-rolling")
		print("")
		print("     _L====L_              V")
		print(" .____|____|____________.  |")
		print(" |R1__|_x__|____________|  |")
		print("")
		print("-Call \"Tom’s Loop\" via the previously created shortcut")
		print("")
		print("This results in a new region created at the playhead, with the length")
		print("of the loop range, containing audio of the original region. The")
		print("playhead moved to the end of this new region so that subsequent calls")
		print("to \"Tom’s Loop\" will result in a gap less series of regions.")
		print("")
		print("     _L====L_               --> V")
		print(" .____|____|____________.  .____|")
		print(" |R1__|_x__|____________|  |_x__|")
		print("")
		print("-Repeat calling \"Tom’s Loop\"")
		print("")
		print("This creates multiple copies of the loop range to line up one after")
		print("each other.")
		print("")
		print("     _L====L_                         --> V")
		print(" .____|____|____________.  .______________|")
		print(" |R1__|_x__|____________|  |_x__|_x__|_x__|")
		print("")
		print("-Set a different loop range and call \"Tom’s Loops\" again")
		print("")
		print("This will create a new region with the length of the new loop range")
		print("at the playhead.")
		print("")
		print("        _L=======L_                           --> V")
		print(" ._______|_______|______.  .______________________|")
		print(" |R1_____|_X_____|______|  |_x__|_x__|_x__|_X_____|")
		print("")
		print("By using \"Tom’s Loop\", the loop range - which can be easily set with")
		print("the handles - and the playhead it’s easy to create any sequence of")
		print("existing regions on the time line. This can be useful during the")
		print("arrangement phase where macro parts of the session are already")
		print("temporally layed out (in the loop) but not part of the final")
		print("arrangement yet. The process is non-destructive in a sense that the")
		print("existing regions layout in the current loop range won’t be touched or")
		print("replaced. The newly created regions are immediately visible on the")
		print("time line at the playhead position.")
		print("")
		print("")
		print("---------------------------------------------------------------------")
		print("")
		print("2. Using mute and solo")
		print("")
		print("---------------------------------------------------------------------")
		print("")
		print("Creating a sequence of regions like described above respects the")
		print("current mute and solo state of a track. Variations of the loop are")
		print("thus easy to create, further supporting the arrangement process.")
		print("")
		print("      _L====L_                         --> V")
		print("  .____|____|____________.  ._________.    |")
		print("  |R1__|_x__|____________|  |_x__|_x__|    |")
		print(" .__|R2|_y__|________|_.    |_y__|_________|")
		print(" |R3___|_z__|__________|         |_z__|_z__|")
		print("")
		print("")
		print("---------------------------------------------------------------------")
		print("")
		print("3. Combining region clouds to a defined length")
		print("")
		print("---------------------------------------------------------------------")
		print("")
		print("Multiple small regions say on a percussive track can be simplified")
		print("for later arrangement keeping the temporal relations by combining")
		print("them. Using \"Tom’s Loop\", the resulting regions will not only combine")
		print("the regions but also automatically extend or shrink the new regions")
		print("start and end point so that it is exactly of the wished length equal")
		print("to the loop range.")
		print("")
		print("_L======================L_                            --> V")
		print(" |   .____  .___.  _____|_______.  .______________________|")
		print(" |   |R1_|  |R2_|  |R3__|_______|  |______________________|")
		print("")
		print("See also: Lua Action Bounce+Replace Regions")
		print("")
		print("")
	end -- print_help()

	-- get options
	local p = params or {}
	local n_paste  = tonumber (p["times"] or 1)
	assert (n_paste > 0)

	local proc     = ARDOUR.LuaAPI.nil_proc () -- bounce w/o processing
	local itt      = ARDOUR.InterThreadInfo () -- bounce progress info (unused)

	local loop     = Session:locations ():auto_loop_location ()
	local playhead = Session:transport_frame ()

	-- make sure we have a loop, and the playhead (edit point) is after it
	if not loop then
		print_help();
		print ("Error: A Loop range must be set.")
		goto errorout
	end
	assert (loop:start () < loop:_end ())
	if loop:_end () >= playhead then
		print_help();
		print ("Error: The Playhead (paste point) needs to be after the loop.")
		goto errorout
	end

	-- prepare undo operation
	Session:begin_reversible_command ("Tom's Loop")
	local add_undo = false -- keep track if something has changed

	-- prefer solo'ed tracks
	local soloed_track_found = false
	for route in Session:get_tracks ():iter () do
		if route:soloed () then
			soloed_track_found = true
			break
		end
	end

	-- count regions that are bounced
	local n_regions_created = 0

	-- loop over all tracks in the session
	for route in Session:get_tracks ():iter () do
		if soloed_track_found then
			-- skip not soloed tracks
			if not route:soloed () then
				goto continue
			end
		end

		-- skip muted tracks (also applies to soloed + muted)
		if route:muted () then
			goto continue
		end

		-- at this point the track is either soloed (if at least one track is soloed)
		-- or not muted (if no track is soloed)

		-- test if bouncing is possible
		local track = route:to_track ()
		if not track:bounceable (proc, false) then
			goto continue
		end

		-- only audio tracks
		local playlist = track:playlist ()
		if playlist:data_type ():to_string () ~= "audio" then
			goto continue
		end

		-- check if there is at least one unmuted region in the loop-range
		local reg_unmuted_count = 0
		for reg in playlist:regions_touched (loop:start (), loop:_end ()):iter () do
			if not reg:muted() then
				reg_unmuted_count = reg_unmuted_count + 1
			end
		end

		if reg_unmuted_count < 1 then
			goto continue
		end

		-- clear existing changes, prepare "diff" of state for undo
		playlist:to_stateful ():clear_changes ()

		-- do the actual work
		local region = track:bounce_range (loop:start (), loop:_end (), itt, proc, false)
		playlist:add_region (region, playhead, n_paste, false, 0, 0, false)

		n_regions_created = n_regions_created + 1

		-- create a diff of the performed work, add it to the session's undo stack
		-- and check if it is not empty
		if not Session:add_stateful_diff_command (playlist:to_statefuldestructible ()):empty () then
			add_undo = true
		end

		::continue::
	end -- for all routes

	--advance playhead so it's just after the newly added regions
	if n_regions_created > 0 then
		Session:request_locate((playhead + loop:length() * n_paste),false)
	end

	-- all done, commit the combined Undo Operation
	if add_undo then
		-- the 'nil' Command here mean to use the collected diffs added above
		Session:commit_reversible_command (nil)
	else
		Session:abort_reversible_command ()
	end

	print ("bounced " .. n_regions_created .. " regions from loop range (" .. loop:length() ..  " frames) to playhead @ frame # " .. playhead)
	::errorout::
end -- end of anonymous action script function
end -- end of script factory


function icon (params) return function (ctx, width, height)
	local x = width * .5
	local y = height * .5
	local r = math.min (x, y)

	ctx:set_line_width (1)

	function stroke_outline ()
		ctx:set_source_rgba (0, 0, 0, 1)
		ctx:stroke_preserve ()
		ctx:set_source_rgba (1, 1, 1, 1)
		ctx:fill ()
	end

	ctx:rectangle (x - r * .6, y - r * .05, r * .6, r * .3)
	stroke_outline ()

	ctx:arc (x, y, r * .61, math.pi, 0.2 * math.pi)
	ctx:arc_negative (x, y, r * .35, 0.2 * math.pi, math.pi);
	stroke_outline ()

	function arc_arrow (rad, ang)
		return x - rad * math.sin (ang * 2.0 * math.pi), y - rad * math.cos (ang * 2.0 * math.pi)
	end

	ctx:move_to (arc_arrow (r * .36, .72))
	ctx:line_to (arc_arrow (r * .17, .72))
	ctx:line_to (arc_arrow (r * .56, .60))
	ctx:line_to (arc_arrow (r * .75, .72))
	ctx:line_to (arc_arrow (r * .62, .72))

	ctx:set_source_rgba (0, 0, 0, 1)
	ctx:stroke_preserve ()
	ctx:close_path ()
	ctx:set_source_rgba (1, 1, 1, 1)
	ctx:fill ()
end end