summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <carl@carlh.net>2011-12-27 01:42:49 +0000
committerCarl Hetherington <carl@carlh.net>2011-12-27 01:42:49 +0000
commit6304261b980419eaafde84bc32ff5e8f1c7aa236 (patch)
tree1261360e5eb22f7539a1a0afcc2d760ea017b7ad
parent0082e3364f7682ff41df52305cfff2cf7a861ef3 (diff)
Re-work layering in possibly debatable ways. Sketchy docs in doc/layering.
git-svn-id: svn://localhost/ardour2/branches/3.0@11088 d708f5d6-7413-0410-9779-e7cbd77b26cf
-rw-r--r--doc/layering/.gitignore3
-rw-r--r--doc/layering/basic-layering.svg322
-rw-r--r--doc/layering/build.sh9
-rw-r--r--doc/layering/explicit-layering1.svg322
-rw-r--r--doc/layering/explicit-layering2.svg322
-rw-r--r--doc/layering/layering.tex150
-rw-r--r--doc/layering/tricky-explicit-layering.svg323
-rw-r--r--gtk2_ardour/audio_streamview.cc19
-rw-r--r--gtk2_ardour/audio_time_axis.cc12
-rw-r--r--gtk2_ardour/editor.cc4
-rw-r--r--gtk2_ardour/editor.h2
-rw-r--r--gtk2_ardour/editor_drag.cc97
-rw-r--r--gtk2_ardour/editor_drag.h13
-rw-r--r--gtk2_ardour/enums.h3
-rw-r--r--gtk2_ardour/midi_time_axis.cc2
-rw-r--r--gtk2_ardour/route_time_axis.cc1
-rw-r--r--gtk2_ardour/session_option_editor.cc10
-rw-r--r--gtk2_ardour/streamview.cc18
-rw-r--r--gtk2_ardour/time_axis_view.cc37
-rw-r--r--gtk2_ardour/time_axis_view.h2
-rw-r--r--libs/ardour/ardour/ardour.h2
-rw-r--r--libs/ardour/ardour/playlist.h91
-rw-r--r--libs/ardour/ardour/region.h29
-rw-r--r--libs/ardour/ardour/region_sorters.h27
-rw-r--r--libs/ardour/ardour/session_configuration_vars.h3
-rw-r--r--libs/ardour/ardour/types.h8
-rw-r--r--libs/ardour/audio_diskstream.cc9
-rw-r--r--libs/ardour/enums.cc2
-rw-r--r--libs/ardour/playlist.cc701
-rw-r--r--libs/ardour/region.cc79
-rwxr-xr-xlibs/ardour/run-tests.sh2
-rw-r--r--libs/ardour/session_state.cc22
-rw-r--r--libs/ardour/test/playlist_layering_test.cc384
-rw-r--r--libs/ardour/test/playlist_layering_test.h34
-rw-r--r--libs/ardour/test/playlist_overlap_cache_test.cc119
-rw-r--r--libs/ardour/test/playlist_overlap_cache_test.h20
-rw-r--r--libs/ardour/test/test_needing_session.cc48
-rw-r--r--libs/ardour/test/test_needing_session.h16
-rw-r--r--libs/ardour/test/test_receiver.h37
-rw-r--r--libs/ardour/wscript2
-rw-r--r--manual/xml/working_with_layers.xml112
41 files changed, 2814 insertions, 604 deletions
diff --git a/doc/layering/.gitignore b/doc/layering/.gitignore
new file mode 100644
index 0000000000..debc120c45
--- /dev/null
+++ b/doc/layering/.gitignore
@@ -0,0 +1,3 @@
+*.aux
+*.pdf
+*.log
diff --git a/doc/layering/basic-layering.svg b/doc/layering/basic-layering.svg
new file mode 100644
index 0000000000..e8b1e64f17
--- /dev/null
+++ b/doc/layering/basic-layering.svg
@@ -0,0 +1,322 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="basic-layering.pdf">
+ <defs
+ id="defs4">
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Lend"
+ style="overflow:visible;">
+ <path
+ id="path3618"
+ style="font-size:12.0;fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(1.1) rotate(180) translate(1,0)" />
+ </marker>
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective10" />
+ <inkscape:perspective
+ id="perspective4058"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4089"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4120"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4151"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4365"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4386"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-6"
+ style="overflow:visible">
+ <path
+ id="path3618-4"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4449"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-5"
+ style="overflow:visible">
+ <path
+ id="path3618-1"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4668"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696-1"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.633643"
+ inkscape:cx="191.36241"
+ inkscape:cy="685.97592"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1280"
+ inkscape:window-height="949"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2816"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ units="mm"
+ spacingx="5mm"
+ spacingy="5mm" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow2Lend);color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 88.582677,432.28344 159.448823,0"
+ id="path3592" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="255.20872"
+ y="435.23877"
+ id="text4042"><tspan
+ sodipodi:role="line"
+ id="tspan4044"
+ x="255.20872"
+ y="435.23877">time</tspan></text>
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818"
+ width="70.866142"
+ height="35.433071"
+ x="88.58268"
+ y="308.2677" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2"
+ width="70.86615"
+ height="35.433071"
+ x="124.01574"
+ y="343.70078" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2"
+ width="70.86615"
+ height="35.433071"
+ x="177.16534"
+ y="379.13385" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2-4"
+ width="35.433086"
+ height="35.433071"
+ x="265.74802"
+ y="379.13385" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="119.61775"
+ y="330.26825"
+ id="text4320"><tspan
+ sodipodi:role="line"
+ id="tspan4322"
+ x="119.61775"
+ y="330.26825">A</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="155.36281"
+ y="365.51532"
+ id="text4324"><tspan
+ sodipodi:role="line"
+ id="tspan4326"
+ x="155.36281"
+ y="365.51532">B</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="208.36243"
+ y="400.94839"
+ id="text4328"><tspan
+ sodipodi:role="line"
+ id="tspan4330"
+ x="208.36243"
+ y="400.94839">C</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="279.05457"
+ y="400.94839"
+ id="text4332"><tspan
+ sodipodi:role="line"
+ id="tspan4334"
+ x="279.05457"
+ y="400.94839">D</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow2Lend);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 318.89764,432.28344 0,-124.01575"
+ id="path3592-8"
+ sodipodi:nodetypes="cc" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="307.30875"
+ y="301.75519"
+ id="text4406"><tspan
+ sodipodi:role="line"
+ id="tspan4408"
+ x="307.30875"
+ y="301.75519">layer</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,397.273 13.54507,0"
+ id="path4439" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="401.58279"
+ id="text4656"><tspan
+ sodipodi:role="line"
+ id="tspan4658"
+ x="328.09393"
+ y="401.58279">0</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 312.32197,361.87109 13.54507,0"
+ id="path4439-1" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.02194"
+ y="366.18088"
+ id="text4656-0-0"><tspan
+ sodipodi:role="line"
+ id="tspan4658-6-3"
+ x="328.02194"
+ y="366.18088">1</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,325.98423 13.54507,0"
+ id="path4439-1-6" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="329.49646"
+ id="text4742"><tspan
+ sodipodi:role="line"
+ id="tspan4744"
+ x="328.09393"
+ y="329.49646">2</tspan></text>
+ </g>
+</svg>
diff --git a/doc/layering/build.sh b/doc/layering/build.sh
new file mode 100644
index 0000000000..5abaaa5721
--- /dev/null
+++ b/doc/layering/build.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+for f in basic-layering explicit-layering1 explicit-layering2 tricky-explicit-layering; do
+ inkscape -z --export-area-drawing -f $f.svg --export-pdf $f.pdf
+done
+
+pdflatex layering.tex
+pdflatex layering.tex
+
diff --git a/doc/layering/explicit-layering1.svg b/doc/layering/explicit-layering1.svg
new file mode 100644
index 0000000000..2a5c921ce7
--- /dev/null
+++ b/doc/layering/explicit-layering1.svg
@@ -0,0 +1,322 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="explicit-layering.svg">
+ <defs
+ id="defs4">
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Lend"
+ style="overflow:visible;">
+ <path
+ id="path3618"
+ style="font-size:12.0;fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(1.1) rotate(180) translate(1,0)" />
+ </marker>
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective10" />
+ <inkscape:perspective
+ id="perspective4058"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4089"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4120"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4151"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4365"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4386"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-6"
+ style="overflow:visible">
+ <path
+ id="path3618-4"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4449"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-5"
+ style="overflow:visible">
+ <path
+ id="path3618-1"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4668"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696-1"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.633643"
+ inkscape:cx="191.36241"
+ inkscape:cy="670.78783"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1280"
+ inkscape:window-height="949"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2816"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ units="mm"
+ spacingx="5mm"
+ spacingy="5mm" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow2Lend);color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 88.582677,432.28344 159.448823,0"
+ id="path3592" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="255.20872"
+ y="435.23877"
+ id="text4042"><tspan
+ sodipodi:role="line"
+ id="tspan4044"
+ x="255.20872"
+ y="435.23877">time</tspan></text>
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818"
+ width="106.29921"
+ height="35.433071"
+ x="88.582687"
+ y="308.2677" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2"
+ width="88.582687"
+ height="35.433071"
+ x="124.01574"
+ y="343.70078" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2"
+ width="70.86615"
+ height="35.433071"
+ x="177.16534"
+ y="379.13385" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2-4"
+ width="35.433086"
+ height="35.433071"
+ x="265.74802"
+ y="379.13385" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="137.33429"
+ y="330.26825"
+ id="text4320"><tspan
+ sodipodi:role="line"
+ id="tspan4322"
+ x="137.33429"
+ y="330.26825">A</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="164.22108"
+ y="365.51532"
+ id="text4324"><tspan
+ sodipodi:role="line"
+ id="tspan4326"
+ x="164.22108"
+ y="365.51532">B</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="208.36243"
+ y="400.94839"
+ id="text4328"><tspan
+ sodipodi:role="line"
+ id="tspan4330"
+ x="208.36243"
+ y="400.94839">C</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="279.05457"
+ y="400.94839"
+ id="text4332"><tspan
+ sodipodi:role="line"
+ id="tspan4334"
+ x="279.05457"
+ y="400.94839">D</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow2Lend);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 318.89764,432.28344 0,-124.01575"
+ id="path3592-8"
+ sodipodi:nodetypes="cc" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="307.30875"
+ y="301.75519"
+ id="text4406"><tspan
+ sodipodi:role="line"
+ id="tspan4408"
+ x="307.30875"
+ y="301.75519">layer</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,397.273 13.54507,0"
+ id="path4439" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="401.58279"
+ id="text4656"><tspan
+ sodipodi:role="line"
+ id="tspan4658"
+ x="328.09393"
+ y="401.58279">0</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 312.32197,361.87109 13.54507,0"
+ id="path4439-1" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.02194"
+ y="366.18088"
+ id="text4656-0-0"><tspan
+ sodipodi:role="line"
+ id="tspan4658-6-3"
+ x="328.02194"
+ y="366.18088">1</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,325.98423 13.54507,0"
+ id="path4439-1-6" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="329.49646"
+ id="text4742"><tspan
+ sodipodi:role="line"
+ id="tspan4744"
+ x="328.09393"
+ y="329.49646">2</tspan></text>
+ </g>
+</svg>
diff --git a/doc/layering/explicit-layering2.svg b/doc/layering/explicit-layering2.svg
new file mode 100644
index 0000000000..4f71a40eb9
--- /dev/null
+++ b/doc/layering/explicit-layering2.svg
@@ -0,0 +1,322 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="explicit-layering2.svg">
+ <defs
+ id="defs4">
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Lend"
+ style="overflow:visible;">
+ <path
+ id="path3618"
+ style="font-size:12.0;fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(1.1) rotate(180) translate(1,0)" />
+ </marker>
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective10" />
+ <inkscape:perspective
+ id="perspective4058"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4089"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4120"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4151"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4365"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4386"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-6"
+ style="overflow:visible">
+ <path
+ id="path3618-4"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4449"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-5"
+ style="overflow:visible">
+ <path
+ id="path3618-1"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4668"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696-1"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.633643"
+ inkscape:cx="191.36241"
+ inkscape:cy="670.02843"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1280"
+ inkscape:window-height="949"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2816"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ units="mm"
+ spacingx="5mm"
+ spacingy="5mm" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow2Lend);color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 88.582677,432.28344 159.448823,0"
+ id="path3592" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="255.20872"
+ y="435.23877"
+ id="text4042"><tspan
+ sodipodi:role="line"
+ id="tspan4044"
+ x="255.20872"
+ y="435.23877">time</tspan></text>
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818"
+ width="106.29921"
+ height="35.433071"
+ x="88.58268"
+ y="343.70078" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2"
+ width="88.582687"
+ height="35.433071"
+ x="124.01575"
+ y="379.13385" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2"
+ width="70.86615"
+ height="35.433071"
+ x="177.16534"
+ y="308.2677" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2-4"
+ width="35.433086"
+ height="35.433071"
+ x="265.74802"
+ y="379.13385" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="137.33429"
+ y="365.70132"
+ id="text4320"><tspan
+ sodipodi:role="line"
+ id="tspan4322"
+ x="137.33429"
+ y="365.70132">A</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="164.22108"
+ y="400.94839"
+ id="text4324"><tspan
+ sodipodi:role="line"
+ id="tspan4326"
+ x="164.22108"
+ y="400.94839">B</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="208.36243"
+ y="330.08224"
+ id="text4328"><tspan
+ sodipodi:role="line"
+ id="tspan4330"
+ x="208.36243"
+ y="330.08224">C</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="279.05457"
+ y="400.94839"
+ id="text4332"><tspan
+ sodipodi:role="line"
+ id="tspan4334"
+ x="279.05457"
+ y="400.94839">D</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow2Lend);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 318.89764,432.28344 0,-124.01575"
+ id="path3592-8"
+ sodipodi:nodetypes="cc" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="307.30875"
+ y="301.75519"
+ id="text4406"><tspan
+ sodipodi:role="line"
+ id="tspan4408"
+ x="307.30875"
+ y="301.75519">layer</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,397.273 13.54507,0"
+ id="path4439" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="401.58279"
+ id="text4656"><tspan
+ sodipodi:role="line"
+ id="tspan4658"
+ x="328.09393"
+ y="401.58279">0</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 312.32197,361.87109 13.54507,0"
+ id="path4439-1" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.02194"
+ y="366.18088"
+ id="text4656-0-0"><tspan
+ sodipodi:role="line"
+ id="tspan4658-6-3"
+ x="328.02194"
+ y="366.18088">1</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,325.98423 13.54507,0"
+ id="path4439-1-6" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="329.49646"
+ id="text4742"><tspan
+ sodipodi:role="line"
+ id="tspan4744"
+ x="328.09393"
+ y="329.49646">2</tspan></text>
+ </g>
+</svg>
diff --git a/doc/layering/layering.tex b/doc/layering/layering.tex
new file mode 100644
index 0000000000..317aaeae43
--- /dev/null
+++ b/doc/layering/layering.tex
@@ -0,0 +1,150 @@
+\documentclass{article}
+\title{Region Layering}
+\author{}
+\date{}
+
+\usepackage{graphicx}
+\begin{document}
+\maketitle
+
+\section{Introduction}
+
+When regions overlap in time, we need to decide which one should be
+played. Ardour has a few options to set how this decision is made.
+
+
+\subsection{Layers}
+
+Each region on a playlist is on a \emph{layer}. All overlapping regions
+are on a unique layer, and when overlaps exist the highest-layered
+region is played. This is illustrated in Figure~\ref{fig:basic-layering}.
+
+\begin{figure}[ht]
+\begin{center}
+\includegraphics{basic-layering.pdf}
+\end{center}
+\caption{Basic region layering}
+\label{fig:basic-layering}
+\end{figure}
+
+Here we see that region $A$ overlaps $B$, $B$ overlaps $C$, and
+$D$ overlaps nothing. There are several ways in which these regions
+could be arranged; in the drawing, $A$ is on layer~2, $B$ on layer~1,
+$C$ and $D$ on layer~0. If this area is played back, region $A$ will
+play in its entirety, followed by the end part of region $B$, followed
+by the end part of region $C$, followed by the whole of region $D$.
+This follows the basic rule that, at any given point, the region on
+the highest layer will be played.
+
+
+\section{Choice of layering}
+
+There are two main decisions to be made with regards to how a playlist should be layered:
+
+\begin{itemize}
+\item Given overlapping regions, what order should they be layered in?
+\item When should layering be changed?
+\end{itemize}
+
+
+\subsection{Layering order}
+
+Ardour provides three-and-a-half ways to decide on the order in which
+regions are layered. The most basic choice is:
+
+\begin{itemize}
+\item \emph{Later is higher} --- regions which are later in time will
+ be on higher layers.
+\item \emph{Most recently added is higher} --- regions which were more
+ recently added to the playlist will be on higher layers.
+\item \emph{Most recently edited or added is higher} --- regions which
+ were more recently edited or added to the playlist will be on
+ higher layers.
+\end{itemize}
+
+This choice can be set per-session from the \emph{Session Properties} dialogue
+box.
+
+\subsubsection{Explicit ordering}
+
+There are also cases when none of these rules should apply. If, for
+example, you want to put a given region at the top of the stack (on
+the highest layer), this is possible using the region `raise to top'
+command. Following such a command (called an `explicit layering'),
+the regions on the playlist may no longer obey any of the standard
+ordering rules.
+
+This situation also arises when editing tracks using the `stacked' layer mode.
+In this mode, almost all layering is explicit. When starting a region drag,
+the other regions on a track spread apart vertically to allow the dragged
+region to be dropped in any position within the region stack. The normal
+layering rules will only be followed if a region is dropped on top of another;
+in all other cases, explicit layering will be used to put the region wherever
+it was dropped.
+
+\subsection{When to update layering}
+
+There are two distinct approaches to updating layering:
+
+\begin{itemize}
+\item Update whenever any region edit is performed.
+\item Update only when a region is edited such that a new overlap has been set up.
+\end{itemize}
+
+The approach to use is optional, and can be set in \emph{Session Properties}.
+
+This decision only has consequences when an explicit layering command has
+been used. Consider the case in Figure~\ref{fig:explicit-layering1}.
+
+\begin{figure}[ht]
+\begin{center}
+\includegraphics{explicit-layering1.pdf}
+\end{center}
+\caption{Explicit layering: stage 1}
+\label{fig:explicit-layering1}
+\end{figure}
+
+Given that arrangement, imagine that we perform a `raise to top' on region $C$.
+This results in the arrangement in Figure~\ref{fig:explicit-layering2}.
+
+\begin{figure}[ht]
+\begin{center}
+\includegraphics{explicit-layering2.pdf}
+\end{center}
+\caption{Explicit layering: stage 2}
+\label{fig:explicit-layering2}
+\end{figure}
+
+Imagine now that region $C$ is moved very slightly to the left, so
+that it still overlaps both $A$ and $B$. If we are updating whenever
+any region edit is performed, this will result in a relayer; the
+regions' arrangement will go back to that in
+Figure~\ref{fig:explicit-layering1}.
+
+If, on the other hand, we only relayer when a new overlap is set up,
+the region layering will remain as in
+Figure~\ref{fig:explicit-layering2}. Before the edit, regions $A$,
+$B$ and $C$ overlapped; after the edit, the situation is the same, so
+no relayering is performed.
+
+Another, more complex, example is shown in Figure~\ref{fig:tricky-explicit-layering}.
+
+\begin{figure}[ht]
+\begin{center}
+\includegraphics{tricky-explicit-layering.pdf}
+\end{center}
+\caption{More complex explicit layering}
+\label{fig:tricky-explicit-layering}
+\end{figure}
+
+% XXX: this makes no sense
+
+Here, imagine that $C$ has been moved to the top of the stack with an explicit
+`raise to top' command. Now consider an extension of $C$ so that its
+right-hand edge overlaps $D$. If we are relayering only on new overlaps, this
+case presents one new overlap (that of $C$ with $D$). In this case, $C$ is
+moved according to the current layering rules so that it is correct with
+respect to $D$. In addition, $A$ and $B$ are re-layered so that the relation
+of $C$ to $A$ and $B$ is preserved.
+
+\end{document}
diff --git a/doc/layering/tricky-explicit-layering.svg b/doc/layering/tricky-explicit-layering.svg
new file mode 100644
index 0000000000..f4e89f6f92
--- /dev/null
+++ b/doc/layering/tricky-explicit-layering.svg
@@ -0,0 +1,323 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="744.09448819"
+ height="1052.3622047"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.47 r22583"
+ sodipodi:docname="tricky-explicit-layering1.svg">
+ <defs
+ id="defs4">
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0.0"
+ refX="0.0"
+ id="Arrow2Lend"
+ style="overflow:visible;">
+ <path
+ id="path3618"
+ style="font-size:12.0;fill-rule:evenodd;stroke-width:0.62500000;stroke-linejoin:round;"
+ d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
+ transform="scale(1.1) rotate(180) translate(1,0)" />
+ </marker>
+ <inkscape:perspective
+ sodipodi:type="inkscape:persp3d"
+ inkscape:vp_x="0 : 526.18109 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_z="744.09448 : 526.18109 : 1"
+ inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
+ id="perspective10" />
+ <inkscape:perspective
+ id="perspective4058"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4089"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4120"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4151"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4365"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4386"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-6"
+ style="overflow:visible">
+ <path
+ id="path3618-4"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4449"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <marker
+ inkscape:stockid="Arrow2Lend"
+ orient="auto"
+ refY="0"
+ refX="0"
+ id="Arrow2Lend-5"
+ style="overflow:visible">
+ <path
+ id="path3618-1"
+ style="font-size:12px;fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round"
+ d="M 8.7185878,4.0337352 -2.2072895,0.01601326 8.7185884,-4.0017078 c -1.7454984,2.3720609 -1.7354408,5.6174519 -6e-7,8.035443 z"
+ transform="matrix(-1.1,0,0,-1.1,-1.1,0)" />
+ </marker>
+ <inkscape:perspective
+ id="perspective4668"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ <inkscape:perspective
+ id="perspective4696-1"
+ inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
+ inkscape:vp_z="1 : 0.5 : 1"
+ inkscape:vp_y="0 : 1000 : 0"
+ inkscape:vp_x="0 : 0.5 : 1"
+ sodipodi:type="inkscape:persp3d" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="2.633643"
+ inkscape:cx="191.36241"
+ inkscape:cy="670.78783"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="true"
+ inkscape:window-width="1280"
+ inkscape:window-height="949"
+ inkscape:window-x="0"
+ inkscape:window-y="25"
+ inkscape:window-maximized="1"
+ gridtolerance="10">
+ <inkscape:grid
+ type="xygrid"
+ id="grid2816"
+ empspacing="5"
+ visible="true"
+ enabled="true"
+ snapvisiblegridlinesonly="true"
+ units="mm"
+ spacingx="5mm"
+ spacingy="5mm" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1">
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow2Lend);color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 88.582677,432.28344 159.448823,0"
+ id="path3592" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="255.20872"
+ y="435.23877"
+ id="text4042"><tspan
+ sodipodi:role="line"
+ id="tspan4044"
+ x="255.20872"
+ y="435.23877">time</tspan></text>
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818"
+ width="106.29921"
+ height="35.433071"
+ x="88.582695"
+ y="343.70078" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2"
+ width="88.582687"
+ height="35.433071"
+ x="124.01575"
+ y="379.13385" />
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2-4"
+ width="70.866142"
+ height="35.433071"
+ x="230.31496"
+ y="379.13385" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="137.33429"
+ y="365.70132"
+ id="text4320"><tspan
+ sodipodi:role="line"
+ id="tspan4322"
+ x="137.33429"
+ y="365.70132">A</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="164.22108"
+ y="400.94839"
+ id="text4324"><tspan
+ sodipodi:role="line"
+ id="tspan4326"
+ x="164.22108"
+ y="400.94839">B</tspan></text>
+ <rect
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ id="rect2818-2-2"
+ width="44.545933"
+ height="35.433071"
+ x="177.16534"
+ y="308.2677" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="195.20232"
+ y="330.08224"
+ id="text4328"><tspan
+ sodipodi:role="line"
+ id="tspan4330"
+ x="195.20232"
+ y="330.08224">C</tspan></text>
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="261.33801"
+ y="400.94839"
+ id="text4332"><tspan
+ sodipodi:role="line"
+ id="tspan4334"
+ x="261.33801"
+ y="400.94839">D</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:url(#Arrow2Lend);visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 318.89764,432.28344 0,-124.01575"
+ id="path3592-8"
+ sodipodi:nodetypes="cc" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="307.30875"
+ y="301.75519"
+ id="text4406"><tspan
+ sodipodi:role="line"
+ id="tspan4408"
+ x="307.30875"
+ y="301.75519">layer</tspan></text>
+ <path
+ style="fill:none;stroke:#000000;stroke-width:0.62500000000000000;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;color:#000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;marker:none;marker-end:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,397.273 13.54507,0"
+ id="path4439" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="401.58279"
+ id="text4656"><tspan
+ sodipodi:role="line"
+ id="tspan4658"
+ x="328.09393"
+ y="401.58279">0</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 312.32197,361.87109 13.54507,0"
+ id="path4439-1" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.02194"
+ y="366.18088"
+ id="text4656-0-0"><tspan
+ sodipodi:role="line"
+ id="tspan4658-6-3"
+ x="328.02194"
+ y="366.18088">1</tspan></text>
+ <path
+ style="color:#000000;fill:none;stroke:#000000;stroke-width:0.625;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate"
+ d="m 311.84462,325.98423 13.54507,0"
+ id="path4439-1-6" />
+ <text
+ xml:space="preserve"
+ style="font-size:12px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:LMRoman12;-inkscape-font-specification:LMRoman12"
+ x="328.09393"
+ y="329.49646"
+ id="text4742"><tspan
+ sodipodi:role="line"
+ id="tspan4744"
+ x="328.09393"
+ y="329.49646">2</tspan></text>
+ </g>
+</svg>
diff --git a/gtk2_ardour/audio_streamview.cc b/gtk2_ardour/audio_streamview.cc
index a69f398ef4..6dbef4484d 100644
--- a/gtk2_ardour/audio_streamview.cc
+++ b/gtk2_ardour/audio_streamview.cc
@@ -809,13 +809,14 @@ AudioStreamView::update_contents_height ()
void
AudioStreamView::update_content_height (CrossfadeView* cv)
{
- if (_layer_display == Overlaid) {
-
+ switch (_layer_display) {
+ case Overlaid:
cv->set_y (0);
cv->set_height (height);
+ break;
- } else {
-
+ case Stacked:
+ case Expanded:
layer_t const inl = cv->crossfade->in()->layer ();
layer_t const outl = cv->crossfade->out()->layer ();
@@ -824,9 +825,13 @@ AudioStreamView::update_content_height (CrossfadeView* cv)
const double h = child_height ();
- cv->set_y ((_layers - high - 1) * h);
- cv->set_height ((high - low + 1) * h);
-
+ if (_layer_display == Stacked) {
+ cv->set_y ((_layers - high - 1) * h);
+ cv->set_height ((high - low + 1) * h);
+ } else {
+ cv->set_y (((_layers - high) * 2 - 1) * h);
+ cv->set_height (((high - low) * 2 + 1) * h);
+ }
}
}
diff --git a/gtk2_ardour/audio_time_axis.cc b/gtk2_ardour/audio_time_axis.cc
index c7ee43f4cb..b2a1d6bf10 100644
--- a/gtk2_ardour/audio_time_axis.cc
+++ b/gtk2_ardour/audio_time_axis.cc
@@ -87,15 +87,23 @@ AudioTimeAxisView::AudioTimeAxisView (PublicEditor& ed, Session* sess, Canvas& c
void
AudioTimeAxisView::set_route (boost::shared_ptr<Route> rt)
{
+ _route = rt;
+
+ /* RouteTimeAxisView::set_route() sets up some things in the View,
+ so it must be created before RouteTimeAxis::set_route() is
+ called.
+ */
+ _view = new AudioStreamView (*this);
+
RouteTimeAxisView::set_route (rt);
+ _view->apply_color (color (), StreamView::RegionColor);
+
// Make sure things are sane...
assert(!is_track() || is_audio_track());
subplugin_menu.set_name ("ArdourContextMenu");
- _view = new AudioStreamView (*this);
-
ignore_toggle = false;
if (is_audio_track()) {
diff --git a/gtk2_ardour/editor.cc b/gtk2_ardour/editor.cc
index fa3ea175ce..30bde56a68 100644
--- a/gtk2_ardour/editor.cc
+++ b/gtk2_ardour/editor.cc
@@ -2477,12 +2477,12 @@ Editor::get_state ()
* TimeAxisView may be 0. Layer index is the layer number if the TimeAxisView is valid and is
* in stacked region display mode, otherwise 0.
*/
-std::pair<TimeAxisView *, layer_t>
+std::pair<TimeAxisView *, double>
Editor::trackview_by_y_position (double y)
{
for (TrackViewList::iterator iter = track_views.begin(); iter != track_views.end(); ++iter) {
- std::pair<TimeAxisView*, int> const r = (*iter)->covers_y_position (y);
+ std::pair<TimeAxisView*, double> const r = (*iter)->covers_y_position (y);
if (r.first) {
return r;
}
diff --git a/gtk2_ardour/editor.h b/gtk2_ardour/editor.h
index 9ecc9c5da6..7f84176d61 100644
--- a/gtk2_ardour/editor.h
+++ b/gtk2_ardour/editor.h
@@ -1032,7 +1032,7 @@ class Editor : public PublicEditor, public PBD::ScopedConnectionList, public ARD
/* track views */
TrackViewList track_views;
- std::pair<TimeAxisView*, ARDOUR::layer_t> trackview_by_y_position (double);
+ std::pair<TimeAxisView*, double> trackview_by_y_position (double);
TimeAxisView* axis_view_from_route (boost::shared_ptr<ARDOUR::Route>) const;
TrackViewList get_tracks_for_range_action () const;
diff --git a/gtk2_ardour/editor_drag.cc b/gtk2_ardour/editor_drag.cc
index 020428a76b..06f602d5a0 100644
--- a/gtk2_ardour/editor_drag.cc
+++ b/gtk2_ardour/editor_drag.cc
@@ -485,9 +485,13 @@ RegionMotionDrag::start_grab (GdkEvent* event, Gdk::Cursor* cursor)
show_verbose_cursor_time (_last_frame_position);
- pair<TimeAxisView*, int> const tv = _editor->trackview_by_y_position (_drags->current_pointer_y ());
+ pair<TimeAxisView*, double> const tv = _editor->trackview_by_y_position (_drags->current_pointer_y ());
_last_pointer_time_axis_view = find_time_axis_view (tv.first);
_last_pointer_layer = tv.first->layer_display() == Overlaid ? 0 : tv.second;
+
+ if (tv.first->view()->layer_display() == Stacked) {
+ tv.first->view()->set_layer_display (Expanded);
+ }
}
double
@@ -554,7 +558,7 @@ RegionMotionDrag::compute_x_delta (GdkEvent const * event, framepos_t* pending_r
}
bool
-RegionMotionDrag::y_movement_allowed (int delta_track, layer_t delta_layer) const
+RegionMotionDrag::y_movement_allowed (int delta_track, double delta_layer) const
{
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
int const n = i->time_axis_view + delta_track;
@@ -569,8 +573,13 @@ RegionMotionDrag::y_movement_allowed (int delta_track, layer_t delta_layer) cons
return false;
}
- int const l = i->layer + delta_layer;
- if (delta_track == 0 && (l < 0 || l >= int (to->view()->layers()))) {
+ double const l = i->layer + delta_layer;
+
+ /* Note that we allow layer to be up to 0.5 below zero, as this is used by `Expanded'
+ mode to allow the user to place a region below another on layer 0.
+ */
+
+ if (delta_track == 0 && (l < -0.5 || l >= int (to->view()->layers()))) {
/* Off the top or bottom layer; note that we only refuse if the track hasn't changed.
If it has, the layers will be munged later anyway, so it's ok.
*/
@@ -588,7 +597,7 @@ RegionMotionDrag::motion (GdkEvent* event, bool first_move)
assert (!_views.empty ());
/* Find the TimeAxisView that the pointer is now over */
- pair<TimeAxisView*, int> const tv = _editor->trackview_by_y_position (_drags->current_pointer_y ());
+ pair<TimeAxisView*, double> const tv = _editor->trackview_by_y_position (_drags->current_pointer_y ());
/* Bail early if we're not over a track */
RouteTimeAxisView* rtv = dynamic_cast<RouteTimeAxisView*> (tv.first);
@@ -601,7 +610,7 @@ RegionMotionDrag::motion (GdkEvent* event, bool first_move)
/* Here's the current pointer position in terms of time axis view and layer */
int const current_pointer_time_axis_view = find_time_axis_view (tv.first);
- layer_t const current_pointer_layer = tv.first->layer_display() == Overlaid ? 0 : tv.second;
+ double const current_pointer_layer = tv.first->layer_display() == Overlaid ? 0 : tv.second;
/* Work out the change in x */
framepos_t pending_region_position;
@@ -609,7 +618,7 @@ RegionMotionDrag::motion (GdkEvent* event, bool first_move)
/* Work out the change in y */
int delta_time_axis_view = current_pointer_time_axis_view - _last_pointer_time_axis_view;
- int delta_layer = current_pointer_layer - _last_pointer_layer;
+ double delta_layer = current_pointer_layer - _last_pointer_layer;
if (!y_movement_allowed (delta_time_axis_view, delta_layer)) {
/* this y movement is not allowed, so do no y movement this time */
@@ -659,14 +668,24 @@ RegionMotionDrag::motion (GdkEvent* event, bool first_move)
confusion when dragging regions from non-zero layers onto different
tracks.
*/
- int this_delta_layer = delta_layer;
+ double this_delta_layer = delta_layer;
if (delta_time_axis_view != 0) {
this_delta_layer = - i->layer;
}
/* The TimeAxisView that this region is now on */
TimeAxisView* tv = _time_axis_views[i->time_axis_view + delta_time_axis_view];
+
+ /* Ensure it is moved from stacked -> expanded if appropriate */
+ if (tv->view()->layer_display() == Stacked) {
+ tv->view()->set_layer_display (Expanded);
+ }
+ /* We're only allowed to go -ve in layer on Expanded views */
+ if (tv->view()->layer_display() != Expanded && (i->layer + this_delta_layer) < 0) {
+ this_delta_layer = - i->layer;
+ }
+
/* Set height */
rv->set_height (tv->view()->child_height ());
@@ -695,10 +714,17 @@ RegionMotionDrag::motion (GdkEvent* event, bool first_move)
/* And adjust for the layer that it should be on */
StreamView* cv = tv->view ();
- if (cv->layer_display() == Stacked) {
+ switch (cv->layer_display ()) {
+ case Overlaid:
+ break;
+ case Stacked:
y += (cv->layers() - i->layer - 1) * cv->child_height ();
+ break;
+ case Expanded:
+ y += (cv->layers() - i->layer - 0.5) * 2 * cv->child_height ();
+ break;
}
-
+
/* Now move the region view */
rv->move (x_delta, y - rv->get_canvas_group()->property_y());
}
@@ -789,8 +815,19 @@ RegionMoveDrag::motion (GdkEvent* event, bool first_move)
}
void
-RegionMoveDrag::finished (GdkEvent *, bool movement_occurred)
+RegionMotionDrag::finished (GdkEvent *, bool)
{
+ for (vector<TimeAxisView*>::iterator i = _time_axis_views.begin(); i != _time_axis_views.end(); ++i) {
+ if ((*i)->view()->layer_display() == Expanded) {
+ (*i)->view()->set_layer_display (Stacked);
+ }
+ }
+}
+
+void
+RegionMoveDrag::finished (GdkEvent* ev, bool movement_occurred)
+{
+ RegionMotionDrag::finished (ev, movement_occurred);
if (!movement_occurred) {
/* just a click */
return;
@@ -924,6 +961,9 @@ RegionMoveDrag::finished_no_copy (
RegionSelection new_views;
PlaylistSet modified_playlists;
PlaylistSet frozen_playlists;
+ PlaylistSet relayer_suspended_playlists;
+
+ list<pair<boost::shared_ptr<Region>, double> > pending_relayers;
if (_brushing) {
/* all changes were made during motion event handlers */
@@ -942,7 +982,7 @@ RegionMoveDrag::finished_no_copy (
RegionView* rv = i->view;
RouteTimeAxisView* const dest_rtv = dynamic_cast<RouteTimeAxisView*> (_time_axis_views[i->time_axis_view]);
- layer_t const dest_layer = i->layer;
+ double const dest_layer = i->layer;
if (rv->region()->locked()) {
++i;
@@ -1002,9 +1042,10 @@ RegionMoveDrag::finished_no_copy (
boost::shared_ptr<Playlist> playlist = dest_rtv->playlist();
- if (dest_rtv->view()->layer_display() == Stacked) {
- rv->region()->set_layer (dest_layer);
- rv->region()->set_pending_explicit_relayer (true);
+ bool const explicit_relayer = dest_rtv->view()->layer_display() == Stacked || dest_rtv->view()->layer_display() == Expanded;
+
+ if (explicit_relayer) {
+ pending_relayers.push_back (make_pair (rv->region (), dest_layer));
}
/* freeze playlist to avoid lots of relayering in the case of a multi-region drag */
@@ -1024,6 +1065,9 @@ RegionMoveDrag::finished_no_copy (
playlist->clear_changes ();
}
+ relayer_suspended_playlists.insert (playlist);
+ playlist->suspend_relayer ();
+
rv->region()->set_position (where);
_editor->session()->add_command (new StatefulDiffCommand (rv->region()));
@@ -1068,10 +1112,18 @@ RegionMoveDrag::finished_no_copy (
_editor->selection->set (new_views);
}
- for (set<boost::shared_ptr<Playlist> >::iterator p = frozen_playlists.begin(); p != frozen_playlists.end(); ++p) {
+ for (PlaylistSet::iterator p = frozen_playlists.begin(); p != frozen_playlists.end(); ++p) {
(*p)->thaw();
}
+ for (PlaylistSet::iterator p = relayer_suspended_playlists.begin(); p != relayer_suspended_playlists.end(); ++p) {
+ (*p)->resume_relayer ();
+ }
+
+ for (list<pair<boost::shared_ptr<Region>, double> >::iterator i = pending_relayers.begin(); i != pending_relayers.end(); ++i) {
+ i->first->playlist()->relayer (i->first, i->second);
+ }
+
/* write commands for the accumulated diffs for all our modified playlists */
add_stateful_diff_commands_for_playlists (modified_playlists);
@@ -1137,9 +1189,8 @@ RegionMoveDrag::insert_region_into_playlist (
dest_playlist->add_region (region, where);
- if (dest_rtv->view()->layer_display() == Stacked) {
- region->set_layer (dest_layer);
- region->set_pending_explicit_relayer (true);
+ if (dest_rtv->view()->layer_display() == Stacked || dest_rtv->view()->layer_display() == Expanded) {
+ dest_playlist->relayer (region, dest_layer);
}
c.disconnect ();
@@ -1188,6 +1239,12 @@ RegionMoveDrag::aborted (bool movement_occurred)
void
RegionMotionDrag::aborted (bool)
{
+ for (vector<TimeAxisView*>::iterator i = _time_axis_views.begin(); i != _time_axis_views.end(); ++i) {
+ if ((*i)->view()->layer_display() == Expanded) {
+ (*i)->view()->set_layer_display (Stacked);
+ }
+ }
+
for (list<DraggingView>::const_iterator i = _views.begin(); i != _views.end(); ++i) {
RegionView* rv = i->view;
TimeAxisView* tv = &(rv->get_time_axis_view ());
@@ -1295,7 +1352,7 @@ RegionSpliceDrag::motion (GdkEvent* event, bool)
{
/* Which trackview is this ? */
- pair<TimeAxisView*, int> const tvp = _editor->trackview_by_y_position (_drags->current_pointer_y ());
+ pair<TimeAxisView*, double> const tvp = _editor->trackview_by_y_position (_drags->current_pointer_y ());
RouteTimeAxisView* tv = dynamic_cast<RouteTimeAxisView*> (tvp.first);
/* The region motion is only processed if the pointer is over
diff --git a/gtk2_ardour/editor_drag.h b/gtk2_ardour/editor_drag.h
index c8308c6910..a6f5088d43 100644
--- a/gtk2_ardour/editor_drag.h
+++ b/gtk2_ardour/editor_drag.h
@@ -249,8 +249,11 @@ struct DraggingView
* or -1 if it is not visible.
*/
int time_axis_view;
- /** layer that this region is currently being displayed on */
- ARDOUR::layer_t layer;
+ /** Layer that this region is currently being displayed on. This is a double
+ rather than a layer_t as we use fractional layers during drags to allow the user
+ to indicate a new layer to put a region on.
+ */
+ double layer;
double initial_y; ///< the initial y position of the view before any reparenting
framepos_t initial_position; ///< initial position of the region
framepos_t initial_end; ///< initial end position of the region
@@ -295,7 +298,7 @@ public:
virtual void start_grab (GdkEvent *, Gdk::Cursor *);
virtual void motion (GdkEvent *, bool);
- virtual void finished (GdkEvent *, bool) = 0;
+ virtual void finished (GdkEvent *, bool);
virtual void aborted (bool);
/** @return true if the regions being `moved' came from somewhere on the canvas;
@@ -306,13 +309,13 @@ public:
protected:
double compute_x_delta (GdkEvent const *, ARDOUR::framecnt_t *);
- bool y_movement_allowed (int, ARDOUR::layer_t) const;
+ bool y_movement_allowed (int, double) const;
bool _brushing;
ARDOUR::framepos_t _last_frame_position; ///< last position of the thing being dragged
double _total_x_delta;
int _last_pointer_time_axis_view;
- ARDOUR::layer_t _last_pointer_layer;
+ double _last_pointer_layer;
};
diff --git a/gtk2_ardour/enums.h b/gtk2_ardour/enums.h
index 92b2923894..680421e271 100644
--- a/gtk2_ardour/enums.h
+++ b/gtk2_ardour/enums.h
@@ -35,7 +35,8 @@ namespace Gnome {
enum LayerDisplay {
Overlaid,
- Stacked
+ Stacked,
+ Expanded
};
struct SelectionRect {
diff --git a/gtk2_ardour/midi_time_axis.cc b/gtk2_ardour/midi_time_axis.cc
index cbdf2919ae..c3df55a439 100644
--- a/gtk2_ardour/midi_time_axis.cc
+++ b/gtk2_ardour/midi_time_axis.cc
@@ -139,6 +139,8 @@ MidiTimeAxisView::set_route (boost::shared_ptr<Route> rt)
*/
RouteTimeAxisView::set_route (rt);
+ _view->apply_color (_color, StreamView::RegionColor);
+
subplugin_menu.set_name ("ArdourContextMenu");
if (!gui_property ("note-range-min").empty ()) {
diff --git a/gtk2_ardour/route_time_axis.cc b/gtk2_ardour/route_time_axis.cc
index 4feafd5667..581e3eb238 100644
--- a/gtk2_ardour/route_time_axis.cc
+++ b/gtk2_ardour/route_time_axis.cc
@@ -468,6 +468,7 @@ RouteTimeAxisView::build_display_menu ()
++overlaid;
break;
case Stacked:
+ case Expanded:
++stacked;
break;
}
diff --git a/gtk2_ardour/session_option_editor.cc b/gtk2_ardour/session_option_editor.cc
index 98ca09bb69..6ffb95b464 100644
--- a/gtk2_ardour/session_option_editor.cc
+++ b/gtk2_ardour/session_option_editor.cc
@@ -272,9 +272,17 @@ SessionOptionEditor::SessionOptionEditor (Session* s)
);
lm->add (LaterHigher, _("later is higher"));
- lm->add (MoveAddHigher, _("most recently moved or added is higher"));
+ lm->add (AddOrBoundsChangeHigher, _("most recently edited or added is higher"));
lm->add (AddHigher, _("most recently added is higher"));
+ add_option (_("Misc"), new BoolOption (
+ "relayer-on-all-edits",
+ _("Relayer regions after every edit"),
+ sigc::mem_fun (*_session_config, &SessionConfiguration::get_relayer_on_all_edits),
+ sigc::mem_fun (*_session_config, &SessionConfiguration::set_relayer_on_all_edits)
+ ));
+
+
add_option (_("Misc"), lm);
add_option (_("Misc"), new OptionEditorHeading (_("MIDI Options")));
diff --git a/gtk2_ardour/streamview.cc b/gtk2_ardour/streamview.cc
index 28905ee45f..2bc70cf997 100644
--- a/gtk2_ardour/streamview.cc
+++ b/gtk2_ardour/streamview.cc
@@ -57,7 +57,6 @@ StreamView::StreamView (RouteTimeAxisView& tv, ArdourCanvas::Group* background_g
, _samples_per_unit (_trackview.editor().get_current_zoom ())
, rec_updating(false)
, rec_active(false)
- , region_color(_trackview.color())
, stream_base_color(0xFFFFFFFF)
, _layers (1)
, _layer_display (Overlaid)
@@ -302,7 +301,7 @@ StreamView::playlist_layered (boost::weak_ptr<Track> wtr)
_layers = tr->playlist()->top_layer() + 1;
}
- if (_layer_display == Stacked) {
+ if (_layer_display == Stacked || _layer_display == Expanded) {
update_contents_height ();
update_coverage_frames ();
} else {
@@ -330,8 +329,6 @@ StreamView::playlist_switched (boost::weak_ptr<Track> wtr)
update_contents_height ();
update_coverage_frames ();
- tr->playlist()->set_explicit_relayering (_layer_display == Stacked);
-
/* draw it */
redisplay_track ();
@@ -569,10 +566,16 @@ StreamView::get_inverted_selectables (Selection& sel, list<Selectable*>& results
double
StreamView::child_height () const
{
- if (_layer_display == Stacked) {
+ switch (_layer_display) {
+ case Overlaid:
+ return height;
+ case Stacked:
return height / _layers;
+ case Expanded:
+ return height / (_layers * 2 + 1);
}
+ /* NOTREACHED */
return height;
}
@@ -589,6 +592,9 @@ StreamView::update_contents_height ()
case Stacked:
(*i)->set_y (height - ((*i)->region()->layer() + 1) * h);
break;
+ case Expanded:
+ (*i)->set_y (height - ((*i)->region()->layer() + 1) * 2 * h);
+ break;
}
(*i)->set_height (h);
@@ -600,6 +606,7 @@ StreamView::update_contents_height ()
i->rectangle->property_y2() = height;
break;
case Stacked:
+ case Expanded:
/* In stacked displays, the recregion is always at the top */
i->rectangle->property_y1() = 0;
i->rectangle->property_y2() = h;
@@ -614,7 +621,6 @@ StreamView::set_layer_display (LayerDisplay d)
_layer_display = d;
update_contents_height ();
update_coverage_frames ();
- _trackview.track()->playlist()->set_explicit_relayering (_layer_display == Stacked);
}
void
diff --git a/gtk2_ardour/time_axis_view.cc b/gtk2_ardour/time_axis_view.cc
index e71bbe4778..fe02a8bc15 100644
--- a/gtk2_ardour/time_axis_view.cc
+++ b/gtk2_ardour/time_axis_view.cc
@@ -1195,10 +1195,10 @@ TimeAxisView::color_handler ()
* TimeAxisView is non-0 if this object covers y, or one of its children does.
* If the covering object is a child axis, then the child is returned.
* TimeAxisView is 0 otherwise.
- * Layer index is the layer number if the TimeAxisView is valid and is in stacked
- * region display mode, otherwise 0.
+ * Layer index is the layer number (possibly fractional) if the TimeAxisView is valid
+ * and is in stacked or expanded region display mode, otherwise 0.
*/
-std::pair<TimeAxisView*, layer_t>
+std::pair<TimeAxisView*, double>
TimeAxisView::covers_y_position (double y)
{
if (hidden()) {
@@ -1208,15 +1208,30 @@ TimeAxisView::covers_y_position (double y)
if (_y_position <= y && y < (_y_position + height)) {
/* work out the layer index if appropriate */
- layer_t l = 0;
- if (layer_display () == Stacked && view ()) {
- /* compute layer */
- l = layer_t ((_y_position + height - y) / (view()->child_height ()));
- /* clamp to max layers to be on the safe side; sometimes the above calculation
- returns a too-high value */
- if (l >= view()->layers ()) {
- l = view()->layers() - 1;
+ double l = 0;
+ switch (layer_display ()) {
+ case Overlaid:
+ break;
+ case Stacked:
+ if (view ()) {
+ /* compute layer */
+ l = floor ((_y_position + height - y) / (view()->child_height ()));
+ /* clamp to max layers to be on the safe side; sometimes the above calculation
+ returns a too-high value */
+ if (l >= view()->layers ()) {
+ l = view()->layers() - 1;
+ }
+ }
+ break;
+ case Expanded:
+ if (view ()) {
+ int n = floor ((_y_position + height - y) / (view()->child_height ()));
+ l = n * 0.5 - 0.5;
+ if (l >= (view()->layers() - 0.5)) {
+ l = view()->layers() - 0.5;
+ }
}
+ break;
}
return std::make_pair (this, l);
diff --git a/gtk2_ardour/time_axis_view.h b/gtk2_ardour/time_axis_view.h
index 9771c8b200..52385b17aa 100644
--- a/gtk2_ardour/time_axis_view.h
+++ b/gtk2_ardour/time_axis_view.h
@@ -146,7 +146,7 @@ class TimeAxisView : public virtual AxisView
virtual void reset_visual_state ();
- std::pair<TimeAxisView*, ARDOUR::layer_t> covers_y_position (double);
+ std::pair<TimeAxisView*, double> covers_y_position (double);
virtual void step_height (bool);
diff --git a/libs/ardour/ardour/ardour.h b/libs/ardour/ardour/ardour.h
index 51101a50ad..5ce29d7580 100644
--- a/libs/ardour/ardour/ardour.h
+++ b/libs/ardour/ardour/ardour.h
@@ -63,8 +63,6 @@ namespace ARDOUR {
std::string translation_kill_path ();
bool translations_are_disabled ();
- const layer_t max_layer = UCHAR_MAX;
-
static inline microseconds_t get_microseconds () {
return (microseconds_t) jack_get_time();
}
diff --git a/libs/ardour/ardour/playlist.h b/libs/ardour/ardour/playlist.h
index e125905e17..f3bf11458d 100644
--- a/libs/ardour/ardour/playlist.h
+++ b/libs/ardour/ardour/playlist.h
@@ -44,6 +44,9 @@
#include "ardour/session_object.h"
#include "ardour/data_type.h"
+class PlaylistOverlapCacheTest;
+class PlaylistLayeringTest;
+
namespace ARDOUR {
class Session;
@@ -215,12 +218,6 @@ public:
void drop_regions ();
- bool explicit_relayering () const {
- return _explicit_relayering;
- }
-
- void set_explicit_relayering (bool e);
-
virtual boost::shared_ptr<Crossfade> find_crossfade (const PBD::ID &) const {
return boost::shared_ptr<Crossfade> ();
}
@@ -228,6 +225,10 @@ public:
framepos_t find_next_top_layer_position (framepos_t) const;
uint32_t combine_ops() const { return _combine_ops; }
+ void relayer (boost::shared_ptr<Region>, double);
+ void suspend_relayer ();
+ void resume_relayer ();
+
protected:
friend class Session;
@@ -288,17 +289,10 @@ public:
bool _frozen;
uint32_t subcnt;
PBD::ID _orig_track_id;
- uint64_t layer_op_counter;
framecnt_t freeze_length;
bool auto_partition;
uint32_t _combine_ops;
- /** true if relayering should be done using region's current layers and their `pending explicit relayer'
- * flags; otherwise false if relayering should be done using the layer-model (most recently moved etc.)
- * Explicit relayering is used by tracks in stacked regionview mode.
- */
- bool _explicit_relayering;
-
void init (bool hide);
bool holding_state () const {
@@ -361,16 +355,14 @@ public:
boost::shared_ptr<Playlist> cut (framepos_t start, framecnt_t cnt, bool result_is_hidden);
boost::shared_ptr<Playlist> copy (framepos_t start, framecnt_t cnt, bool result_is_hidden);
- int move_region_to_layer (layer_t, boost::shared_ptr<Region> r, int dir);
- void relayer ();
+ void relayer (boost::shared_ptr<Region>);
+ void relayer (RegionList const &);
void begin_undo ();
void end_undo ();
void unset_freeze_parent (Playlist*);
void unset_freeze_child (Playlist*);
- void timestamp_layer_op (boost::shared_ptr<Region>);
-
void _split_region (boost::shared_ptr<Region>, framepos_t position);
typedef std::pair<boost::shared_ptr<Region>, boost::shared_ptr<Region> > TwoRegions;
@@ -391,6 +383,71 @@ public:
with its constituent regions
*/
virtual void pre_uncombine (std::vector<boost::shared_ptr<Region> >&, boost::shared_ptr<Region>) {}
+
+private:
+ friend class ::PlaylistOverlapCacheTest;
+ friend class ::PlaylistLayeringTest;
+
+ /** A class which is used to store temporary (fractional)
+ * layer assignments for some regions.
+ */
+ class TemporaryLayers
+ {
+ public:
+ void set (boost::shared_ptr<Region>, double);
+ double get (boost::shared_ptr<Region>) const;
+
+ private:
+ typedef std::map<boost::shared_ptr<Region>, double> Map;
+ Map _map;
+ };
+
+ /** Class to sort by temporary layer, for use with std::list<>::sort() */
+ class SortByTemporaryLayer
+ {
+ public:
+ SortByTemporaryLayer (TemporaryLayers const & t)
+ : _temporary_layers (t) {}
+
+ bool operator() (boost::shared_ptr<Region> a, boost::shared_ptr<Region> b) const {
+ return _temporary_layers.get (a) < _temporary_layers.get (b);
+ }
+
+ private:
+ Playlist::TemporaryLayers const & _temporary_layers;
+ };
+
+ /** A cache of what overlaps what, for a given playlist in a given state.
+ * Divides a playlist up into time periods and notes which regions cover those
+ * periods, so that get() is reasonably quick.
+ */
+ class OverlapCache
+ {
+ public:
+ OverlapCache (Playlist *);
+
+ RegionList get (Evoral::Range<framepos_t>) const;
+
+ private:
+ std::pair<int, int> cache_indices (Evoral::Range<framepos_t>) const;
+
+ double _division_size;
+ std::vector<RegionList> _cache;
+ Evoral::Range<framepos_t> _range;
+
+ static int const _divisions;
+ };
+
+ TemporaryLayers compute_temporary_layers (RegionList const &);
+ void commit_temporary_layers (TemporaryLayers const &);
+
+ RegionList recursive_regions_touched (boost::shared_ptr<Region>, OverlapCache const &, boost::shared_ptr<Region>) const;
+ void recursive_regions_touched_sub (boost::shared_ptr<Region>, OverlapCache const &, boost::shared_ptr<Region>, RegionList &) const;
+
+ void timestamp_layer_op (LayerOp, boost::shared_ptr<Region>);
+ uint64_t layer_op_counter;
+
+ bool _relayer_suspended;
};
} /* namespace ARDOUR */
diff --git a/libs/ardour/ardour/region.h b/libs/ardour/ardour/region.h
index a9d18f95ca..4315ed7aa8 100644
--- a/libs/ardour/ardour/region.h
+++ b/libs/ardour/ardour/region.h
@@ -112,6 +112,7 @@ class Region
framepos_t start () const { return _start; }
framecnt_t length () const { return _length; }
layer_t layer () const { return _layer; }
+ Evoral::Range<framepos_t> bounds () const;
framecnt_t source_length(uint32_t n) const;
uint32_t max_source_level () const;
@@ -201,7 +202,7 @@ class Region
void cut_front (framepos_t new_position);
void cut_end (framepos_t new_position);
- void set_layer (layer_t l); /* ONLY Playlist can call this */
+ void set_layer (layer_t l); /* ONLY Playlist should call this */
void raise ();
void lower ();
void raise_to_top ();
@@ -251,8 +252,8 @@ class Region
virtual boost::shared_ptr<Region> get_parent() const;
- uint64_t last_layer_op() const { return _last_layer_op; }
- void set_last_layer_op (uint64_t when);
+ uint64_t last_layer_op (LayerOp) const;
+ void set_last_layer_op (LayerOp, uint64_t);
virtual bool is_dependent() const { return false; }
virtual bool depends_on (boost::shared_ptr<Region> /*other*/) const { return false; }
@@ -293,16 +294,13 @@ class Region
void invalidate_transients ();
- void set_pending_explicit_relayer (bool p) {
- _pending_explicit_relayer = p;
- }
+ void drop_sources ();
- bool pending_explicit_relayer () const {
- return _pending_explicit_relayer;
+ /** @return our bounds the last time our relayer() method was called */
+ Evoral::Range<framepos_t> last_relayer_bounds () const {
+ return Evoral::Range<framepos_t> (_last_relayer_bounds_from, _last_relayer_bounds_to);
}
- void drop_sources ();
-
protected:
friend class RegionFactory;
@@ -387,16 +385,17 @@ class Region
PBD::Property<float> _shift;
PBD::EnumProperty<PositionLockStyle> _position_lock_style;
+ /* XXX: could use a Evoral::Range<> but I'm too lazy to make PBD::Property serialize such a thing nicely */
+ PBD::Property<framepos_t> _last_relayer_bounds_from; ///< from of our bounds last time relayer() was called
+ PBD::Property<framepos_t> _last_relayer_bounds_to; ///< to of our bounds last time relayer() was called
+ PBD::Property<uint64_t> _last_layer_op_add;
+ PBD::Property<uint64_t> _last_layer_op_bounds_change;
+
framecnt_t _last_length;
framepos_t _last_position;
mutable RegionEditState _first_edit;
Timecode::BBT_Time _bbt_time;
- uint64_t _last_layer_op; ///< timestamp
-
- /** true if this region has had its layer explicitly set since the playlist last relayered */
- bool _pending_explicit_relayer;
-
void register_properties ();
void use_sources (SourceList const &);
diff --git a/libs/ardour/ardour/region_sorters.h b/libs/ardour/ardour/region_sorters.h
index ee34dcaafe..0e3203ef20 100644
--- a/libs/ardour/ardour/region_sorters.h
+++ b/libs/ardour/ardour/region_sorters.h
@@ -30,31 +30,24 @@ struct RegionSortByPosition {
}
};
-struct RegionSortByLastLayerOp {
+struct RegionSortByLayer {
bool operator() (boost::shared_ptr<Region> a, boost::shared_ptr<Region> b) {
- return a->last_layer_op() < b->last_layer_op();
+ return a->layer() < b->layer();
}
};
-struct RegionSortByLayer {
+struct RegionSortByAdd {
bool operator() (boost::shared_ptr<Region> a, boost::shared_ptr<Region> b) {
- return a->layer() < b->layer();
+ return (
+ (a->last_layer_op (LayerOpAdd) < b->last_layer_op (LayerOpAdd))
+ );
}
};
-struct RegionSortByLayerWithPending {
- bool operator () (boost::shared_ptr<Region> a, boost::shared_ptr<Region> b) {
-
- double p = a->layer ();
- if (a->pending_explicit_relayer()) {
- p += 0.5;
- }
-
- double q = b->layer ();
- if (b->pending_explicit_relayer()) {
- q += 0.5;
- }
-
+struct RegionSortByAddOrBounds {
+ bool operator() (boost::shared_ptr<Region> a, boost::shared_ptr<Region> b) {
+ uint64_t const p = std::max (a->last_layer_op (LayerOpAdd), a->last_layer_op (LayerOpBoundsChange));
+ uint64_t const q = std::max (b->last_layer_op (LayerOpAdd), b->last_layer_op (LayerOpBoundsChange));
return p < q;
}
};
diff --git a/libs/ardour/ardour/session_configuration_vars.h b/libs/ardour/ardour/session_configuration_vars.h
index 45300d3e0f..57f390f100 100644
--- a/libs/ardour/ardour/session_configuration_vars.h
+++ b/libs/ardour/ardour/session_configuration_vars.h
@@ -47,7 +47,8 @@ CONFIG_VARIABLE_SPECIAL(std::string, audio_search_path, "audio-search-path", "",
CONFIG_VARIABLE_SPECIAL(std::string, midi_search_path, "midi-search-path", "", search_path_expand)
CONFIG_VARIABLE (std::string, bwf_country_code, "bwf-country-code", "US")
CONFIG_VARIABLE (std::string, bwf_organization_code, "bwf-organization-code", "US")
-CONFIG_VARIABLE (LayerModel, layer_model, "layer-model", MoveAddHigher)
+CONFIG_VARIABLE (LayerModel, layer_model, "layer-model", AddOrBoundsChangeHigher)
+CONFIG_VARIABLE (bool, relayer_on_all_edits, "relayer-on-all-edits", true)
CONFIG_VARIABLE (std::string, auditioner_output_left, "auditioner-output-left", "default")
CONFIG_VARIABLE (std::string, auditioner_output_right, "auditioner-output-right", "default")
CONFIG_VARIABLE (bool, timecode_source_is_synced, "timecode-source-is-synced", true)
diff --git a/libs/ardour/ardour/types.h b/libs/ardour/ardour/types.h
index 7d5c5626e3..b5e5d89cc1 100644
--- a/libs/ardour/ardour/types.h
+++ b/libs/ardour/ardour/types.h
@@ -75,6 +75,7 @@ namespace ARDOUR {
static const framepos_t max_framepos = INT64_MAX;
static const framecnt_t max_framecnt = INT64_MAX;
+ static const layer_t max_layer = UINT32_MAX;
// a set of (time) intervals: first of pair is the offset of the start within the region, second is the offset of the end
typedef std::list<std::pair<frameoffset_t, frameoffset_t> > AudioIntervalResult;
@@ -410,7 +411,7 @@ namespace ARDOUR {
enum LayerModel {
LaterHigher,
- MoveAddHigher,
+ AddOrBoundsChangeHigher,
AddHigher
};
@@ -591,6 +592,11 @@ namespace ARDOUR {
FadeLogB
};
+ enum LayerOp {
+ LayerOpAdd,
+ LayerOpBoundsChange
+ };
+
} // namespace ARDOUR
diff --git a/libs/ardour/audio_diskstream.cc b/libs/ardour/audio_diskstream.cc
index 603179a8e2..83399d2962 100644
--- a/libs/ardour/audio_diskstream.cc
+++ b/libs/ardour/audio_diskstream.cc
@@ -1510,15 +1510,6 @@ AudioDiskstream::transport_stopped_wallclock (struct tm& when, time_t twhen, boo
}
i_am_the_modifier++;
-
- if (_playlist->explicit_relayering()) {
- /* We are in `explicit relayering' mode, so we must specify which layer this new region
- should end up on. Put it at the top.
- */
- region->set_layer (_playlist->top_layer() + 1);
- region->set_pending_explicit_relayer (true);
- }
-
_playlist->add_region (region, (*ci)->start, 1, non_layered());
i_am_the_modifier--;
diff --git a/libs/ardour/enums.cc b/libs/ardour/enums.cc
index 1f75f5ab43..12de366c79 100644
--- a/libs/ardour/enums.cc
+++ b/libs/ardour/enums.cc
@@ -265,7 +265,7 @@ setup_enum_writer ()
REGISTER (_CrossfadeModel);
REGISTER_ENUM (LaterHigher);
- REGISTER_ENUM (MoveAddHigher);
+ REGISTER_ENUM (AddOrBoundsChangeHigher);
REGISTER_ENUM (AddHigher);
REGISTER (_LayerModel);
diff --git a/libs/ardour/playlist.cc b/libs/ardour/playlist.cc
index 641aeff012..f2084fda0c 100644
--- a/libs/ardour/playlist.cc
+++ b/libs/ardour/playlist.cc
@@ -30,6 +30,7 @@
#include "pbd/convert.h"
#include "pbd/failed_constructor.h"
+#include "pbd/stacktrace.h"
#include "pbd/stateful_diff_command.h"
#include "pbd/xml++.h"
@@ -320,8 +321,8 @@ Playlist::init (bool hide)
_frozen = false;
layer_op_counter = 0;
freeze_length = 0;
- _explicit_relayering = false;
_combine_ops = 0;
+ _relayer_suspended = false;
_session.history().BeginUndoRedo.connect_same_thread (*this, boost::bind (&Playlist::begin_undo, this));
_session.history().EndUndoRedo.connect_same_thread (*this, boost::bind (&Playlist::end_undo, this));
@@ -487,6 +488,8 @@ Playlist::notify_region_moved (boost::shared_ptr<Region> r)
{
Evoral::RangeMove<framepos_t> const move (r->last_position (), r->length (), r->position ());
+ /* We could timestamp the region's layer op here, but we're doing it in region_bounds_changed */
+
if (holding_state ()) {
pending_range_moves.push_back (move);
@@ -503,6 +506,8 @@ Playlist::notify_region_moved (boost::shared_ptr<Region> r)
void
Playlist::notify_region_start_trimmed (boost::shared_ptr<Region> r)
{
+ timestamp_layer_op (LayerOpBoundsChange, r);
+
if (r->position() >= r->last_position()) {
/* trimmed shorter */
return;
@@ -526,6 +531,8 @@ Playlist::notify_region_start_trimmed (boost::shared_ptr<Region> r)
void
Playlist::notify_region_end_trimmed (boost::shared_ptr<Region> r)
{
+ timestamp_layer_op (LayerOpBoundsChange, r);
+
if (r->length() < r->last_length()) {
/* trimmed shorter */
}
@@ -552,6 +559,8 @@ Playlist::notify_region_added (boost::shared_ptr<Region> r)
as though it could be.
*/
+ timestamp_layer_op (LayerOpAdd, r);
+
if (holding_state()) {
pending_adds.insert (r);
pending_contents_change = true;
@@ -560,6 +569,7 @@ Playlist::notify_region_added (boost::shared_ptr<Region> r)
pending_contents_change = false;
RegionAdded (boost::weak_ptr<Region> (r)); /* EMIT SIGNAL */
ContentsChanged (); /* EMIT SIGNAL */
+ relayer (r);
}
}
@@ -577,34 +587,35 @@ Playlist::flush_notifications (bool from_undo)
in_flush = true;
+ /* We have:
+
+ pending_bounds: regions whose bounds position and/or length changes
+ pending_removes: regions which were removed
+ pending_adds: regions which were added
+ pending_length: true if the playlist length might have changed
+ pending_contents_change: true if there was almost any change in the playlist
+ pending_range_moves: details of periods of time that have been moved about (when regions have been moved)
+
+ */
+
if (!pending_bounds.empty() || !pending_removes.empty() || !pending_adds.empty()) {
regions_changed = true;
}
- /* we have no idea what order the regions ended up in pending
- bounds (it could be based on selection order, for example).
- so, to preserve layering in the "most recently moved is higher"
- model, sort them by existing layer, then timestamp them.
- */
-
- // RegionSortByLayer cmp;
- // pending_bounds.sort (cmp);
+ /* Make a list of regions that need relayering */
+ RegionList regions_to_relayer;
for (RegionList::iterator r = pending_bounds.begin(); r != pending_bounds.end(); ++r) {
- if (_session.config.get_layer_model() == MoveAddHigher) {
- timestamp_layer_op (*r);
- }
+ regions_to_relayer.push_back (*r);
dependent_checks_needed.insert (*r);
}
for (s = pending_removes.begin(); s != pending_removes.end(); ++s) {
remove_dependents (*s);
- // cerr << _name << " sends RegionRemoved\n";
RegionRemoved (boost::weak_ptr<Region> (*s)); /* EMIT SIGNAL */
}
for (s = pending_adds.begin(); s != pending_adds.end(); ++s) {
- // cerr << _name << " sends RegionAdded\n";
/* don't emit RegionAdded signal until relayering is done,
so that the region is fully setup by the time
anyone hear's that its been added
@@ -613,18 +624,14 @@ Playlist::flush_notifications (bool from_undo)
}
if (regions_changed || pending_contents_change) {
- if (!in_set_state) {
- relayer ();
- }
pending_contents_change = false;
- // cerr << _name << " sends 5 contents change @ " << get_microseconds() << endl;
ContentsChanged (); /* EMIT SIGNAL */
- // cerr << _name << "done contents change @ " << get_microseconds() << endl;
}
for (s = pending_adds.begin(); s != pending_adds.end(); ++s) {
(*s)->clear_changes ();
RegionAdded (boost::weak_ptr<Region> (*s)); /* EMIT SIGNAL */
+ regions_to_relayer.push_back (*s);
}
for (s = dependent_checks_needed.begin(); s != dependent_checks_needed.end(); ++s) {
@@ -639,6 +646,10 @@ Playlist::flush_notifications (bool from_undo)
RegionsExtended (pending_region_extensions);
}
+ if (!regions_to_relayer.empty ()) {
+ relayer (regions_to_relayer);
+ }
+
clear_pending ();
in_flush = false;
@@ -741,18 +752,11 @@ Playlist::flush_notifications (bool from_undo)
region->set_position (position);
- timestamp_layer_op (region);
-
regions.insert (upper_bound (regions.begin(), regions.end(), region, cmp), region);
all_regions.insert (region);
possibly_splice_unlocked (position, region->length(), region);
- if (!holding_state ()) {
- /* layers get assigned from XML state, and are not reset during undo/redo */
- relayer ();
- }
-
/* we need to notify the existence of new region before checking dependents. Ick. */
notify_region_added (region);
@@ -812,7 +816,6 @@ Playlist::flush_notifications (bool from_undo)
possibly_splice_unlocked (pos, -distance);
if (!holding_state ()) {
- relayer ();
remove_dependents (region);
}
@@ -865,11 +868,20 @@ Playlist::flush_notifications (bool from_undo)
}
}
+/** Go through each region on the playlist and cut them at start and end, removing the section between
+ * start and end if cutting == true. Regions that lie entirely within start and end are always
+ * removed.
+ */
void
Playlist::partition_internal (framepos_t start, framepos_t end, bool cutting, RegionList& thawlist)
{
RegionList new_regions;
+ /* Don't relayer regions that are created during this operation; leave them
+ on the same region as the original.
+ */
+ suspend_relayer ();
+
{
RegionLock rlock (this);
@@ -948,7 +960,7 @@ Playlist::flush_notifications (bool from_undo)
plist.add (Properties::start, current->start() + (pos2 - pos1));
plist.add (Properties::length, pos3 - pos2);
plist.add (Properties::name, new_name);
- plist.add (Properties::layer, regions.size());
+ plist.add (Properties::layer, current->layer());
plist.add (Properties::automatic, true);
plist.add (Properties::left_of_split, true);
plist.add (Properties::right_of_split, true);
@@ -967,7 +979,7 @@ Playlist::flush_notifications (bool from_undo)
plist.add (Properties::start, current->start() + (pos3 - pos1));
plist.add (Properties::length, pos4 - pos3);
plist.add (Properties::name, new_name);
- plist.add (Properties::layer, regions.size());
+ plist.add (Properties::layer, current->layer());
plist.add (Properties::automatic, true);
plist.add (Properties::right_of_split, true);
@@ -1005,7 +1017,7 @@ Playlist::flush_notifications (bool from_undo)
plist.add (Properties::start, current->start() + (pos2 - pos1));
plist.add (Properties::length, pos4 - pos2);
plist.add (Properties::name, new_name);
- plist.add (Properties::layer, regions.size());
+ plist.add (Properties::layer, current->layer());
plist.add (Properties::automatic, true);
plist.add (Properties::left_of_split, true);
@@ -1048,7 +1060,7 @@ Playlist::flush_notifications (bool from_undo)
plist.add (Properties::start, current->start());
plist.add (Properties::length, pos3 - pos1);
plist.add (Properties::name, new_name);
- plist.add (Properties::layer, regions.size());
+ plist.add (Properties::layer, current->layer());
plist.add (Properties::automatic, true);
plist.add (Properties::right_of_split, true);
@@ -1095,6 +1107,8 @@ Playlist::flush_notifications (bool from_undo)
for (RegionList::iterator i = new_regions.begin(); i != new_regions.end(); ++i) {
check_dependents (*i, false);
}
+
+ resume_relayer ();
}
boost::shared_ptr<Playlist>
@@ -1370,18 +1384,6 @@ Playlist::flush_notifications (bool from_undo)
add_region_internal (left, region->position());
add_region_internal (right, region->position() + before);
- uint64_t orig_layer_op = region->last_layer_op();
- for (RegionList::iterator i = regions.begin(); i != regions.end(); ++i) {
- if ((*i)->last_layer_op() > orig_layer_op) {
- (*i)->set_last_layer_op( (*i)->last_layer_op() + 1 );
- }
- }
-
- left->set_last_layer_op ( orig_layer_op );
- right->set_last_layer_op ( orig_layer_op + 1);
-
- layer_op_counter++;
-
finalize_split_region (region, left, right);
remove_region_internal (region);
@@ -1467,6 +1469,8 @@ Playlist::flush_notifications (bool from_undo)
if (what_changed.contains (Properties::position)) {
+ timestamp_layer_op (LayerOpBoundsChange, region);
+
/* remove it from the list then add it back in
the right place again.
*/
@@ -1506,13 +1510,8 @@ Playlist::flush_notifications (bool from_undo)
if (holding_state ()) {
pending_bounds.push_back (region);
} else {
- if (_session.config.get_layer_model() == MoveAddHigher) {
- /* it moved or changed length, so change the timestamp */
- timestamp_layer_op (region);
- }
-
notify_contents_changed ();
- relayer ();
+ relayer (region);
check_dependents (region, false);
}
}
@@ -1572,10 +1571,6 @@ Playlist::flush_notifications (bool from_undo)
notify_region_start_trimmed (region);
}
- /* don't notify about layer changes, since we are the only object that can initiate
- them, and we notify in ::relayer()
- */
-
if (what_changed.contains (our_interests)) {
save = true;
}
@@ -2134,6 +2129,7 @@ Playlist::flush_notifications (bool from_undo)
return -1;
}
+ suspend_relayer ();
freeze ();
plist = node.properties();
@@ -2195,9 +2191,7 @@ Playlist::flush_notifications (bool from_undo)
}
add_region (region, region->position(), 1.0);
-
- // So that layer_op ordering doesn't get screwed up
- region->set_last_layer_op( region->layer());
+
region->resume_property_changes ();
}
@@ -2218,6 +2212,7 @@ Playlist::flush_notifications (bool from_undo)
thaw ();
notify_contents_changed ();
+ resume_relayer ();
in_set_state--;
first_set_state = false;
@@ -2345,289 +2340,253 @@ Playlist::set_edit_mode (EditMode mode)
_edit_mode = mode;
}
-/********************
- * Region Layering
- ********************/
-
+/** Relayer a region. See the other relayer() methods for commentary. */
void
-Playlist::relayer ()
+Playlist::relayer (boost::shared_ptr<Region> region)
{
- /* never compute layers when changing state for undo/redo or setting from XML */
-
- if (in_update || in_set_state) {
+ if (_relayer_suspended) {
return;
}
+
+ RegionList r;
+ r.push_back (region);
+ relayer (r);
+}
- bool changed = false;
-
- /* Build up a new list of regions on each layer, stored in a set of lists
- each of which represent some period of time on some layer. The idea
- is to avoid having to search the entire region list to establish whether
- each region overlaps another */
-
- /* how many pieces to divide this playlist's time up into */
- int const divisions = 512;
-
- /* find the start and end positions of the regions on this playlist */
- framepos_t start = INT64_MAX;
- framepos_t end = 0;
- for (RegionList::const_iterator i = regions.begin(); i != regions.end(); ++i) {
- start = min (start, (*i)->position());
- end = max (end, (*i)->position() + (*i)->length());
- }
-
- /* hence the size of each time division */
- double const division_size = (end - start) / double (divisions);
-
- vector<vector<RegionList> > layers;
- layers.push_back (vector<RegionList> (divisions));
-
- /* we want to go through regions from desired lowest to desired highest layer,
- which depends on the layer model
- */
-
- RegionList copy = regions.rlist();
-
- /* sort according to the model and the layering mode that we're in */
-
- if (_explicit_relayering) {
+Playlist::TemporaryLayers
+Playlist::compute_temporary_layers (RegionList const & relayer_regions)
+{
+ TemporaryLayers temporary_layers;
+ OverlapCache cache (this);
- copy.sort (RegionSortByLayerWithPending ());
+ for (RegionList::const_iterator i = relayer_regions.begin(); i != relayer_regions.end(); ++i) {
- } else if (_session.config.get_layer_model() == MoveAddHigher || _session.config.get_layer_model() == AddHigher) {
+ /* current_overlaps: regions that overlap *i now */
+ RegionList current_overlaps = cache.get ((*i)->bounds ());
+ current_overlaps.remove (*i);
+
+ /* overlaps_to_preserve: regions that overlap *i now, but which aren't being
+ worked on during this relayer: these will have their relationship with
+ *i preserved.
+ */
+ RegionList overlaps_to_preserve;
- copy.sort (RegionSortByLastLayerOp ());
+ /* overlaps_to_check: regions that overlap *i now, and must be checked to
+ see if *i is still on the correct layer with respect to them (according
+ to current layering rules).
+ */
+ RegionList overlaps_to_check;
- }
+ if (_session.config.get_relayer_on_all_edits () || (*i)->last_layer_op (LayerOpAdd) > (*i)->last_layer_op (LayerOpBoundsChange)) {
+ /* We're configured to relayer on all edits, or this region has had
+ no edit since it was added to the playlist, so we're relayering
+ the whole lot; in this case there are no `overlaps_to_preserve'.
+ */
+ overlaps_to_check = current_overlaps;
+ } else {
+ /* We're only relayering new overlaps; find them */
+ RegionList last_overlaps = cache.get ((*i)->last_relayer_bounds ());
+ last_overlaps.remove (*i);
+ for (RegionList::const_iterator j = current_overlaps.begin(); j != current_overlaps.end(); ++j) {
+ if (find (last_overlaps.begin(), last_overlaps.end(), *j) == last_overlaps.end()) {
+ /* This is a new overlap, which must be checked */
+ overlaps_to_check.push_back (*j);
+ } else {
+ /* This is an existing overlap, which must be preserved */
+ overlaps_to_preserve.push_back (*j);
+ }
+ }
+ }
+ if (overlaps_to_check.empty ()) {
+ /* There are no overlaps to check, so just leave everything as it is */
+ continue;
+ }
- for (RegionList::iterator i = copy.begin(); i != copy.end(); ++i) {
+ /* Put *i on our overlaps_to_check_list */
+ overlaps_to_check.push_back (*i);
- /* reset the pending explicit relayer flag for every region, now that we're relayering */
- (*i)->set_pending_explicit_relayer (false);
+ /* And sort it according to the current layer model */
+ switch (_session.config.get_layer_model()) {
+ case LaterHigher:
+ overlaps_to_check.sort (RegionSortByPosition ());
+ break;
+ case AddOrBoundsChangeHigher:
+ overlaps_to_check.sort (RegionSortByAddOrBounds ());
+ break;
+ case AddHigher:
+ overlaps_to_check.sort (RegionSortByAdd ());
+ break;
+ }
- /* find the time divisions that this region covers; if there are no regions on the list,
- division_size will equal 0 and in this case we'll just say that
- start_division = end_division = 0.
+ /* Now find *i in our overlaps_to_check list; within this list it will be in the
+ right place wrt the current layering rules, so we can work out the layers of the
+ nearest regions below and above.
*/
- int start_division = 0;
- int end_division = 0;
-
- if (division_size > 0) {
- start_division = floor ( ((*i)->position() - start) / division_size);
- end_division = floor ( ((*i)->position() + (*i)->length() - start) / division_size );
- if (end_division == divisions) {
- end_division--;
- }
+ double previous_layer = -DBL_MAX;
+ double next_layer = DBL_MAX;
+ RegionList::const_iterator j = overlaps_to_check.begin();
+ while (*j != *i) {
+ previous_layer = temporary_layers.get (*j);
+ ++j;
}
- assert (divisions == 0 || end_division < divisions);
+ /* we must have found *i */
+ assert (j != overlaps_to_check.end ());
- /* find the lowest layer that this region can go on */
- size_t j = layers.size();
- while (j > 0) {
- /* try layer j - 1; it can go on if it overlaps no other region
- that is already on that layer
- */
+ ++j;
+ if (j != overlaps_to_check.end ()) {
+ next_layer = temporary_layers.get (*j);
+ }
- bool overlap = false;
- for (int k = start_division; k <= end_division; ++k) {
- RegionList::iterator l = layers[j-1][k].begin ();
- while (l != layers[j-1][k].end()) {
- if ((*l)->overlap_equivalent (*i)) {
- overlap = true;
- break;
- }
- l++;
- }
+ /* Now we know where *i and overlaps_to_preserve should go: between previous_layer and
+ next_layer.
+ */
- if (overlap) {
- break;
+ /* Recurse into overlaps_to_preserve to find dependents */
+ RegionList recursed_overlaps_to_preserve;
+
+ for (RegionList::const_iterator k = overlaps_to_preserve.begin(); k != overlaps_to_preserve.end(); ++k) {
+ recursed_overlaps_to_preserve.push_back (*k);
+ RegionList touched = recursive_regions_touched (*k, cache, *i);
+ for (RegionList::iterator m = touched.begin(); m != touched.end(); ++m) {
+ if (find (recursed_overlaps_to_preserve.begin(), recursed_overlaps_to_preserve.end(), *m) == recursed_overlaps_to_preserve.end()) {
+ recursed_overlaps_to_preserve.push_back (*m);
}
}
-
- if (overlap) {
- /* overlap, so we must use layer j */
- break;
- }
-
- --j;
}
- if (j == layers.size()) {
- /* we need a new layer for this region */
- layers.push_back (vector<RegionList> (divisions));
- }
+ /* Put *i into the overlaps_to_preserve list */
+ recursed_overlaps_to_preserve.push_back (*i);
- /* put a reference to this region in each of the divisions that it exists in */
- for (int k = start_division; k <= end_division; ++k) {
- layers[j][k].push_back (*i);
- }
+ /* Sort it by layer, so that we preserve layering */
+ recursed_overlaps_to_preserve.sort (SortByTemporaryLayer (temporary_layers));
- if ((*i)->layer() != j) {
- changed = true;
- }
+ /* Divide available space up into chunks so that we can relayer everything in that space */
+ double const space = (next_layer - previous_layer) / (recursed_overlaps_to_preserve.size() + 1);
- (*i)->set_layer (j);
+ /* And relayer */
+ int m = 1;
+ for (RegionList::const_iterator k = recursed_overlaps_to_preserve.begin(); k != recursed_overlaps_to_preserve.end(); ++k) {
+ temporary_layers.set (*k, previous_layer + space * m);
+ ++m;
+ }
}
- if (changed) {
- notify_layering_changed ();
- }
+ return temporary_layers;
}
-/* XXX these layer functions are all deprecated */
-
+/** Take a list of temporary layer indices and set up the layers of all regions
+ * based on them.
+ */
void
-Playlist::raise_region (boost::shared_ptr<Region> region)
+Playlist::commit_temporary_layers (TemporaryLayers const & temporary_layers)
{
- uint32_t top = regions.size() - 1;
- layer_t target = region->layer() + 1U;
+ /* Sort all the playlist's regions by layer, ascending */
+ RegionList all_regions = regions.rlist ();
+ all_regions.sort (SortByTemporaryLayer (temporary_layers));
- if (target >= top) {
- /* its already at the effective top */
- return;
- }
+ for (RegionList::iterator i = all_regions.begin(); i != all_regions.end(); ++i) {
- move_region_to_layer (target, region, 1);
-}
+ /* Go through the regions that we have already layered and hence work
+ out the maximum layer index that is in used at some point during
+ region *i.
+ */
+
+ layer_t max_layer_here = 0;
+ bool have_overlap = false;
+ for (RegionList::iterator j = all_regions.begin(); j != i; ++j) {
+ if ((*j)->overlap_equivalent (*i)) {
+ max_layer_here = max ((*j)->layer (), max_layer_here);
+ have_overlap = true;
+ }
+ }
-void
-Playlist::lower_region (boost::shared_ptr<Region> region)
-{
- if (region->layer() == 0) {
- /* its already at the bottom */
- return;
+ if (have_overlap) {
+ /* *i overlaps something, so put it on the next available layer */
+ (*i)->set_layer (max_layer_here + 1);
+ } else {
+ /* no overlap, so put on the bottom layer */
+ (*i)->set_layer (0);
+ }
}
- layer_t target = region->layer() - 1U;
-
- move_region_to_layer (target, region, -1);
+ notify_layering_changed ();
}
+/** Relayer a list of regions.
+ *
+ * Taking each region R in turn, this method examines the regions O that overlap R in time.
+ * If the session configuration option "relayer-on-all-moves" is false, we reduce O so that
+ * it contains only those regions with which new overlaps have been formed since the last
+ * relayer.
+ *
+ * We then change the layer of R and its indirect overlaps so that R meets the current
+ * Session layer model with respect to O. See doc/layering.
+ */
+
void
-Playlist::raise_region_to_top (boost::shared_ptr<Region> region)
+Playlist::relayer (RegionList const & relayer_regions)
{
- /* does nothing useful if layering mode is later=higher */
- switch (_session.config.get_layer_model()) {
- case LaterHigher:
+ if (_relayer_suspended) {
return;
- default:
- break;
}
- layer_t top = regions.size() - 1;
+ /* We do this in two parts: first; compute `temporary layer' indices for
+ regions on the playlist. These are (possibly) fractional indices, which
+ are a convenient means of working with things when you want to insert layers
+ between others.
+ */
- if (region->layer() >= top) {
- /* already on the top */
- return;
- }
+ TemporaryLayers temporary_layers = compute_temporary_layers (relayer_regions);
- move_region_to_layer (top, region, 1);
- /* mark the region's last_layer_op as now, so that it remains on top when
- doing future relayers (until something else takes over)
- */
- timestamp_layer_op (region);
+ /* Second, we fix up these temporary layers and `commit' them by writing
+ them to the regions involved.
+ */
+
+ commit_temporary_layers (temporary_layers);
}
+/** Put a region on some fractional layer and sort everything else out around it.
+ * This can be used to force a region into some layering; for example, calling
+ * this method with temporary_layer == -0.5 will put the region at the bottom of
+ * the stack.
+ */
+
void
-Playlist::lower_region_to_bottom (boost::shared_ptr<Region> region)
+Playlist::relayer (boost::shared_ptr<Region> region, double temporary_layer)
{
- /* does nothing useful if layering mode is later=higher */
- switch (_session.config.get_layer_model()) {
- case LaterHigher:
- return;
- default:
- break;
- }
-
- if (region->layer() == 0) {
- /* already on the bottom */
+ if (_relayer_suspended) {
return;
}
- move_region_to_layer (0, region, -1);
- /* force region's last layer op to zero so that it stays at the bottom
- when doing future relayers
- */
- region->set_last_layer_op (0);
+ TemporaryLayers t;
+ t.set (region, temporary_layer);
+ commit_temporary_layers (t);
}
-int
-Playlist::move_region_to_layer (layer_t target_layer, boost::shared_ptr<Region> region, int dir)
+void
+Playlist::raise_region (boost::shared_ptr<Region> region)
{
- RegionList::iterator i;
- typedef pair<boost::shared_ptr<Region>,layer_t> LayerInfo;
- list<LayerInfo> layerinfo;
-
- {
- RegionLock rlock (const_cast<Playlist *> (this));
-
- for (i = regions.begin(); i != regions.end(); ++i) {
-
- if (region == *i) {
- continue;
- }
-
- layer_t dest;
-
- if (dir > 0) {
-
- /* region is moving up, move all regions on intermediate layers
- down 1
- */
-
- if ((*i)->layer() > region->layer() && (*i)->layer() <= target_layer) {
- dest = (*i)->layer() - 1;
- } else {
- /* not affected */
- continue;
- }
- } else {
-
- /* region is moving down, move all regions on intermediate layers
- up 1
- */
-
- if ((*i)->layer() < region->layer() && (*i)->layer() >= target_layer) {
- dest = (*i)->layer() + 1;
- } else {
- /* not affected */
- continue;
- }
- }
-
- LayerInfo newpair;
-
- newpair.first = *i;
- newpair.second = dest;
-
- layerinfo.push_back (newpair);
- }
- }
-
- freeze ();
-
- /* now reset the layers without holding the region lock */
-
- for (list<LayerInfo>::iterator x = layerinfo.begin(); x != layerinfo.end(); ++x) {
- x->first->set_layer (x->second);
- }
-
- region->set_layer (target_layer);
-
- /* now check all dependents, since we changed the layering */
-
- for (list<LayerInfo>::iterator x = layerinfo.begin(); x != layerinfo.end(); ++x) {
- check_dependents (x->first, false);
- }
+ relayer (region, region->layer() + 0.5);
+}
- check_dependents (region, false);
- notify_layering_changed ();
+void
+Playlist::lower_region (boost::shared_ptr<Region> region)
+{
+ relayer (region, region->layer() - 0.5);
+}
- thaw ();
+void
+Playlist::raise_region_to_top (boost::shared_ptr<Region> region)
+{
+ relayer (region, max_layer);
+}
- return 0;
+void
+Playlist::lower_region_to_bottom (boost::shared_ptr<Region> region)
+{
+ relayer (region, -0.5);
}
void
@@ -2763,15 +2722,23 @@ Playlist::set_frozen (bool yn)
}
void
-Playlist::timestamp_layer_op (boost::shared_ptr<Region> region)
+Playlist::timestamp_layer_op (LayerOp op, boost::shared_ptr<Region> region)
{
- region->set_last_layer_op (++layer_op_counter);
+ region->set_last_layer_op (op, ++layer_op_counter);
}
+/** Find the next or previous region after `region' (next if dir > 0, previous otherwise)
+ * and swap its position with `region'.
+ */
void
Playlist::shuffle (boost::shared_ptr<Region> region, int dir)
{
+ /* As regards layering, the calls we make to set_position() will
+ perform layering as if the regions had been moved, which I think
+ is about right.
+ */
+
bool moved = false;
if (region->locked()) {
@@ -2876,13 +2843,9 @@ Playlist::shuffle (boost::shared_ptr<Region> region, int dir)
_shuffling = false;
if (moved) {
-
- relayer ();
check_dependents (region, false);
-
notify_contents_changed();
}
-
}
bool
@@ -2921,29 +2884,6 @@ Playlist::foreach_region (boost::function<void(boost::shared_ptr<Region>)> s)
}
}
-void
-Playlist::set_explicit_relayering (bool e)
-{
- if (e == false && _explicit_relayering == true) {
-
- /* We are changing from explicit to implicit relayering; layering may have been changed whilst
- we were in explicit mode, and we don't want that to be undone next time an implicit relayer
- occurs. Hence now we'll set up region last_layer_op values so that an implicit relayer
- at this point would keep regions on the same layers.
-
- From then on in, it's just you and your towel.
- */
-
- RegionLock rl (this);
- for (RegionList::iterator i = regions.begin(); i != regions.end(); ++i) {
- (*i)->set_last_layer_op ((*i)->layer ());
- }
- }
-
- _explicit_relayering = e;
-}
-
-
bool
Playlist::has_region_at (framepos_t const p) const
{
@@ -3323,3 +3263,146 @@ Playlist::set_orig_track_id (const PBD::ID& id)
{
_orig_track_id = id;
}
+
+/** Set the temporary layer for a region */
+void
+Playlist::TemporaryLayers::set (boost::shared_ptr<Region> r, double l)
+{
+ _map[r] = l;
+}
+
+/** Return the temporary layer for a region, if one has been specified
+ * to this TemporaryLayers object; if not return the region's current
+ * layer.
+ */
+double
+Playlist::TemporaryLayers::get (boost::shared_ptr<Region> r) const
+{
+ Map::const_iterator i = _map.find (r);
+ if (i != _map.end ()) {
+ return i->second;
+ }
+
+ return double (r->layer ());
+}
+
+int const Playlist::OverlapCache::_divisions = 512;
+
+/** Set up an OverlapCache for a playlist; the cache will only be valid until
+ * the Playlist is changed.
+ */
+Playlist::OverlapCache::OverlapCache (Playlist* playlist)
+ : _range (0, 0)
+{
+ /* Find the start and end positions of the regions on this playlist */
+ _range = Evoral::Range<framepos_t> (max_framepos, 0);
+ RegionList const & rl = playlist->region_list().rlist ();
+ for (RegionList::const_iterator i = rl.begin(); i != rl.end(); ++i) {
+ Evoral::Range<framepos_t> const b = (*i)->bounds ();
+ _range.from = min (_range.from, b.from);
+ _range.to = max (_range.to, b.to);
+ }
+
+ /* Hence the size of each time divison */
+ _division_size = (_range.to - _range.from) / double (_divisions);
+
+ _cache.resize (_divisions);
+
+ /* Build the cache */
+ for (RegionList::const_iterator i = rl.begin(); i != rl.end(); ++i) {
+ pair<int, int> ind = cache_indices ((*i)->bounds ());
+ for (int j = ind.first; j < ind.second; ++j) {
+ _cache[j].push_back (*i);
+ }
+ }
+}
+
+/** @param range Range, in frames.
+ * @return From and to cache indices for (to is exclusive).
+ */
+pair<int, int>
+Playlist::OverlapCache::cache_indices (Evoral::Range<framepos_t> range) const
+{
+ range.from = max (range.from, _range.from);
+ range.to = min (range.to, _range.to);
+
+ pair<int, int> const p = make_pair (
+ floor ((range.from - _range.from) / _division_size),
+ ceil ((range.to - _range.from) / _division_size)
+ );
+
+ assert (p.first >= 0);
+ assert (p.second <= _divisions);
+
+ return p;
+}
+
+/** Return the regions that overlap a given range. The returned list
+ * is not guaranteed to be in the same order as the Playlist that it was
+ * generated from.
+ */
+Playlist::RegionList
+Playlist::OverlapCache::get (Evoral::Range<framepos_t> range) const
+{
+ if (_range.from == max_framepos) {
+ return RegionList ();
+ }
+
+ RegionList r;
+
+ pair<int, int> ind = cache_indices (range);
+ for (int i = ind.first; i < ind.second; ++i) {
+ for (RegionList::const_iterator j = _cache[i].begin(); j != _cache[i].end(); ++j) {
+ if ((*j)->coverage (range.from, range.to) != OverlapNone) {
+ r.push_back (*j);
+ }
+ }
+ }
+
+ r.sort ();
+ r.unique ();
+
+ return r;
+}
+
+void
+Playlist::suspend_relayer ()
+{
+ _relayer_suspended = true;
+}
+
+void
+Playlist::resume_relayer ()
+{
+ _relayer_suspended = false;
+}
+
+/** Examine a region and return regions which overlap it, and also those which overlap those which overlap etc.
+ * @param ignore Optional region which should be treated as if it doesn't exist (ie not returned in the list,
+ * and not recursed into).
+ */
+Playlist::RegionList
+Playlist::recursive_regions_touched (boost::shared_ptr<Region> region, OverlapCache const & cache, boost::shared_ptr<Region> ignore) const
+{
+ RegionList touched;
+ recursive_regions_touched_sub (region, cache, ignore, touched);
+
+ touched.remove (region);
+ return touched;
+}
+
+/** Recursive sub-routine of recursive_regions_touched */
+void
+Playlist::recursive_regions_touched_sub (
+ boost::shared_ptr<Region> region, OverlapCache const & cache, boost::shared_ptr<Region> ignore, RegionList & touched
+ ) const
+{
+ RegionList r = cache.get (region->bounds ());
+ for (RegionList::iterator i = r.begin(); i != r.end(); ++i) {
+ if (find (touched.begin(), touched.end(), *i) == touched.end() && *i != ignore) {
+ touched.push_back (*i);
+ recursive_regions_touched_sub (*i, cache, ignore, touched);
+ }
+ }
+}
+
diff --git a/libs/ardour/region.cc b/libs/ardour/region.cc
index adc6334334..89abdbd230 100644
--- a/libs/ardour/region.cc
+++ b/libs/ardour/region.cc
@@ -73,6 +73,10 @@ namespace ARDOUR {
PBD::PropertyDescriptor<float> stretch;
PBD::PropertyDescriptor<float> shift;
PBD::PropertyDescriptor<PositionLockStyle> position_lock_style;
+ PBD::PropertyDescriptor<framepos_t> last_relayer_bounds_from;
+ PBD::PropertyDescriptor<framepos_t> last_relayer_bounds_to;
+ PBD::PropertyDescriptor<uint64_t> last_layer_op_add;
+ PBD::PropertyDescriptor<uint64_t> last_layer_op_bounds_change;
}
}
@@ -127,6 +131,14 @@ Region::make_property_quarks ()
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for shift = %1\n", Properties::shift.property_id));
Properties::position_lock_style.property_id = g_quark_from_static_string (X_("positional-lock-style"));
DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for position_lock_style = %1\n", Properties::position_lock_style.property_id));
+ Properties::last_relayer_bounds_from.property_id = g_quark_from_static_string (X_("last-relayer-bounds-from"));
+ DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for last_relayer_bounds_from = %1\n", Properties::last_relayer_bounds_from.property_id));
+ Properties::last_relayer_bounds_to.property_id = g_quark_from_static_string (X_("last-relayer-bounds-to"));
+ DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for last_relayer_bounds_to = %1\n", Properties::last_relayer_bounds_to.property_id));
+ Properties::last_layer_op_add.property_id = g_quark_from_static_string (X_("last-layer-op-add"));
+ DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for last_layer_op_add = %1\n", Properties::last_layer_op_add.property_id));
+ Properties::last_layer_op_bounds_change.property_id = g_quark_from_static_string (X_("last-layer-op-bounds-change"));
+ DEBUG_TRACE (DEBUG::Properties, string_compose ("quark for last_layer_op_bounds_change = %1\n", Properties::last_layer_op_bounds_change.property_id));
}
void
@@ -157,6 +169,10 @@ Region::register_properties ()
add_property (_stretch);
add_property (_shift);
add_property (_position_lock_style);
+ add_property (_last_relayer_bounds_from);
+ add_property (_last_relayer_bounds_to);
+ add_property (_last_layer_op_add);
+ add_property (_last_layer_op_bounds_change);
}
#define REGION_DEFAULT_STATE(s,l) \
@@ -182,7 +198,11 @@ Region::register_properties ()
, _ancestral_length (Properties::ancestral_length, (l)) \
, _stretch (Properties::stretch, 1.0) \
, _shift (Properties::shift, 1.0) \
- , _position_lock_style (Properties::position_lock_style, _type == DataType::AUDIO ? AudioTime : MusicTime)
+ , _position_lock_style (Properties::position_lock_style, _type == DataType::AUDIO ? AudioTime : MusicTime) \
+ , _last_relayer_bounds_from (Properties::last_relayer_bounds_from, 0) \
+ , _last_relayer_bounds_to (Properties::last_relayer_bounds_to, 0) \
+ , _last_layer_op_add (Properties::last_layer_op_add, 0) \
+ , _last_layer_op_bounds_change (Properties::last_layer_op_bounds_change, 0)
#define REGION_COPY_STATE(other) \
_sync_marked (Properties::sync_marked, other->_sync_marked) \
@@ -207,7 +227,11 @@ Region::register_properties ()
, _ancestral_length (Properties::ancestral_length, other->_ancestral_length) \
, _stretch (Properties::stretch, other->_stretch) \
, _shift (Properties::shift, other->_shift) \
- , _position_lock_style (Properties::position_lock_style, other->_position_lock_style)
+ , _position_lock_style (Properties::position_lock_style, other->_position_lock_style) \
+ , _last_relayer_bounds_from (Properties::last_relayer_bounds_from, other->_last_relayer_bounds_from) \
+ , _last_relayer_bounds_to (Properties::last_relayer_bounds_to, other->_last_relayer_bounds_to) \
+ , _last_layer_op_add (Properties::last_layer_op_add, other->_last_layer_op_add) \
+ , _last_layer_op_bounds_change (Properties::last_layer_op_bounds_change, other->_last_layer_op_bounds_change)
/* derived-from-derived constructor (no sources in constructor) */
Region::Region (Session& s, framepos_t start, framecnt_t length, const string& name, DataType type)
@@ -217,11 +241,8 @@ Region::Region (Session& s, framepos_t start, framecnt_t length, const string& n
, _last_length (length)
, _last_position (0)
, _first_edit (EditChangesNothing)
- , _last_layer_op(0)
- , _pending_explicit_relayer (false)
{
register_properties ();
-
/* no sources at this point */
}
@@ -233,8 +254,6 @@ Region::Region (const SourceList& srcs)
, _last_length (0)
, _last_position (0)
, _first_edit (EditChangesNothing)
- , _last_layer_op (0)
- , _pending_explicit_relayer (false)
{
register_properties ();
@@ -254,9 +273,6 @@ Region::Region (boost::shared_ptr<const Region> other)
, _last_length (other->_last_length)
, _last_position(other->_last_position) \
, _first_edit (EditChangesNothing)
- , _last_layer_op (0)
- , _pending_explicit_relayer (false)
-
{
register_properties ();
@@ -326,9 +342,6 @@ Region::Region (boost::shared_ptr<const Region> other, frameoffset_t offset)
, _last_length (other->_last_length)
, _last_position(other->_last_position) \
, _first_edit (EditChangesNothing)
- , _last_layer_op (0)
- , _pending_explicit_relayer (false)
-
{
register_properties ();
@@ -383,8 +396,6 @@ Region::Region (boost::shared_ptr<const Region> other, const SourceList& srcs)
, _last_length (other->_last_length)
, _last_position (other->_last_position)
, _first_edit (EditChangesID)
- , _last_layer_op (other->_last_layer_op)
- , _pending_explicit_relayer (false)
{
register_properties ();
@@ -1123,9 +1134,12 @@ Region::set_layer (layer_t l)
{
if (_layer != l) {
_layer = l;
-
send_change (Properties::layer);
}
+
+ Evoral::Range<framepos_t> const b = bounds ();
+ _last_relayer_bounds_from = b.from;
+ _last_relayer_bounds_to = b.to;
}
XMLNode&
@@ -1317,9 +1331,16 @@ Region::send_change (const PropertyChange& what_changed)
}
void
-Region::set_last_layer_op (uint64_t when)
+Region::set_last_layer_op (LayerOp op, uint64_t when)
{
- _last_layer_op = when;
+ switch (op) {
+ case LayerOpAdd:
+ _last_layer_op_add = when;
+ break;
+ case LayerOpBoundsChange:
+ _last_layer_op_bounds_change = when;
+ break;
+ }
}
bool
@@ -1661,3 +1682,25 @@ Region::post_set (const PropertyChange& pc)
recompute_position_from_lock_style ();
}
}
+
+uint64_t
+Region::last_layer_op (LayerOp op) const
+{
+ switch (op) {
+ case LayerOpAdd:
+ return _last_layer_op_add;
+ case LayerOpBoundsChange:
+ return _last_layer_op_bounds_change;
+ }
+
+ /* NOTREACHED */
+ return 0;
+}
+
+Evoral::Range<framepos_t>
+Region::bounds () const
+{
+ return Evoral::Range<framepos_t> (_position, _position + _length);
+}
+
+
diff --git a/libs/ardour/run-tests.sh b/libs/ardour/run-tests.sh
index 379678c968..76fc4f8aea 100755
--- a/libs/ardour/run-tests.sh
+++ b/libs/ardour/run-tests.sh
@@ -19,6 +19,8 @@ export ARDOUR_PANNER_PATH=$libs/panners/2in2out:$libs/panners/1in2out:$libs/pann
if [ "$1" == "--debug" ]; then
gdb ./libs/ardour/run-tests
+elif [ "$1" == "--valgrind" ]; then
+ valgrind --tool="memcheck" ./libs/ardour/run-tests
else
./libs/ardour/run-tests
fi
diff --git a/libs/ardour/session_state.cc b/libs/ardour/session_state.cc
index 7d6f44b786..532d010680 100644
--- a/libs/ardour/session_state.cc
+++ b/libs/ardour/session_state.cc
@@ -974,7 +974,27 @@ int
Session::load_options (const XMLNode& node)
{
LocaleGuard lg (X_("POSIX"));
- config.set_variables (node);
+
+ /* Copy the node */
+ XMLNode node_copy = node;
+
+ /* XXX: evil hack: fix up sessions from before the layering alterations
+ (during A3 beta)
+ */
+
+ XMLNodeList children = node_copy.children ();
+ for (XMLNodeIterator i = children.begin(); i != children.end(); ++i) {
+ XMLProperty* p = (*i)->property (X_("name"));
+ if (p && p->name() == X_("name") && p->value() == X_("layer-model") ) {
+ p = (*i)->property (X_("value"));
+ if (p && p->value() == X_("MoveAddHigher")) {
+ (*i)->add_property (X_("value"), X_("AddOrBoundsChangeHigher"));
+ }
+ }
+ }
+
+ config.set_variables (node_copy);
+
return 0;
}
diff --git a/libs/ardour/test/playlist_layering_test.cc b/libs/ardour/test/playlist_layering_test.cc
index 1371b0cfae..23b22f9d90 100644
--- a/libs/ardour/test/playlist_layering_test.cc
+++ b/libs/ardour/test/playlist_layering_test.cc
@@ -1,13 +1,14 @@
-#include "midi++/manager.h"
-#include "pbd/textreceiver.h"
#include "pbd/compose.h"
-#include "ardour/session.h"
-#include "ardour/audioengine.h"
+#include "midi++/manager.h"
#include "ardour/playlist_factory.h"
#include "ardour/source_factory.h"
#include "ardour/region.h"
#include "ardour/region_factory.h"
+#include "ardour/session.h"
+#include "ardour/audiosource.h"
+#include "ardour/audioengine.h"
#include "playlist_layering_test.h"
+#include "test_receiver.h"
CPPUNIT_TEST_SUITE_REGISTRATION (PlaylistLayeringTest);
@@ -15,68 +16,28 @@ using namespace std;
using namespace ARDOUR;
using namespace PBD;
-class TestReceiver : public Receiver
-{
-protected:
- void receive (Transmitter::Channel chn, const char * str) {
- const char *prefix = "";
-
- switch (chn) {
- case Transmitter::Error:
- prefix = ": [ERROR]: ";
- break;
- case Transmitter::Info:
- /* ignore */
- return;
- case Transmitter::Warning:
- prefix = ": [WARNING]: ";
- break;
- case Transmitter::Fatal:
- prefix = ": [FATAL]: ";
- break;
- case Transmitter::Throw:
- /* this isn't supposed to happen */
- abort ();
- }
-
- /* note: iostreams are already thread-safe: no external
- lock required.
- */
-
- cout << prefix << str << endl;
-
- if (chn == Transmitter::Fatal) {
- exit (9);
- }
- }
-};
-
-TestReceiver test_receiver;
+int const PlaylistLayeringTest::num_regions = 6;
void
PlaylistLayeringTest::setUp ()
{
- string const test_session_path = "libs/ardour/test/playlist_layering_test";
- string const test_wav_path = "libs/ardour/test/playlist_layering_test/playlist_layering_test.wav";
- system (string_compose ("rm -rf %1", test_session_path).c_str());
+ TestNeedingSession::setUp ();
+ string const test_wav_path = "libs/ardour/test/test.wav";
- init (false, true);
- SessionEvent::create_per_thread_pool ("test", 512);
-
- test_receiver.listen_to (error);
- test_receiver.listen_to (info);
- test_receiver.listen_to (fatal);
- test_receiver.listen_to (warning);
-
- AudioEngine* engine = new AudioEngine ("test", "");
- MIDI::Manager::create (engine->jack ());
- CPPUNIT_ASSERT (engine->start () == 0);
-
- _session = new Session (*engine, test_session_path, "playlist_layering_test");
- engine->set_session (_session);
-
_playlist = PlaylistFactory::create (DataType::AUDIO, *_session, "test");
_source = SourceFactory::createWritable (DataType::AUDIO, *_session, test_wav_path, "", false, 44100);
+
+ system ("pwd");
+
+ /* Must write some data to our source, otherwise regions which use it will
+ be limited in whether they can be trimmed or not.
+ */
+ boost::shared_ptr<AudioSource> a = boost::dynamic_pointer_cast<AudioSource> (_source);
+ Sample silence[512];
+ memset (silence, 0, 512 * sizeof (Sample));
+ a->write (silence, 512);
+
+ _region = new boost::shared_ptr<Region>[num_regions];
}
void
@@ -84,68 +45,303 @@ PlaylistLayeringTest::tearDown ()
{
_playlist.reset ();
_source.reset ();
- for (int i = 0; i < 16; ++i) {
+ for (int i = 0; i < num_regions; ++i) {
_region[i].reset ();
}
- AudioEngine::instance()->remove_session ();
- delete _session;
- EnumWriter::destroy ();
- MIDI::Manager::destroy ();
- AudioEngine::destroy ();
+ delete[] _region;
+
+ TestNeedingSession::tearDown ();
}
void
-PlaylistLayeringTest::create_three_short_regions ()
+PlaylistLayeringTest::create_short_regions ()
{
PropertyList plist;
plist.add (Properties::start, 0);
plist.add (Properties::length, 100);
- for (int i = 0; i < 3; ++i) {
+ for (int i = 0; i < num_regions; ++i) {
_region[i] = RegionFactory::create (_source, plist);
+ _region[i]->set_name (string_compose ("%1", char (int ('A') + i)));
}
}
void
-PlaylistLayeringTest::addHigherTest ()
+PlaylistLayeringTest::laterHigher_relayerOnAll_Test ()
+{
+ _session->config.set_layer_model (LaterHigher);
+ _session->config.set_relayer_on_all_edits (true);
+
+ create_short_regions ();
+
+ /* three overlapping regions */
+ _playlist->add_region (_region[A], 0);
+ _playlist->add_region (_region[B], 10);
+ _playlist->add_region (_region[C], 20);
+ /* and another non-overlapping one */
+ _playlist->add_region (_region[D], 200);
+
+ /* LaterHigher means that they should be arranged thus */
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ _region[A]->set_position (5);
+
+ /* Region move should have no effect in LaterHigher mode */
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* C -> bottom should give C A B, not touching D */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* C -> top should go back to A B C, not touching D */
+ _region[C]->raise_to_top ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+}
+
+void
+PlaylistLayeringTest::addHigher_relayerOnAll_Test ()
{
_session->config.set_layer_model (AddHigher);
- create_three_short_regions ();
+ _session->config.set_relayer_on_all_edits (true);
+
+ create_short_regions ();
+
+ /* three overlapping regions */
+ _playlist->add_region (_region[A], 0);
+ _playlist->add_region (_region[B], 10);
+ _playlist->add_region (_region[C], 20);
+ /* and another non-overlapping one */
+ _playlist->add_region (_region[D], 200);
+
+ /* AddHigher means that they should be arranged thus */
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
- _playlist->add_region (_region[0], 0);
- _playlist->add_region (_region[1], 10);
- _playlist->add_region (_region[2], 20);
+ _region[A]->set_position (5);
- CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[0]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[1]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[2]->layer ());
+ /* region move should have no effect in AddHigher mode */
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
- _region[0]->set_position (5);
+ /* C -> bottom should give C A B, not touching D */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
- /* region move should have no effect */
- CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[0]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[1]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[2]->layer ());
+ /* C -> top should go back to A B C, not touching D */
+ _region[C]->raise_to_top ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
}
void
-PlaylistLayeringTest::moveAddHigherTest ()
+PlaylistLayeringTest::addOrBoundsHigher_relayerOnAll_Test ()
{
- _session->config.set_layer_model (MoveAddHigher);
- create_three_short_regions ();
+ _session->config.set_layer_model (AddOrBoundsChangeHigher);
+ _session->config.set_relayer_on_all_edits (true);
+
+ create_short_regions ();
+
+ /* three overlapping regions */
+ _playlist->add_region (_region[A], 0);
+ _playlist->add_region (_region[B], 10);
+ _playlist->add_region (_region[C], 20);
+ /* and another non-overlapping one */
+ _playlist->add_region (_region[D], 200);
+
+ /* AddOrBoundsHigher means that they should be arranged thus */
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* region move should put A on top for B C A, not touching D */
+ _region[A]->set_position (5);
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* C -> bottom should give C B A, not touching D */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* C -> top should go back to B A C, not touching D */
+ _region[C]->raise_to_top ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* Put C on the bottom */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* Now move it slightly, and it should go back to the top again */
+ _region[C]->set_position (21);
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* Put C back on the bottom */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* Trim it slightly, and it should go back to the top again */
+ _region[C]->trim_front (23);
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* Same with the end */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ _region[C]->trim_end (118);
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+}
+
+void
+PlaylistLayeringTest::addOrBoundsHigher_relayerWhenNecessary_Test ()
+{
+ _session->config.set_layer_model (AddOrBoundsChangeHigher);
+ _session->config.set_relayer_on_all_edits (false);
+
+ create_short_regions ();
+
+ /* three overlapping regions */
+ _playlist->add_region (_region[A], 0);
+ _playlist->add_region (_region[B], 10);
+ _playlist->add_region (_region[C], 20);
+ /* and another non-overlapping one */
+ _playlist->add_region (_region[D], 200);
+
+ /* AddOrBoundsHigher means that they should be arranged thus */
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ _region[A]->set_position (5);
+
+ /* region move should not have changed anything, since in
+ this mode we only relayer when there is a new overlap
+ */
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* C -> bottom should give C A B, not touching D */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* C -> top should go back to A B C, not touching D */
+ _region[C]->raise_to_top ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* Put C on the bottom */
+ _region[C]->lower_to_bottom ();
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+
+ /* Now move it slightly, and it should stay where it is */
+ _region[C]->set_position (21);
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+}
+
+void
+PlaylistLayeringTest::lastLayerOpTest ()
+{
+ create_short_regions ();
+
+ _playlist->add_region (_region[A], 0);
+ CPPUNIT_ASSERT_EQUAL (_playlist->layer_op_counter, _region[A]->last_layer_op (LayerOpAdd));
+ uint64_t const last_add = _region[A]->last_layer_op (LayerOpAdd);
+
+ _region[A]->set_position (42);
+ CPPUNIT_ASSERT_EQUAL (_playlist->layer_op_counter, _region[A]->last_layer_op (LayerOpBoundsChange));
+ CPPUNIT_ASSERT_EQUAL (last_add, _region[A]->last_layer_op (LayerOpAdd));
+
+ _region[A]->trim_front (46);
+ CPPUNIT_ASSERT_EQUAL (_playlist->layer_op_counter, _region[A]->last_layer_op (LayerOpBoundsChange));
+ CPPUNIT_ASSERT_EQUAL (last_add, _region[A]->last_layer_op (LayerOpAdd));
+
+ _region[A]->trim_end (102);
+ CPPUNIT_ASSERT_EQUAL (_playlist->layer_op_counter, _region[A]->last_layer_op (LayerOpBoundsChange));
+ CPPUNIT_ASSERT_EQUAL (last_add, _region[A]->last_layer_op (LayerOpAdd));
+}
+
+void
+PlaylistLayeringTest::recursiveRelayerTest ()
+{
+ _session->config.set_layer_model (AddOrBoundsChangeHigher);
+ _session->config.set_relayer_on_all_edits (false);
+
+ create_short_regions ();
- _playlist->add_region (_region[0], 0);
- _playlist->add_region (_region[1], 10);
- _playlist->add_region (_region[2], 20);
+ _playlist->add_region (_region[A], 100);
+ _playlist->add_region (_region[B], 125);
+ _playlist->add_region (_region[C], 50);
+ _playlist->add_region (_region[D], 250);
- CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[0]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[1]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[2]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[C]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
- _region[0]->set_position (5);
+ _region[A]->set_position (200);
- /* region move should have put 0 on top */
- CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[0]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[1]->layer ());
- CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[2]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (0), _region[D]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (1), _region[A]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (2), _region[B]->layer ());
+ CPPUNIT_ASSERT_EQUAL (layer_t (3), _region[C]->layer ());
}
diff --git a/libs/ardour/test/playlist_layering_test.h b/libs/ardour/test/playlist_layering_test.h
index 7757e017c4..ab5fe701b1 100644
--- a/libs/ardour/test/playlist_layering_test.h
+++ b/libs/ardour/test/playlist_layering_test.h
@@ -1,5 +1,6 @@
#include <cppunit/TestFixture.h>
#include <cppunit/extensions/HelperMacros.h>
+#include "test_needing_session.h"
namespace ARDOUR {
class Session;
@@ -7,25 +8,42 @@ namespace ARDOUR {
class Source;
}
-class PlaylistLayeringTest : public CppUnit::TestFixture
+class PlaylistLayeringTest : public TestNeedingSession
{
CPPUNIT_TEST_SUITE (PlaylistLayeringTest);
- CPPUNIT_TEST (addHigherTest);
- CPPUNIT_TEST (moveAddHigherTest);
+ CPPUNIT_TEST (lastLayerOpTest);
+ CPPUNIT_TEST (addHigher_relayerOnAll_Test);
+ CPPUNIT_TEST (addOrBoundsHigher_relayerOnAll_Test);
+ CPPUNIT_TEST (laterHigher_relayerOnAll_Test);
+ CPPUNIT_TEST (addOrBoundsHigher_relayerWhenNecessary_Test);
+ CPPUNIT_TEST (recursiveRelayerTest);
CPPUNIT_TEST_SUITE_END ();
public:
void setUp ();
void tearDown ();
- void addHigherTest ();
- void moveAddHigherTest ();
+ void lastLayerOpTest ();
+ void addHigher_relayerOnAll_Test ();
+ void addOrBoundsHigher_relayerOnAll_Test ();
+ void laterHigher_relayerOnAll_Test ();
+ void addOrBoundsHigher_relayerWhenNecessary_Test ();
+ void recursiveRelayerTest ();
private:
- void create_three_short_regions ();
+ void create_short_regions ();
+
+ static int const num_regions;
+ enum {
+ A = 0,
+ B,
+ C,
+ D,
+ E,
+ F
+ };
- ARDOUR::Session* _session;
boost::shared_ptr<ARDOUR::Playlist> _playlist;
boost::shared_ptr<ARDOUR::Source> _source;
- boost::shared_ptr<ARDOUR::Region> _region[16];
+ boost::shared_ptr<ARDOUR::Region>* _region;
};
diff --git a/libs/ardour/test/playlist_overlap_cache_test.cc b/libs/ardour/test/playlist_overlap_cache_test.cc
new file mode 100644
index 0000000000..9263d80d3b
--- /dev/null
+++ b/libs/ardour/test/playlist_overlap_cache_test.cc
@@ -0,0 +1,119 @@
+#include "pbd/compose.h"
+#include "ardour/playlist.h"
+#include "ardour/playlist_factory.h"
+#include "ardour/source_factory.h"
+#include "ardour/region.h"
+#include "ardour/region_sorters.h"
+#include "ardour/region_factory.h"
+#include "playlist_overlap_cache_test.h"
+
+using namespace std;
+using namespace PBD;
+using namespace ARDOUR;
+
+CPPUNIT_TEST_SUITE_REGISTRATION (PlaylistOverlapCacheTest);
+
+void
+PlaylistOverlapCacheTest::tearDown ()
+{
+ _playlist.reset ();
+ _source.reset ();
+
+ TestNeedingSession::tearDown ();
+}
+
+void
+PlaylistOverlapCacheTest::basicTest ()
+{
+ string const test_wav_path = "libs/ardour/test/test.wav";
+
+ _playlist = PlaylistFactory::create (DataType::AUDIO, *_session, "test");
+ _source = SourceFactory::createWritable (DataType::AUDIO, *_session, test_wav_path, "", false, 44100);
+
+ PropertyList plist;
+ plist.add (Properties::length, 256);
+
+ boost::shared_ptr<Region> regionA = RegionFactory::create (_source, plist);
+ regionA->set_name ("A");
+ _playlist->add_region (regionA, 0);
+
+
+ {
+ Playlist::OverlapCache cache (_playlist.get ());
+ Playlist::RegionList rl = cache.get (Evoral::Range<framepos_t> (0, 256));
+ CPPUNIT_ASSERT_EQUAL (size_t (1), rl.size ());
+ CPPUNIT_ASSERT_EQUAL (regionA, rl.front ());
+
+ rl = cache.get (Evoral::Range<framepos_t> (-1000, 1000));
+ CPPUNIT_ASSERT_EQUAL (size_t (1), rl.size ());
+ CPPUNIT_ASSERT_EQUAL (regionA, rl.front ());
+ }
+
+ boost::shared_ptr<Region> regionB = RegionFactory::create (_source, plist);
+ regionA->set_name ("B");
+ _playlist->add_region (regionB, 53);
+
+ {
+ Playlist::OverlapCache cache (_playlist.get ());
+ Playlist::RegionList rl = cache.get (Evoral::Range<framepos_t> (0, 256));
+ CPPUNIT_ASSERT_EQUAL (size_t (2), rl.size ());
+ rl.sort (RegionSortByPosition ());
+ CPPUNIT_ASSERT_EQUAL (regionA, rl.front ());
+ CPPUNIT_ASSERT_EQUAL (regionB, rl.back ());
+
+ rl = cache.get (Evoral::Range<framepos_t> (260, 274));
+ CPPUNIT_ASSERT_EQUAL (size_t (1), rl.size ());
+ CPPUNIT_ASSERT_EQUAL (regionB, rl.front ());
+ }
+}
+
+void
+PlaylistOverlapCacheTest::stressTest ()
+{
+ string const test_wav_path = "libs/ardour/test/test.wav";
+
+ _playlist = PlaylistFactory::create (DataType::AUDIO, *_session, "test");
+ _source = SourceFactory::createWritable (DataType::AUDIO, *_session, test_wav_path, "", false, 44100);
+
+ srand (42);
+
+ int const num_regions = rand () % 256;
+
+ for (int i = 0; i < num_regions; ++i) {
+ PropertyList plist;
+ plist.add (Properties::length, rand () % 32768);
+ boost::shared_ptr<Region> r = RegionFactory::create (_source, plist);
+ r->set_name (string_compose ("%1", i));
+ _playlist->add_region (r, rand() % 32768);
+ }
+
+ Playlist::OverlapCache cache (_playlist.get ());
+
+ int const tests = rand () % 256;
+
+ for (int i = 0; i < tests; ++i) {
+ framepos_t const start = rand () % 32768;
+ framepos_t const length = rand () % 32768;
+ framepos_t const end = start + length;
+
+ Playlist::RegionList cached = cache.get (Evoral::Range<framepos_t> (start, end));
+
+ Playlist::RegionList actual;
+ Playlist::RegionList regions = _playlist->region_list().rlist();
+ for (Playlist::RegionList::iterator j = regions.begin(); j != regions.end(); ++j) {
+ if ((*j)->coverage (start, end) != OverlapNone) {
+ actual.push_back (*j);
+ }
+ }
+
+ cached.sort (RegionSortByPosition ());
+ actual.sort (RegionSortByPosition ());
+
+ CPPUNIT_ASSERT_EQUAL (actual.size (), cached.size ());
+ Playlist::RegionList::iterator j = actual.begin ();
+ Playlist::RegionList::iterator k = cached.begin ();
+ for (; j != actual.end(); ++j, ++k) {
+ CPPUNIT_ASSERT_EQUAL (*j, *k);
+ }
+ }
+}
diff --git a/libs/ardour/test/playlist_overlap_cache_test.h b/libs/ardour/test/playlist_overlap_cache_test.h
new file mode 100644
index 0000000000..c75d1691d9
--- /dev/null
+++ b/libs/ardour/test/playlist_overlap_cache_test.h
@@ -0,0 +1,20 @@
+#include "test_needing_session.h"
+
+class PlaylistOverlapCacheTest : public TestNeedingSession
+{
+public:
+ CPPUNIT_TEST_SUITE (PlaylistOverlapCacheTest);
+ CPPUNIT_TEST (basicTest);
+ CPPUNIT_TEST (stressTest);
+ CPPUNIT_TEST_SUITE_END ();
+
+public:
+ void tearDown ();
+
+ void basicTest ();
+ void stressTest ();
+
+private:
+ boost::shared_ptr<ARDOUR::Playlist> _playlist;
+ boost::shared_ptr<ARDOUR::Source> _source;
+};
diff --git a/libs/ardour/test/test_needing_session.cc b/libs/ardour/test/test_needing_session.cc
new file mode 100644
index 0000000000..5981553601
--- /dev/null
+++ b/libs/ardour/test/test_needing_session.cc
@@ -0,0 +1,48 @@
+#include "midi++/manager.h"
+#include "pbd/textreceiver.h"
+#include "pbd/compose.h"
+#include "pbd/enumwriter.h"
+#include "ardour/session.h"
+#include "ardour/audioengine.h"
+#include "test_needing_session.h"
+#include "test_receiver.h"
+
+using namespace std;
+using namespace ARDOUR;
+using namespace PBD;
+
+TestReceiver test_receiver;
+
+void
+TestNeedingSession::setUp ()
+{
+ string const test_session_path = "libs/ardour/test/test_session";
+ system (string_compose ("rm -rf %1", test_session_path).c_str());
+
+ init (false, true);
+ SessionEvent::create_per_thread_pool ("test", 512);
+
+ test_receiver.listen_to (error);
+ test_receiver.listen_to (info);
+ test_receiver.listen_to (fatal);
+ test_receiver.listen_to (warning);
+
+ AudioEngine* engine = new AudioEngine ("test", "");
+ MIDI::Manager::create (engine->jack ());
+ CPPUNIT_ASSERT (engine->start () == 0);
+
+ _session = new Session (*engine, test_session_path, "test_session");
+ engine->set_session (_session);
+}
+
+void
+TestNeedingSession::tearDown ()
+{
+ AudioEngine::instance()->remove_session ();
+
+ delete _session;
+
+ EnumWriter::destroy ();
+ MIDI::Manager::destroy ();
+ AudioEngine::destroy ();
+}
diff --git a/libs/ardour/test/test_needing_session.h b/libs/ardour/test/test_needing_session.h
new file mode 100644
index 0000000000..3839855179
--- /dev/null
+++ b/libs/ardour/test/test_needing_session.h
@@ -0,0 +1,16 @@
+#include <cppunit/TestFixture.h>
+#include <cppunit/extensions/HelperMacros.h>
+
+namespace ARDOUR {
+ class Session;
+}
+
+class TestNeedingSession : public CppUnit::TestFixture
+{
+public:
+ void setUp ();
+ void tearDown ();
+
+protected:
+ ARDOUR::Session* _session;
+};
diff --git a/libs/ardour/test/test_receiver.h b/libs/ardour/test/test_receiver.h
new file mode 100644
index 0000000000..537bac3cff
--- /dev/null
+++ b/libs/ardour/test/test_receiver.h
@@ -0,0 +1,37 @@
+#include "pbd/receiver.h"
+
+class TestReceiver : public Receiver
+{
+protected:
+ void receive (Transmitter::Channel chn, const char * str) {
+ const char *prefix = "";
+
+ switch (chn) {
+ case Transmitter::Error:
+ prefix = ": [ERROR]: ";
+ break;
+ case Transmitter::Info:
+ /* ignore */
+ return;
+ case Transmitter::Warning:
+ prefix = ": [WARNING]: ";
+ break;
+ case Transmitter::Fatal:
+ prefix = ": [FATAL]: ";
+ break;
+ case Transmitter::Throw:
+ /* this isn't supposed to happen */
+ abort ();
+ }
+
+ /* note: iostreams are already thread-safe: no external
+ lock required.
+ */
+
+ std::cout << prefix << str << std::endl;
+
+ if (chn == Transmitter::Fatal) {
+ exit (9);
+ }
+ }
+};
diff --git a/libs/ardour/wscript b/libs/ardour/wscript
index 42c72784f9..e49be33b8c 100644
--- a/libs/ardour/wscript
+++ b/libs/ardour/wscript
@@ -431,6 +431,8 @@ def build(bld):
test/framepos_plus_beats_test.cc
test/framepos_minus_beats_test.cc
test/playlist_layering_test.cc
+ test/playlist_overlap_cache_test.cc
+ test/test_needing_session.cc
test/testrunner.cc
'''.split()
diff --git a/manual/xml/working_with_layers.xml b/manual/xml/working_with_layers.xml
index bd843ca305..7027064079 100644
--- a/manual/xml/working_with_layers.xml
+++ b/manual/xml/working_with_layers.xml
@@ -21,8 +21,8 @@
</para>
<para>
- Of course, nothing in digital audio is ever quite that simple, and so of
- course there are some complications:
+ Of course, nothing in digital audio is ever quite that simple, and so
+ there are some complications:
</para>
<section id="layers-crossfades">
@@ -38,14 +38,14 @@
<section id="region-opacity">
<title> Region Opacity </title>
<para>
- In a perverse nod to image manipulation programs, Ardour allows you to
+ With a nod to image manipulation programs, Ardour allows you to
make regions transparent. By default, all regions are created opaque,
which means that when they are playing, no region below them are
audible. However, if you change the region to be transparent, the
region will be audible together with any regions below it. This
- capability should probably not be abused - if you really want to mix
- sounds together in this way, they should probably live in their own
- tracks. Occasionally though, this can be useful trick.
+ capability should probably not be abused; if you really want to mix
+ sounds together in this way, they should probably be on their own
+ tracks. Occasionally though, this can be a useful trick.
</para>
<para>
@@ -55,70 +55,74 @@
</para>
</section>
- <section id="layering-styles">
- <title> Layering Styles </title>
- <para>
- When you are recording new material for a track, its typical to want
- to new material recorded "over" existing material in the track to be
- what you hear on playback. For example, if you overdub part of a
- guitar solo, you normally want the overdub to be audible, not hidden
- by the old version that was already there. By contrast, when editing
- using splitting/trimming/moving of regions to create a particular
- arrangement along the timeline, many people find that they want
- regions that start later on the timeline to be the ones that are
- audible.
- </para>
+ <section id="choice-of-layering">
+ <title>Choice of layering</title>
<para>
- To facilitate these two contradictory desires, Ardour features three
- different styles for assigning regions to layers.
+ There are two main decisions to be made with regard to how a playlist
+ should be layered:
</para>
<variablelist>
- <title></title>
- <varlistentry>
- <term>Most recently added regions are higher</term>
- <listitem>
- <para>
- Use this style when recording/overdubbing new material. Edits of
- any kind do not modify the layering.
- </para>
- </listitem>
- </varlistentry>
-
<varlistentry>
- <term>Most recently added/moved/trimmed regions are higher</term>
- <listitem>
- <para>
- Use this style when recording/overdubbing new material, but you
- want basic edits to cause regions to rise to the top.
- </para>
- </listitem>
+ Given overlapping regions, which order should they be layered in?
</varlistentry>
-
<varlistentry>
- <term>Later regions are higher</term>
- <listitem>
- <para>
- Use this style when rearranging and editing regions.
- </para>
- </listitem>
+ When should layering be changed?
</varlistentry>
</variablelist>
+ <section id="layering-order">
+ <title>Layering Order</title>
+ <para>
+ Ardour provides three-and-a-half ways to decide on the order in which regions are layered. The most basic choice is:
+ </para>
+
+ <variablelist>
+ <title></title>
+ <varlistentry>
+ <term>Most recently added regions are higher</term>
+ <listitem>
+ <para>
+ Regions which are later in time will be on higher layers.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>Most recently added or edited regions are higher</term>
+ <listitem>
+ <para>
+ Regions which were more recently edited or added to the playlist
+ will be on higher layers.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term>Later regions are higher</term>
+ <listitem>
+ <para>
+ Regions which were more recently added to the playlist will be on higher
+ layers.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </section>
+
<para>
- A new session has the layering style set to "Most recently
- added/moved/trimmed regions are higher". To change the layering style,
- open the <emphasis>options editor</emphasis> and select the
- "Layers&amp;Fades" page. There is an option there to select the style
- you want. Layering style may be changed at any time. The existing
- layering of all playlists is not changed when changing the layering
- model.
+ A new session has the layering style set to "Most recently edited or
+ added regions are higher". To change the layering style, open the
+ <emphasis>Session Properties</emphasis> dialogue and choose your layering
+ style from the "Misc" page. Changing the layering style only affects
+ future edits to the playlist; the existing layering of all playlists is
+ preserved when changing the layering mode.
</para>
</section>
<section id="modifying-layering-by-hand">
- <title> Modifying Layering By Hand </title>
+ <title>Modifying Layering Explicitly</title>
<para>
If you want a particular region to be the uppermost when the current
layering style has put it on a lower layer, context click on the