1ardour {
2	["type"] = "EditorAction",
3	name     = "Swing It (Rubberband)",
4	license  = "MIT",
5	author   = "Ardour Team",
6description = [[
7Create a 'swing feel' in selected regions.
8
9The beat position of selected audio regions is analyzed,
10then the audio is time-stretched, moving 8th notes back in
11time while keeping 1/4-note beats in place to produce
12a rhythmic swing style.
13
14(This script also servers as example for both VAMP
15analysis as well as Rubberband region stretching.)
16
17Kudos to Chris Cannam.
18]]
19}
20
21function factory () return function ()
22
23	-- helper function --
24	-- there is currently no direct way to find the track
25	-- corresponding to a [selected] region
26	function find_track_for_region (region_id)
27		for route in Session:get_tracks ():iter () do
28			local track = route:to_track ()
29			local pl = track:playlist ()
30			if not pl:region_by_id (region_id):isnil () then
31				return track
32			end
33		end
34		assert (0) -- can't happen, region must be in a playlist
35	end
36
37	-- get Editor selection
38	-- http://manual.ardour.org/lua-scripting/class_reference/#ArdourUI:Editor
39	-- http://manual.ardour.org/lua-scripting/class_reference/#ArdourUI:Selection
40	local sel = Editor:get_selection ()
41
42	-- Instantiate the QM BarBeat Tracker
43	-- see http://manual.ardour.org/lua-scripting/class_reference/#ARDOUR:LuaAPI:Vamp
44	-- http://vamp-plugins.org/plugin-doc/qm-vamp-plugins.html#qm-barbeattracker
45	local vamp = ARDOUR.LuaAPI.Vamp ("libardourvampplugins:qm-barbeattracker", Session:nominal_sample_rate ())
46
47	-- prepare undo operation
48	Session:begin_reversible_command ("Rubberband Regions")
49	local add_undo = false -- keep track if something has changed
50
51	-- for each selected region
52	-- http://manual.ardour.org/lua-scripting/class_reference/#ArdourUI:RegionSelection
53	for r in sel.regions:regionlist ():iter () do
54		-- "r" is-a http://manual.ardour.org/lua-scripting/class_reference/#ARDOUR:Region
55
56		-- test if it's an audio region
57		local ar = r:to_audioregion ()
58		if ar:isnil () then
59			goto next
60		end
61
62		-- create Rubberband stretcher
63		local rb = ARDOUR.LuaAPI.Rubberband (ar, false)
64
65		-- the rubberband-filter also implements the readable API.
66		-- https://manual.ardour.org/lua-scripting/class_reference/#ARDOUR:Readable
67		-- This allows to read from the master-source of the given audio-region.
68		-- Any prior time-stretch or pitch-shift are ignored when reading, however
69		-- processing retains the previous settings
70		local max_pos = rb:readable ():readable_length ()
71
72		-- prepare table to hold analysis results
73		-- the beat-map is a table holding audio-sample positions:
74		-- [from] = to
75		local beat_map = {}
76		local prev_beat = 0
77
78		-- construct a progress-dialog with cancle button
79		local pdialog = LuaDialog.ProgressWindow ("Rubberband", true)
80		-- progress dialog callbacks
81		function vamp_callback (_, pos)
82			return pdialog:progress (pos / max_pos, "Analyzing")
83		end
84		function rb_progress (_, pos)
85			return pdialog:progress (pos / max_pos, "Stretching")
86		end
87
88		-- run VAMP plugin, analyze the first channel of the audio-region
89		vamp:analyze (rb:readable (), 0, vamp_callback)
90
91		-- getRemainingFeatures returns a http://manual.ardour.org/lua-scripting/class_reference/#Vamp:Plugin:FeatureSet
92		-- get the first output. here: Beats, estimated beat locations & beat-number
93		-- "fl" is-a http://manual.ardour.org/lua-scripting/class_reference/#Vamp:Plugin:FeatureList
94		local fl = vamp:plugin ():getRemainingFeatures ():at (0)
95		local beatcount = 0
96		-- iterate over returned features
97		for f in fl:iter () do
98			-- "f" is-a  http://manual.ardour.org/lua-scripting/class_reference/#Vamp:Plugin:Feature
99			local fn = Vamp.RealTime.realTime2Frame (f.timestamp, Session:nominal_sample_rate ())
100			beat_map[fn] = fn -- keep beats (1/4 notes) unchanged
101			if prev_beat > 0 then
102				-- move the half beats (1/8th) back
103				local diff = (fn - prev_beat) / 2
104				beat_map[fn - diff] = fn - diff + diff / 3 -- moderate swing 2:1 (triplet)
105				--beat_map[fn - diff] = fn - diff + diff / 2 -- hard swing 3:1 (dotted 8th)
106				beatcount = beatcount + 1
107			end
108			prev_beat = fn
109		end
110		-- reset the plugin state (prepare for next iteration)
111		vamp:reset ()
112
113		if pdialog:canceled () then goto out end
114
115		-- skip regions shorter than a bar
116		if beatcount < 8 then
117			pdialog:done ()
118			goto next
119		end
120
121		-- configure rubberband stretch tool
122		rb:set_strech_and_pitch (1, 1) -- no overall stretching, no pitch-shift
123		rb:set_mapping (beat_map) -- apply beat-map from/to
124
125		-- now stretch the region
126		local nar = rb:process (rb_progress)
127
128		if pdialog:canceled () then goto out end
129
130		-- hide modal progress dialog and destroy it
131		pdialog:done ()
132		pdialog = nil
133
134		-- replace region
135		if not nar:isnil () then
136			print ("new audio region: ", nar:name (), nar:length ())
137			local track = find_track_for_region (r:to_stateful ():id ())
138			local playlist = track:playlist ()
139			playlist:to_stateful ():clear_changes () -- prepare undo
140			playlist:remove_region (r)
141			playlist:add_region (nar, r:position (), 1, false, 0, 0, false)
142			-- create a diff of the performed work, add it to the session's undo stack
143			-- and check if it is not empty
144			if not Session:add_stateful_diff_command (playlist:to_statefuldestructible ()):empty () then
145				add_undo = true
146			end
147		end
148
149		::next::
150	end
151
152	::out::
153
154	-- all done, commit the combined Undo Operation
155	if add_undo then
156		-- the 'nil' Command here mean to use the collected diffs added above
157		Session:commit_reversible_command (nil)
158	else
159		Session:abort_reversible_command ()
160	end
161end end
162
163
164function icon (params) return function (ctx, width, height, fg)
165	local txt = Cairo.PangoLayout (ctx, "ArdourMono ".. math.ceil(width * .7) .. "px")
166	txt:set_text ("\u{266b}\u{266a}") -- 8th note symbols
167	local tw, th = txt:get_pixel_size ()
168	ctx:set_source_rgba (ARDOUR.LuaAPI.color_to_rgba (fg))
169	ctx:move_to (.5 * (width - tw), .5 * (height - th))
170	txt:show_in_cairo_context (ctx)
171end end
172