diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /third_party/webkit/PerformanceTests/webaudio | |
parent | Initial commit. (diff) | |
download | firefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/webkit/PerformanceTests/webaudio')
12 files changed, 691 insertions, 0 deletions
diff --git a/third_party/webkit/PerformanceTests/webaudio/README.md b/third_party/webkit/PerformanceTests/webaudio/README.md new file mode 100644 index 0000000000..8fd378b573 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/README.md @@ -0,0 +1,13 @@ +# webaudio-benchmark + +## Run + +Just open `index.html`. Time are in milliseconds, lower is better. + +## Adding new benchmarks + +Look into `benchmarks.js`, it's pretty straightforward. + +## License + +MPL 2.0 diff --git a/third_party/webkit/PerformanceTests/webaudio/benchmarks.js b/third_party/webkit/PerformanceTests/webaudio/benchmarks.js new file mode 100644 index 0000000000..1d8f0e78bb --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/benchmarks.js @@ -0,0 +1,377 @@ +if (typeof(window) == "undefined") { + benchmarks = [] + registerTestFile = function() {} + registerTestCase = function(o) { return benchmarks.push(o.name); } +} + +registerTestFile({ sampleRate: 48000, url: "think-mono-48000.wav" }); +registerTestFile({ sampleRate: 44100, url: "think-mono-44100.wav" }); +registerTestFile({ sampleRate: 38000, url: "think-mono-38000.wav" }); +registerTestFile({ sampleRate: 48000, url: "think-stereo-48000.wav" }); +registerTestFile({ sampleRate: 44100, url: "think-stereo-44100.wav" }); +registerTestFile({ sampleRate: 38000, url: "think-stereo-38000.wav" }); + +registerTestCase({ + func: function () { + var oac = new OfflineAudioContext(1, DURATION * sampleRate, sampleRate); + return oac; + }, + name: "Empty testcase" +}); + +registerTestCase({ + func: function () { + var oac = new OfflineAudioContext(1, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + source0.buffer = getSpecificFile({ sampleRate: oac.sampleRate, numberOfChannels: 1 }); + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Simple source test without resampling" +}); + +registerTestCase({ + func: function() { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + source0.buffer = getSpecificFile({ sampleRate: oac.sampleRate, numberOfChannels: 2 }); + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Simple source test without resampling (Stereo)" +}); + +registerTestCase({ + func: function () { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + var panner = oac.createPanner(); + source0.buffer = getSpecificFile({ sampleRate: oac.sampleRate, numberOfChannels: 2 }); + source0.loop = true; + panner.setPosition(1, 2, 3); + panner.setOrientation(10, 10, 10); + source0.connect(panner); + panner.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Simple source test without resampling (Stereo and positional)" +}); + +registerTestCase({ + func: function() { + var oac = new OfflineAudioContext(1, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + source0.buffer = getSpecificFile({ sampleRate: 38000, numberOfChannels: 1 }); + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Simple source test with resampling (Mono)" +}); + +registerTestCase({ + func: function() { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + source0.buffer = getSpecificFile({ sampleRate: 38000, numberOfChannels: 2 }); + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Simple source test with resampling (Stereo)" +}); + +registerTestCase({ + func: function() { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + var panner = oac.createPanner(); + source0.buffer = getSpecificFile({ sampleRate: 38000, numberOfChannels: 2 }); + source0.loop = true; + panner.setPosition(1, 2, 3); + panner.setOrientation(10, 10, 10); + source0.connect(panner); + panner.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Simple source test with resampling (Stereo and positional)" +}); + +registerTestCase({ + func: function() { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + source0.buffer = getSpecificFile({ sampleRate: oac.sampleRate, numberOfChannels: 1 }); + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Upmix without resampling (Mono -> Stereo)" +}); + +registerTestCase({ + func: function() { + var oac = new OfflineAudioContext(1, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + source0.buffer = getSpecificFile({ sampleRate: oac.sampleRate, numberOfChannels: 2 }); + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Downmix without resampling (Stereo -> Mono)" +}); + +registerTestCase({ + func: function() { + var duration_adjusted = DURATION / 4; + var oac = new OfflineAudioContext(2, duration_adjusted * sampleRate, sampleRate); + for (var i = 0; i < 100; i++) { + var source0 = oac.createBufferSource(); + source0.buffer = getSpecificFile({ sampleRate: 38000, numberOfChannels: 1 }); + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + } + return oac; + }, + name: "Simple mixing (100x same buffer)" +}); + +registerTestCase({ + func: function() { + var duration_adjusted = DURATION / 4; + var oac = new OfflineAudioContext(2, duration_adjusted * sampleRate, sampleRate); + var reference = getSpecificFile({ sampleRate: 38000, numberOfChannels: 1 }).getChannelData(0); + for (var i = 0; i < 100; i++) { + var source0 = oac.createBufferSource(); + // copy the buffer into the a new one, so we know the implementation is not + // sharing them. + var b = oac.createBuffer(1, reference.length, 38000); + var data = b.getChannelData(0); + for (var j = 0; j < b.length; j++) { + data[i] = reference[i]; + } + source0.buffer = b; + source0.loop = true; + source0.connect(oac.destination); + source0.start(0); + } + return oac; + }, + name: "Simple mixing (100 different buffers)" +}); + +registerTestCase({ + func: function() { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var gain = oac.createGain(); + gain.gain.value = -1; + gain.connect(oac.destination); + var gainsi = []; + for (var i = 0; i < 4; i++) { + var gaini = oac.createGain(); + gaini.gain.value = 0.25; + gaini.connect(gain); + gainsi[i] = gaini + } + for (var j = 0; j < 2; j++) { + var sourcej = oac.createBufferSource(); + sourcej.buffer = getSpecificFile({ sampleRate: 38000, numberOfChannels: 1 }); + sourcej.loop = true; + sourcej.start(0); + for (var i = 0; i < 4; i++) { + var gainij = oac.createGain(); + gainij.gain.value = 0.5; + gainij.connect(gainsi[i]); + sourcej.connect(gainij); + } + } + return oac; + }, + name: "Simple mixing with gains" +}); + +registerTestCase({ + func: function() { + var duration_adjusted = DURATION / 8; + var oac = new OfflineAudioContext(1, duration_adjusted * sampleRate, sampleRate); + var i,l; + var decay = 10; + var duration = 4; + var len = sampleRate * duration; + var buffer = ac.createBuffer(2, len, oac.sampleRate) + var iL = buffer.getChannelData(0) + var iR = buffer.getChannelData(1) + // Simple exp decay loop + for(i=0,l=buffer.length;i<l;i++) { + iL[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay); + iR[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay); + } + var convolver = oac.createConvolver(); + convolver.buffer = buffer; + convolver.connect(oac.destination); + + var audiobuffer = getSpecificFile({ sampleRate: sampleRate, numberOfChannels: 1 }); + var source0 = oac.createBufferSource(); + source0.buffer = audiobuffer; + source0.loop = true; + source0.connect(convolver); + source0.start(0); + return oac; + }, + name: "Convolution reverb" +}); + +registerTestCase({ + func: function() { + // this test case is very slow in chrome, reduce the total duration to avoid + // timeout + var duration_adjusted = DURATION / 16; + var oac = new OfflineAudioContext(1, duration_adjusted * sampleRate, sampleRate); + var duration = duration_adjusted * sampleRate; + var audiobuffer = getSpecificFile({ sampleRate: sampleRate, numberOfChannels: 1 }); + var offset = 0; + while (offset < duration / sampleRate) { + var grain = oac.createBufferSource(); + var gain = oac.createGain(); + grain.connect(gain); + gain.connect(oac.destination); + grain.buffer = audiobuffer; + // get a random 100-ish ms with enveloppes + var start = offset * Math.random() * 0.5; + var end = start + 0.005 * (0.999 * Math.random()); + grain.start(offset, start, end); + gain.gain.setValueAtTime(offset, 0); + gain.gain.linearRampToValueAtTime(.5, offset + 0.005); + var startRelease = Math.max(offset + (end - start), 0); + gain.gain.setValueAtTime(0.5, startRelease); + gain.gain.linearRampToValueAtTime(0.0, startRelease + 0.05); + + // some overlap + offset += 0.005; + } + return oac; + }, + name: "Granular synthesis" +}); + +registerTestCase({ + func: function() { + var sampleRate = 44100; + var duration = DURATION; + var oac = new OfflineAudioContext(1, duration * sampleRate, 44100); + var offset = 0; + while (offset < duration) { + var note = oac.createOscillator(); + var env = oac.createGain(); + note.type = "sawtooth"; + note.frequency.value = 110; + note.connect(env); + env.gain.setValueAtTime(0, 0); + env.gain.setValueAtTime(0.5, offset); + env.gain.setTargetAtTime(0, offset+0.01, 0.1); + env.connect(oac.destination); + note.start(offset); + note.stop(offset + 1.0); + offset += 140 / 60 / 4; // 140 bpm + } + return oac; + }, + name: "Synth" +}); + +registerTestCase({ + func: function() { + var sampleRate = 44100; + var duration = DURATION; + var oac = new OfflineAudioContext(1, duration * sampleRate, sampleRate); + var offset = 0; + var osc = oac.createOscillator(); + osc.type = "sawtooth"; + var enveloppe = oac.createGain(); + enveloppe.gain.setValueAtTime(0, 0); + var filter = oac.createBiquadFilter(); + osc.connect(enveloppe); + enveloppe.connect(filter); + filter.connect(oac.destination); + filter.frequency.setValueAtTime(0.0, 0.0); + filter.Q.setValueAtTime(20, 0.0); + osc.start(0); + osc.frequency.setValueAtTime(110, 0); + + while (offset < duration) { + enveloppe.gain.setValueAtTime(1.0, offset); + enveloppe.gain.setTargetAtTime(0.0, offset, 0.1); + filter.frequency.setValueAtTime(0, offset); + filter.frequency.setTargetAtTime(3500, offset, 0.03); + offset += 140 / 60 / 16; + } + return oac; + }, + name: "Substractive synth" +}); + +registerTestCase({ + func: function () { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + var panner = oac.createStereoPanner(); + source0.buffer = getSpecificFile({ sampleRate: oac.sampleRate, numberOfChannels: 2 }); + source0.loop = true; + panner.pan = 0.1; + source0.connect(panner); + panner.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Stereo Panning" +}); + +registerTestCase({ + func: function () { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var source0 = oac.createBufferSource(); + var panner = oac.createStereoPanner(); + source0.buffer = getSpecificFile({ sampleRate: oac.sampleRate, numberOfChannels: 2 }); + source0.loop = true; + panner.pan.setValueAtTime(-0.1, 0.0); + panner.pan.setValueAtTime(0.2, 0.5); + source0.connect(panner); + panner.connect(oac.destination); + source0.start(0); + return oac; + }, + name: "Stereo Panning with Automation" +}); + +registerTestCase({ + func: function () { + var oac = new OfflineAudioContext(2, DURATION * sampleRate, sampleRate); + var osc = oac.createOscillator(); + osc.type = 'sawtooth'; + var freq = 2000; + osc.frequency.value = freq; + osc.frequency.linearRampToValueAtTime(20, 10.0); + osc.connect(oac.destination); + osc.start(0); + return oac; + }, + name: "Periodic Wave with Automation" +}); + + + +if (typeof(window) == "undefined") { + exports.benchmarks = benchmarks; +} + diff --git a/third_party/webkit/PerformanceTests/webaudio/index.html b/third_party/webkit/PerformanceTests/webaudio/index.html new file mode 100644 index 0000000000..e7329cc5ef --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/index.html @@ -0,0 +1,47 @@ +<html> +<head> +<script src=webaudio-bench.js></script> +<script src=benchmarks.js></script> +<meta charset="utf-8"> +<style> + html { + font-family: helvetica, arial; + } + table { + margin: 1em; + border-collapse: collapse; + } + thead { + background-color: #aaaaaa; + } + tr:nth-child(even) { + background-color: #eeeeee; + } + td { + border: 1px solid black; + text-align: center; + } + #controls { + display: none; + } + #in-progress { + display: none; + } +</style> +</head> +<body> +<div id=loading> +Loading assets +<progress></progress> +</div> +<div id=controls> + <button id=run-all>Run all</button> + <div id=in-progress> + <progress id=progress-bar> </progress> + Benchmark in progress... + </div> +</div> +<div id=results> +</div> +</body> +</html> diff --git a/third_party/webkit/PerformanceTests/webaudio/think-mono-38000.wav b/third_party/webkit/PerformanceTests/webaudio/think-mono-38000.wav Binary files differnew file mode 100644 index 0000000000..953127bde5 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/think-mono-38000.wav diff --git a/third_party/webkit/PerformanceTests/webaudio/think-mono-44100.wav b/third_party/webkit/PerformanceTests/webaudio/think-mono-44100.wav Binary files differnew file mode 100644 index 0000000000..22c1ad6a04 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/think-mono-44100.wav diff --git a/third_party/webkit/PerformanceTests/webaudio/think-mono-48000.wav b/third_party/webkit/PerformanceTests/webaudio/think-mono-48000.wav Binary files differnew file mode 100644 index 0000000000..87be7e5a18 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/think-mono-48000.wav diff --git a/third_party/webkit/PerformanceTests/webaudio/think-mono.wav b/third_party/webkit/PerformanceTests/webaudio/think-mono.wav Binary files differnew file mode 100644 index 0000000000..553a1f3b2b --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/think-mono.wav diff --git a/third_party/webkit/PerformanceTests/webaudio/think-stereo-38000.wav b/third_party/webkit/PerformanceTests/webaudio/think-stereo-38000.wav Binary files differnew file mode 100644 index 0000000000..ab2a7c4271 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/think-stereo-38000.wav diff --git a/third_party/webkit/PerformanceTests/webaudio/think-stereo-44100.wav b/third_party/webkit/PerformanceTests/webaudio/think-stereo-44100.wav Binary files differnew file mode 100644 index 0000000000..017e0cb241 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/think-stereo-44100.wav diff --git a/third_party/webkit/PerformanceTests/webaudio/think-stereo-48000.wav b/third_party/webkit/PerformanceTests/webaudio/think-stereo-48000.wav Binary files differnew file mode 100644 index 0000000000..69175381f0 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/think-stereo-48000.wav diff --git a/third_party/webkit/PerformanceTests/webaudio/update.sh b/third_party/webkit/PerformanceTests/webaudio/update.sh new file mode 100755 index 0000000000..773def3e34 --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/update.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +usage() { + echo "Usage:" + echo "\t$0" /path/to/webaudio-benchmarks + exit 1 +} + +if [ $# != "1" ] +then + usage $0 +fi + +cp $1/* . diff --git a/third_party/webkit/PerformanceTests/webaudio/webaudio-bench.js b/third_party/webkit/PerformanceTests/webaudio/webaudio-bench.js new file mode 100644 index 0000000000..b4fb99612b --- /dev/null +++ b/third_party/webkit/PerformanceTests/webaudio/webaudio-bench.js @@ -0,0 +1,240 @@ +if (window.AudioContext == undefined) { + window.AudioContext = window.webkitAudioContext; + window.OfflineAudioContext = window.webkitOfflineAudioContext; +} + +$ = document.querySelectorAll.bind(document); + +let DURATION = null; +if (location.search) { + let duration = location.search.match(/rendering-buffer-length=(\d+)/); + if (duration) { + DURATION = duration[1]; + } else { + DURATION = 120; + } +} else { + DURATION = 120; +} + + +// Global sample rate at which we run the context. +var sampleRate = 48000; +// Array containing at first the url of the audio resources to fetch, and the +// the actual buffers audio buffer we have at our disposal to for tests. +var sources = []; +// Array containing the results, for each benchmark. +var results = []; +// Array containing the offline contexts used to run the testcases. +var testcases = []; +// Array containing the functions that can return a runnable testcase. +var testcases_registered = []; +// Array containing the audio buffers for each benchmark +var buffers = []; +var playingSource = null; +// audiocontext used to play back the result of the benchmarks +var ac = new AudioContext(); + +function getFile(source, callback) { + var request = new XMLHttpRequest(); + request.open("GET", source.url, true); + request.responseType = "arraybuffer"; + + request.onload = function() { + // decode buffer at its initial sample rate + var ctx = new OfflineAudioContext(1, 1, source.sampleRate); + + ctx.decodeAudioData(request.response, function(buffer) { + callback(buffer, undefined); + }, function() { + callback(undefined, "Error decoding the file " + source.url); + }); + } + request.send(); +} + +function recordResult(result) { + results.push(result); +} + +function benchmark(testcase, ended) { + var context = testcase.ctx; + var start; + + context.oncomplete = function(e) { + var end = Date.now(); + recordResult({ + name: testcase.name, + duration: end - start, + buffer: e.renderedBuffer + }); + ended(); + }; + + start = Date.now(); + context.startRendering(); +} + +function getMonoFile() { + return getSpecificFile({ numberOfChannels: 1 }); +} + +function getStereoFile() { + return getSpecificFile({ numberOfChannels: 2 }); +} + +function matchIfSpecified(a, b) { + if (b) { + return a == b; + } + return true; +} + +function getSpecificFile(spec) { + for (var i = 0 ; i < sources.length; i++) { + if (matchIfSpecified(sources[i].numberOfChannels, spec.numberOfChannels) && + matchIfSpecified(sources[i].sampleRate, spec.sampleRate)) { + return sources[i]; + } + } + + throw new Error("Could not find a file that matches the specs."); +} + +function allDone() { + document.getElementById("in-progress").style.display = "none"; + var result = document.getElementById("results"); + var str = "<table><thead><tr><td>Test name</td><td>Time in ms</td><td>Speedup vs. realtime</td><td>Sound</td></tr></thead>"; + var buffers_base = buffers.length; + var product_of_durations = 1.0; + + for (var i = 0 ; i < results.length; i++) { + var r = results[i]; + product_of_durations *= r.duration; + str += "<tr><td>" + r.name + "</td>" + + "<td>" + r.duration + "</td>"+ + "<td>" + Math.round((r.buffer.duration * 1000) / r.duration) + "x</td>"+ + "<td><button data-soundindex="+(buffers_base + i)+">Play</button> ("+r.buffer.duration+"s)</td>" + +"</tr>"; + buffers[buffers_base + i] = r.buffer; + } + recordResult({ + name: "Geometric Mean", + duration: Math.round(Math.pow(product_of_durations, 1.0/results.length)), + buffer: {} + }); + str += "</table>"; + result.innerHTML += str; + result.addEventListener("click", function(e) { + var t = e.target; + if (t.dataset.soundindex != undefined) { + if (playingSource != null) { + playingSource.button.innerHTML = "Play"; + playingSource.onended = undefined; + playingSource.stop(0); + if (playingSource.button == t) { + playingSource = null; + return; + } + } + playingSource = ac.createBufferSource(); + playingSource.connect(ac.destination); + playingSource.buffer = buffers[t.dataset.soundindex]; + playingSource.start(0); + playingSource.button = t; + t.innerHTML = "Pause"; + playingSource.onended = function () { + playingSource = null; + } + } + }); + + document.getElementById("run-all").disabled = false; + + if (location.search.includes("raptor")) { + var _data = ['raptor-benchmark', 'webaudio', JSON.stringify(results)]; + window.postMessage(_data, '*'); + window.sessionStorage.setItem('benchmark_results', JSON.stringify(_data)); + } else { + var xhr = new XMLHttpRequest(); + xhr.open("POST", "/results", true); + xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded"); + xhr.send("results=" + JSON.stringify(results)); + } +} + +function runOne(i) { + benchmark(testcases[i], function() { + i++; + $("#progress-bar")[0].value++; + if (i < testcases.length) { + runOne(i); + } else { + allDone(); + } + }); +} + +function runAll() { + $("#progress-bar")[0].max = testcases_registered.length; + $("#progress-bar")[0].value = 0; + initAll(); + results = []; + runOne(0); +} + +function initAll() { + for (var i = 0; i < testcases_registered.length; i++) { + testcases[i] = {}; + testcases[i].ctx = testcases_registered[i].func(); + testcases[i].name = testcases_registered[i].name; + } +} + +function loadOne(i, endCallback) { + getFile(sources[i], function(buffer, err) { + if (err) { + throw new Error(msg); + } + + sources[i] = buffer; + i++; + + if (i < sources.length) { + loadOne(i, endCallback); + } else { + endCallback(); + } + }); +} + +function loadAllSources(endCallback) { + loadOne(0, endCallback); +} + +document.addEventListener("DOMContentLoaded", function() { + document.getElementById("run-all").addEventListener("click", function() { + document.getElementById("run-all").disabled = true; + document.getElementById("in-progress").style.display = "inline"; + runAll(); + }); + loadAllSources(function() { + // auto-run when running in raptor + document.getElementById("loading").remove(); + document.getElementById("controls").style.display = "block"; + if (location.search.includes("raptor")) { + document.getElementById("run-all").disabled = true; + document.getElementById("in-progress").style.display = "inline"; + setTimeout(runAll, 100); + } + }); +}); + +/* Public API */ +function registerTestCase(testCase) { + testcases_registered.push(testCase); +} + +function registerTestFile(url) { + sources.push(url); +} |