Support per-note detuning and panning with Sf2 Player (#6602)

* Add `ArrayVector` class template and tests

* Fix counting of failed test suites

* Support detuning and panning with Sf2 Player

* Restrict panning to supported FluidSynth versions

* Fix data array cast type

* Fix tests for Qt<5.10 and correct mistaken test

* DIsplay warning for FluidSynth < 2

* Remove unnecessary clamp

* Update include guard name
This commit is contained in:
Dominic Clark
2023-08-31 12:12:00 +01:00
committed by GitHub
parent 3263bfd555
commit 4804ab6785
7 changed files with 1306 additions and 24 deletions

View File

@@ -494,7 +494,11 @@ IF(WANT_SF2)
find_package(FluidSynth 1.1.0)
if(FluidSynth_FOUND)
SET(LMMS_HAVE_FLUIDSYNTH TRUE)
SET(STATUS_FLUIDSYNTH "OK")
if(FluidSynth_VERSION_STRING VERSION_GREATER_EQUAL 2)
set(STATUS_FLUIDSYNTH "OK")
else()
set(STATUS_FLUIDSYNTH "OK (FluidSynth version < 2: per-note panning unsupported)")
endif()
else()
SET(STATUS_FLUIDSYNTH "not found, libfluidsynth-dev (or similar)"
"is highly recommended")

388
include/ArrayVector.h Normal file
View File

@@ -0,0 +1,388 @@
/*
* ArrayVector.h
*
* Copyright (c) 2023 Dominic Clark
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#ifndef LMMS_ARRAY_VECTOR_H
#define LMMS_ARRAY_VECTOR_H
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <iterator>
#include <memory>
#include <new>
#include <stdexcept>
#include <utility>
#include <type_traits>
namespace lmms {
namespace detail {
template<typename T, typename = void>
constexpr bool is_input_iterator_v = false;
template<typename T>
constexpr bool is_input_iterator_v<T, std::void_t<typename std::iterator_traits<T>::iterator_category>> =
std::is_convertible_v<typename std::iterator_traits<T>::iterator_category, std::input_iterator_tag>;
} // namespace detail
/**
* A container that stores up to a maximum of `N` elements of type `T` directly
* within itself, rather than separately on the heap. Useful when a dynamically
* resizeable container is needed for use in real-time code. Can be thought of
* as a hybrid between `std::array` and `std::vector`. The interface follows
* that of `std::vector` - see standard C++ documentation.
*/
template<typename T, std::size_t N>
class ArrayVector
{
public:
using size_type = std::size_t;
using difference_type = std::ptrdiff_t;
using value_type = T;
using reference = T&;
using const_reference = const T&;
using pointer = T*;
using const_pointer = const T*;
using iterator = pointer;
using const_iterator = const_pointer;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
ArrayVector() = default;
ArrayVector(const ArrayVector& other) noexcept(std::is_nothrow_copy_constructible_v<T>) :
m_size{other.m_size}
{
std::uninitialized_copy(other.begin(), other.end(), begin());
}
ArrayVector(ArrayVector&& other) noexcept(std::is_nothrow_move_constructible_v<T>) :
m_size{other.m_size}
{
std::uninitialized_move(other.begin(), other.end(), begin());
other.clear();
}
ArrayVector(size_type count, const T& value) noexcept(std::is_nothrow_copy_constructible_v<T>) :
m_size{count}
{
assert(count <= N);
std::uninitialized_fill_n(begin(), count, value);
}
explicit ArrayVector(size_type count) noexcept(std::is_nothrow_default_constructible_v<T>) :
m_size{count}
{
assert(count <= N);
std::uninitialized_value_construct_n(begin(), count);
}
template<typename It, std::enable_if_t<detail::is_input_iterator_v<It>, int> = 0>
ArrayVector(It first, It last)
{
// Can't check the size first as the iterator may not be multipass
const auto end = std::uninitialized_copy(first, last, begin());
m_size = end - begin();
assert(m_size <= N);
}
ArrayVector(std::initializer_list<T> il) noexcept(std::is_nothrow_copy_constructible_v<T>) :
m_size{il.size()}
{
assert(il.size() <= N);
std::uninitialized_copy(il.begin(), il.end(), begin());
}
~ArrayVector() { std::destroy(begin(), end()); }
ArrayVector& operator=(const ArrayVector& other)
noexcept(std::is_nothrow_copy_assignable_v<T> && std::is_nothrow_copy_constructible_v<T>)
{
if (this != &other) {
const auto toAssign = std::min(other.size(), size());
const auto assignedFromEnd = other.begin() + toAssign;
const auto assignedToEnd = std::copy(other.begin(), other.begin() + toAssign, begin());
std::destroy(assignedToEnd, end());
std::uninitialized_copy(assignedFromEnd, other.end(), end());
m_size = other.size();
}
return *this;
}
ArrayVector& operator=(ArrayVector&& other)
noexcept(std::is_nothrow_move_assignable_v<T> && std::is_nothrow_move_constructible_v<T>)
{
if (this != &other) {
const auto toAssign = std::min(other.size(), size());
const auto assignedFromEnd = other.begin() + toAssign;
const auto assignedToEnd = std::move(other.begin(), other.begin() + toAssign, begin());
std::destroy(assignedToEnd, end());
std::uninitialized_move(assignedFromEnd, other.end(), end());
m_size = other.size();
other.clear();
}
return *this;
}
ArrayVector& operator=(std::initializer_list<T> il)
noexcept(std::is_nothrow_copy_assignable_v<T> && std::is_nothrow_copy_constructible_v<T>)
{
assert(il.size() <= N);
const auto toAssign = std::min(il.size(), size());
const auto assignedFromEnd = il.begin() + toAssign;
const auto assignedToEnd = std::copy(il.begin(), assignedFromEnd, begin());
std::destroy(assignedToEnd, end());
std::uninitialized_copy(assignedFromEnd, il.end(), end());
m_size = il.size();
return *this;
}
void assign(size_type count, const T& value)
noexcept(std::is_nothrow_copy_assignable_v<T> && std::is_nothrow_copy_constructible_v<T>)
{
assert(count <= N);
const auto temp = value;
const auto toAssign = std::min(count, size());
const auto toConstruct = count - toAssign;
const auto assignedToEnd = std::fill_n(begin(), toAssign, temp);
std::destroy(assignedToEnd, end());
std::uninitialized_fill_n(assignedToEnd, toConstruct, temp);
m_size = count;
}
template<typename It, std::enable_if_t<detail::is_input_iterator_v<It>, int> = 0>
void assign(It first, It last)
{
// Can't check the size first as the iterator may not be multipass
auto pos = begin();
for (; first != last && pos != end(); ++pos, ++first) {
*pos = *first;
}
std::destroy(pos, end());
pos = std::uninitialized_copy(first, last, pos);
m_size = pos - begin();
assert(m_size <= N);
}
reference at(size_type index)
{
if (index >= m_size) { throw std::out_of_range{"index out of range"}; }
return data()[index];
}
const_reference at(size_type index) const
{
if (index >= m_size) { throw std::out_of_range{"index out of range"}; }
return data()[index];
}
reference operator[](size_type index) noexcept
{
assert(index < m_size);
return data()[index];
}
const_reference operator[](size_type index) const noexcept
{
assert(index < m_size);
return data()[index];
}
reference front() noexcept { return operator[](0); }
const_reference front() const noexcept { return operator[](0); }
reference back() noexcept { return operator[](m_size - 1); }
const_reference back() const noexcept { return operator[](m_size - 1); }
pointer data() noexcept { return *std::launder(reinterpret_cast<T(*)[N]>(m_data)); }
const_pointer data() const noexcept { return *std::launder(reinterpret_cast<const T(*)[N]>(m_data)); }
iterator begin() noexcept { return data(); }
const_iterator begin() const noexcept { return data(); }
const_iterator cbegin() const noexcept { return data(); }
iterator end() noexcept { return data() + m_size; }
const_iterator end() const noexcept { return data() + m_size; }
const_iterator cend() const noexcept { return data() + m_size; }
reverse_iterator rbegin() noexcept { return std::reverse_iterator{end()}; }
const_reverse_iterator rbegin() const noexcept { return std::reverse_iterator{end()}; }
const_reverse_iterator crbegin() const noexcept { return std::reverse_iterator{cend()}; }
reverse_iterator rend() noexcept { return std::reverse_iterator{begin()}; }
const_reverse_iterator rend() const noexcept { return std::reverse_iterator{begin()}; }
const_reverse_iterator crend() const noexcept { return std::reverse_iterator{cbegin()}; }
bool empty() const noexcept { return m_size == 0; }
bool full() const noexcept { return m_size == N; }
size_type size() const noexcept { return m_size; }
size_type max_size() const noexcept { return N; }
size_type capacity() const noexcept { return N; }
void clear() noexcept
{
std::destroy(begin(), end());
m_size = 0;
}
iterator insert(const_iterator pos, const T& value) { return emplace(pos, value); }
iterator insert(const_iterator pos, T&& value) { return emplace(pos, std::move(value)); }
iterator insert(const_iterator pos, size_type count, const T& value)
{
assert(m_size + count <= N);
assert(cbegin() <= pos && pos <= cend());
const auto mutPos = begin() + (pos - cbegin());
const auto newEnd = std::uninitialized_fill_n(end(), count, value);
std::rotate(mutPos, end(), newEnd);
m_size += count;
return mutPos;
}
template<typename It, std::enable_if_t<detail::is_input_iterator_v<It>, int> = 0>
iterator insert(const_iterator pos, It first, It last)
{
// Can't check the size first as the iterator may not be multipass
assert(cbegin() <= pos && pos <= cend());
const auto mutPos = begin() + (pos - cbegin());
const auto newEnd = std::uninitialized_copy(first, last, end());
std::rotate(mutPos, end(), newEnd);
m_size = newEnd - begin();
assert(m_size <= N);
return mutPos;
}
iterator insert(const_iterator pos, std::initializer_list<T> il) { return insert(pos, il.begin(), il.end()); }
template<typename... Args>
iterator emplace(const_iterator pos, Args&&... args)
{
assert(cbegin() <= pos && pos <= cend());
const auto mutPos = begin() + (pos - cbegin());
emplace_back(std::forward<Args>(args)...);
std::rotate(mutPos, end() - 1, end());
return mutPos;
}
iterator erase(const_iterator pos) { return erase(pos, pos + 1); }
iterator erase(const_iterator first, const_iterator last)
{
assert(cbegin() <= first && first <= last && last <= cend());
const auto mutFirst = begin() + (first - cbegin());
const auto mutLast = begin() + (last - cbegin());
const auto newEnd = std::move(mutLast, end(), mutFirst);
std::destroy(newEnd, end());
m_size = newEnd - begin();
return mutFirst;
}
void push_back(const T& value) { emplace_back(value); }
void push_back(T&& value) { emplace_back(std::move(value)); }
template<typename... Args>
reference emplace_back(Args&&... args)
{
assert(!full());
// TODO C++20: Use std::construct_at
const auto result = new(static_cast<void*>(end())) T(std::forward<Args>(args)...);
++m_size;
return *result;
}
void pop_back()
{
assert(!empty());
--m_size;
std::destroy_at(end());
}
void resize(size_type size)
{
if (size > N) { throw std::length_error{"size exceeds maximum size"}; }
if (size < m_size) {
std::destroy(begin() + size, end());
} else {
std::uninitialized_value_construct(end(), begin() + size);
}
m_size = size;
}
void resize(size_type size, const value_type& value)
{
if (size > N) { throw std::length_error{"size exceeds maximum size"}; }
if (size < m_size) {
std::destroy(begin() + size, end());
} else {
std::uninitialized_fill(end(), begin() + size, value);
}
m_size = size;
}
void swap(ArrayVector& other)
noexcept(std::is_nothrow_swappable_v<T> && std::is_nothrow_move_constructible_v<T>)
{
using std::swap;
swap(*this, other);
}
friend void swap(ArrayVector& a, ArrayVector& b)
noexcept(std::is_nothrow_swappable_v<T> && std::is_nothrow_move_constructible_v<T>)
{
const auto toSwap = std::min(a.size(), b.size());
const auto aSwapEnd = a.begin() + toSwap;
const auto bSwapEnd = b.begin() + toSwap;
std::swap_ranges(a.begin(), aSwapEnd, b.begin());
std::uninitialized_move(aSwapEnd, a.end(), bSwapEnd);
std::uninitialized_move(bSwapEnd, b.end(), aSwapEnd);
std::destroy(aSwapEnd, a.end());
std::destroy(bSwapEnd, b.end());
std::swap(a.m_size, b.m_size);
}
// TODO C++20: Replace with operator<=>
friend bool operator<(const ArrayVector& l, const ArrayVector& r)
{
return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end());
}
friend bool operator<=(const ArrayVector& l, const ArrayVector& r) { return !(r < l); }
friend bool operator>(const ArrayVector& l, const ArrayVector& r) { return r < l; }
friend bool operator>=(const ArrayVector& l, const ArrayVector& r) { return !(l < r); }
friend bool operator==(const ArrayVector& l, const ArrayVector& r)
{
return std::equal(l.begin(), l.end(), r.begin(), r.end());
}
// TODO C++20: Remove
friend bool operator!=(const ArrayVector& l, const ArrayVector& r) { return !(l == r); }
private:
alignas(T) std::byte m_data[std::max(N * sizeof(T), std::size_t{1})]; // Intentionally a raw array
size_type m_size = 0;
};
} // namespace lmms
#endif // LMMS_ARRAY_VECTOR_H

View File

@@ -108,6 +108,9 @@ public:
return m_unpitchedFrequency;
}
//! Get the current per-note detuning for this note
float currentDetuning() const { return m_baseDetuning->value(); }
/*! Renders one chunk using the attached instrument into the buffer */
void play( sampleFrame* buffer ) override;

View File

@@ -30,6 +30,7 @@
#include <QDomElement>
#include <QLabel>
#include "ArrayVector.h"
#include "AudioEngine.h"
#include "ConfigManager.h"
#include "FileDialog.h"
@@ -71,17 +72,47 @@ Plugin::Descriptor PLUGIN_EXPORT sf2player_plugin_descriptor =
}
/**
* A non-owning reference to a single FluidSynth voice, for tracking whether the
* referenced voice is still the same voice that was passed to the constructor.
*/
class FluidVoice
{
public:
//! Create a reference to the voice currently pointed at by `voice`.
explicit FluidVoice(fluid_voice_t* voice) :
m_voice{voice},
m_id{fluid_voice_get_id(voice)}
{ }
//! Get a pointer to the referenced voice.
fluid_voice_t* get() const noexcept { return m_voice; }
//! Test whether this object still refers to the original voice.
bool isValid() const
{
return fluid_voice_get_id(m_voice) == m_id && fluid_voice_is_playing(m_voice);
}
private:
fluid_voice_t* m_voice;
unsigned int m_id;
};
struct Sf2PluginData
{
int midiNote;
int lastPanning;
float lastVelocity;
fluid_voice_t * fluidVoice;
// The soundfonts I checked used at most two voices per note, so space for
// four should be safe. This may need to be increased if a soundfont with
// more voices per note is found.
ArrayVector<FluidVoice, 4> fluidVoices;
bool isNew;
f_cnt_t offset;
bool noteOffSent;
} ;
panning_t panning;
};
@@ -681,10 +712,10 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * )
pluginData->midiNote = midiNote;
pluginData->lastPanning = 0;
pluginData->lastVelocity = _n->midiVelocity( baseVelocity );
pluginData->fluidVoice = nullptr;
pluginData->isNew = true;
pluginData->offset = _n->offset();
pluginData->noteOffSent = false;
pluginData->panning = _n->getPanning();
_n->m_pluginData = pluginData;
@@ -703,6 +734,17 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * )
m_playingNotes.append( _n );
m_playingNotesMutex.unlock();
}
// Update the pitch of all the voices
if (const auto data = static_cast<Sf2PluginData*>(_n->m_pluginData)) {
const auto detuning = _n->currentDetuning();
for (const auto& voice : data->fluidVoices) {
if (voice.isValid()) {
fluid_voice_gen_set(voice.get(), GEN_COARSETUNE, detuning);
fluid_voice_update_param(voice.get(), GEN_COARSETUNE);
}
}
}
}
@@ -715,35 +757,47 @@ void Sf2Instrument::noteOn( Sf2PluginData * n )
const int poly = fluid_synth_get_polyphony( m_synth );
#ifndef _MSC_VER
fluid_voice_t* voices[poly];
unsigned int id[poly];
#else
const auto voices = static_cast<fluid_voice_t**>(_alloca(poly * sizeof(fluid_voice_t*)));
const auto id = static_cast<unsigned int*>(_alloca(poly * sizeof(unsigned int)));
#endif
fluid_synth_get_voicelist( m_synth, voices, poly, -1 );
for( int i = 0; i < poly; ++i )
{
id[i] = 0;
}
for( int i = 0; i < poly && voices[i]; ++i )
{
id[i] = fluid_voice_get_id( voices[i] );
}
fluid_synth_noteon( m_synth, m_channel, n->midiNote, n->lastVelocity );
// get new voice and save it
fluid_synth_get_voicelist( m_synth, voices, poly, -1 );
for( int i = 0; i < poly && voices[i]; ++i )
// Get any new voices and store them in the plugin data
fluid_synth_get_voicelist(m_synth, voices, poly, -1);
for (int i = 0; i < poly && voices[i] && !n->fluidVoices.full(); ++i)
{
const unsigned int newID = fluid_voice_get_id( voices[i] );
if( id[i] != newID || newID == 0 )
{
n->fluidVoice = voices[i];
break;
const auto voice = voices[i];
// FluidSynth stops voices with the same channel and pitch upon note-on,
// so voices with the current channel and pitch are playing this note.
if (fluid_voice_get_channel(voice) == m_channel
&& fluid_voice_get_key(voice) == n->midiNote
&& fluid_voice_is_on(voice)
) {
n->fluidVoices.emplace_back(voices[i]);
}
}
#if FLUIDSYNTH_VERSION_MAJOR >= 2
// Smallest balance value that results in full attenuation of one channel.
// Corresponds to internal FluidSynth macro `FLUID_CB_AMP_SIZE`.
constexpr static auto maxBalance = 1441.f;
// Convert panning from linear to exponential for FluidSynth
const auto panning = n->panning;
const auto factor = 1.f - std::abs(panning) / static_cast<float>(PanningRight);
const auto balance = std::copysign(
factor <= 0 ? maxBalance : std::min(-200.f * std::log10(factor), maxBalance),
panning
);
// Set note panning on all the voices
for (const auto& voice : n->fluidVoices) {
if (voice.isValid()) {
fluid_voice_gen_set(voice.get(), GEN_CUSTOM_BALANCE, balance);
fluid_voice_update_param(voice.get(), GEN_CUSTOM_BALANCE);
}
}
#endif
m_synthMutex.unlock();
m_notesRunningMutex.lock();
@@ -859,6 +913,7 @@ void Sf2Instrument::play( sampleFrame * _working_buffer )
void Sf2Instrument::renderFrames( f_cnt_t frames, sampleFrame * buf )
{
m_synthMutex.lock();
fluid_synth_get_gain(m_synth); // This flushes voice updates as a side effect
if( m_internalSampleRate < Engine::audioEngine()->processingSampleRate() &&
m_srcState != nullptr )
{

View File

@@ -19,6 +19,7 @@ ADD_EXECUTABLE(tests
QTestSuite.cpp
$<TARGET_OBJECTS:lmmsobjs>
src/core/ArrayVectorTest.cpp
src/core/AutomatableModelTest.cpp
src/core/ProjectVersionTest.cpp
src/core/RelativePathsTest.cpp

View File

@@ -16,7 +16,7 @@ int main(int argc, char* argv[])
int failed = 0;
for (QTestSuite*& suite : QTestSuite::suites())
{
failed += QTest::qExec(suite, argc, argv);
if (QTest::qExec(suite, argc, argv) != 0) { ++failed; }
}
qDebug() << "<<" << failed << "out of"<<numsuites<<"test suites failed.";
return failed;

View File

@@ -0,0 +1,831 @@
/*
* ArrayVectorTest.cpp
*
* Copyright (c) 2023 Dominic Clark
*
* This file is part of LMMS - https://lmms.io
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA.
*
*/
#include "ArrayVector.h"
#include <array>
#include <iterator>
#include "QTestSuite.h"
using lmms::ArrayVector;
struct ShouldNotConstruct
{
ShouldNotConstruct() { QFAIL("should not construct"); }
};
struct ShouldNotDestruct
{
~ShouldNotDestruct() { QFAIL("should not destruct"); }
};
enum class Construction { Default, Copy, Move, CopyAssign, MoveAssign };
struct Constructible
{
Constructible() : construction{Construction::Default} {}
Constructible(const Constructible&) : construction{Construction::Copy} {}
Constructible(Constructible&&) : construction{Construction::Move} {}
Constructible& operator=(const Constructible&) { construction = Construction::CopyAssign; return *this; }
Constructible& operator=(Constructible&&) { construction = Construction::MoveAssign; return *this; }
Construction construction;
};
struct DestructorCheck
{
~DestructorCheck() { *destructed = true; }
bool* destructed;
};
class ArrayVectorTest : QTestSuite
{
Q_OBJECT
private slots:
void defaultConstructorTest()
{
// Ensure no elements are constructed
const auto v = ArrayVector<ShouldNotConstruct, 1>();
// Ensure the container is empty
QVERIFY(v.empty());
}
void copyConstructorTest()
{
{
// Ensure all elements are copy constructed
const auto v = ArrayVector<Constructible, 1>{{}};
const auto copy = v;
for (const auto& element : copy) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure corresponding elements are used
const auto v = ArrayVector<int, 5>{1, 2, 3};
const auto copy = v;
const auto expected = std::array{1, 2, 3};
QVERIFY(std::equal(copy.begin(), copy.end(), expected.begin(), expected.end()));
}
}
void moveConstructorTest()
{
{
// Ensure all elements are move constructed
auto v = ArrayVector<Constructible, 1>{{}};
const auto moved = std::move(v);
for (const auto& element : moved) {
QCOMPARE(element.construction, Construction::Move);
}
}
{
// Ensure corresponding elements are used
auto v = ArrayVector<int, 5>{1, 2, 3};
const auto moved = std::move(v);
const auto expected = std::array{1, 2, 3};
QVERIFY(std::equal(moved.begin(), moved.end(), expected.begin(), expected.end()));
// Move construction should leave the source empty
QVERIFY(v.empty());
}
}
void fillValueConstructorTest()
{
// Ensure all elements are copy constructed
const auto v = ArrayVector<Constructible, 2>(1, {});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
// Ensure the container has the correct size
QCOMPARE(v.size(), std::size_t{1});
}
void fillDefaultConstructorTest()
{
// Ensure all elements are copy constructed
const auto v = ArrayVector<Constructible, 2>(1);
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Default);
}
// Ensure the container has the correct size
QCOMPARE(v.size(), std::size_t{1});
}
void rangeConstructorTest()
{
{
// Ensure the elements are copy constructed from normal iterators
const auto data = std::array{Constructible{}};
const auto v = ArrayVector<Constructible, 1>(data.begin(), data.end());
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure the elements are move constructed from move iterators
auto data = std::array{Constructible{}};
const auto v = ArrayVector<Constructible, 1>(
std::move_iterator{data.begin()}, std::move_iterator{data.end()});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Move);
}
}
{
// Ensure corresponding elements are used
const auto data = std::array{1, 2, 3};
const auto v = ArrayVector<int, 5>(data.begin(), data.end());
QVERIFY(std::equal(v.begin(), v.end(), data.begin(), data.end()));
}
}
void initializerListConstructorTest()
{
// Ensure the container is constructed with the correct data
const auto v = ArrayVector<int, 5>{1, 2, 3};
const auto expected = std::array{1, 2, 3};
QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end()));
}
void destructorTest()
{
{
// Should not call destructors for space without elements
const auto v = ArrayVector<ShouldNotDestruct, 1>{};
}
{
// Should call destructors for all elements
auto destructed = false;
{
const auto v = ArrayVector<DestructorCheck, 1>{{&destructed}};
}
QVERIFY(destructed);
}
}
void copyAssignmentTest()
{
{
// Self-assignment should not change the contents
auto v = ArrayVector<int, 5>{1, 2, 3};
const auto oldValue = v;
v = v;
QCOMPARE(v, oldValue);
}
{
// Assignment to a larger container should copy assign
const auto src = ArrayVector<Constructible, 5>(3);
auto dst = ArrayVector<Constructible, 5>(5);
dst = src;
QCOMPARE(dst.size(), std::size_t{3});
for (const auto& element : dst) {
QCOMPARE(element.construction, Construction::CopyAssign);
}
}
{
// Assignment to a smaller container should copy construct
const auto src = ArrayVector<Constructible, 5>(3);
auto dst = ArrayVector<Constructible, 5>{};
dst = src;
QCOMPARE(dst.size(), std::size_t{3});
for (const auto& element : dst) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure corresponding elements are used
const auto src = ArrayVector<int, 5>{1, 2, 3};
auto dst = ArrayVector<int, 5>{};
dst = src;
QCOMPARE(dst, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void moveAssignmentTest()
{
{
// Self-assignment should not change the contents
auto v = ArrayVector<int, 5>{1, 2, 3};
const auto oldValue = v;
v = std::move(v);
QCOMPARE(v, oldValue);
}
{
// Assignment to a larger container should move assign
auto src = ArrayVector<Constructible, 5>(3);
auto dst = ArrayVector<Constructible, 5>(5);
dst = std::move(src);
QCOMPARE(dst.size(), std::size_t{3});
for (const auto& element : dst) {
QCOMPARE(element.construction, Construction::MoveAssign);
}
}
{
// Assignment to a smaller container should move construct
auto src = ArrayVector<Constructible, 5>(3);
auto dst = ArrayVector<Constructible, 5>{};
dst = std::move(src);
QCOMPARE(dst.size(), std::size_t{3});
for (const auto& element : dst) {
QCOMPARE(element.construction, Construction::Move);
}
}
{
// Ensure corresponding elements are used
auto src = ArrayVector<int, 5>{1, 2, 3};
auto dst = ArrayVector<int, 5>{};
dst = std::move(src);
QCOMPARE(dst, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void initializerListAssignmentTest()
{
{
// Assignment to a larger container should copy assign
auto v = ArrayVector<Constructible, 2>(2);
v = {Constructible{}};
QCOMPARE(v.size(), std::size_t{1});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::CopyAssign);
}
}
{
// Assignment to a smaller container should copy construct
auto v = ArrayVector<Constructible, 2>{};
v = {Constructible{}};
QCOMPARE(v.size(), std::size_t{1});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure corresponding elements are used
auto v = ArrayVector<int, 5>{};
v = {1, 2, 3};
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void fillValueAssignTest()
{
{
// Assignment to a larger container should copy assign
auto v = ArrayVector<Constructible, 5>(5);
v.assign(3, {});
QCOMPARE(v.size(), std::size_t{3});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::CopyAssign);
}
}
{
// Assignment to a smaller container should copy construct
auto v = ArrayVector<Constructible, 5>{};
v.assign(3, {});
QCOMPARE(v.size(), std::size_t{3});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure correct value is filled
auto v = ArrayVector<int, 5>{};
v.assign(3, 1);
QCOMPARE(v, (ArrayVector<int, 5>{1, 1, 1}));
}
}
void rangeAssignTest()
{
{
// Assignment to a larger container should copy assign
const auto data = std::array{Constructible{}};
auto v = ArrayVector<Constructible, 2>(2);
v.assign(data.begin(), data.end());
QCOMPARE(v.size(), std::size_t{1});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::CopyAssign);
}
}
{
// Assignment to a smaller container should copy construct
const auto data = std::array{Constructible{}};
auto v = ArrayVector<Constructible, 2>{};
v.assign(data.begin(), data.end());
QCOMPARE(v.size(), std::size_t{1});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure correct value is filled
const auto data = std::array{1, 2, 3};
auto v = ArrayVector<int, 5>{};
v.assign(data.begin(), data.end());
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void atTest()
{
{
// Non-const version
auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.at(1), 2);
QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range);
}
{
// Const version
const auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.at(1), 2);
QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range);
}
}
void subscriptTest()
{
{
// Non-const version
auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v[1], 2);
}
{
// Const version
const auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v[1], 2);
}
}
void frontTest()
{
{
// Non-const version
auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.front(), 1);
}
{
// Const version
const auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.front(), 1);
}
}
void backTest()
{
{
// Non-const version
auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.back(), 3);
}
{
// Const version
const auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.back(), 3);
}
}
void dataTest()
{
{
// Non-const version
auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.data(), &v.front());
}
{
// Const version
const auto v = ArrayVector<int, 5>{1, 2, 3};
QCOMPARE(v.data(), &v.front());
}
}
void beginEndTest()
{
const auto expected = std::array{1, 2, 3};
{
// Non-const version
auto v = ArrayVector<int, 5>{1, 2, 3};
QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end()));
QVERIFY(std::equal(v.cbegin(), v.cend(), expected.begin(), expected.end()));
}
{
// Const version
const auto v = ArrayVector<int, 5>{1, 2, 3};
QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end()));
}
}
void rbeginRendTest()
{
const auto expected = std::array{3, 2, 1};
{
// Non-const version
auto v = ArrayVector<int, 5>{1, 2, 3};
QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end()));
QVERIFY(std::equal(v.crbegin(), v.crend(), expected.begin(), expected.end()));
}
{
// Const version
const auto v = ArrayVector<int, 5>{1, 2, 3};
QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end()));
}
}
void emptyFullSizeMaxCapacityTest()
{
auto v = ArrayVector<int, 2>{};
QVERIFY(v.empty());
QVERIFY(!v.full());
QCOMPARE(v.size(), std::size_t{0});
QCOMPARE(v.max_size(), std::size_t{2});
QCOMPARE(v.capacity(), std::size_t{2});
v.push_back(1);
QVERIFY(!v.empty());
QVERIFY(!v.full());
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v.max_size(), std::size_t{2});
QCOMPARE(v.capacity(), std::size_t{2});
v.push_back(2);
QVERIFY(!v.empty());
QVERIFY(v.full());
QCOMPARE(v.size(), std::size_t{2});
QCOMPARE(v.max_size(), std::size_t{2});
QCOMPARE(v.capacity(), std::size_t{2});
auto empty = ArrayVector<int, 0>{};
QVERIFY(empty.empty());
QVERIFY(empty.full());
QCOMPARE(empty.size(), std::size_t{0});
QCOMPARE(empty.max_size(), std::size_t{0});
QCOMPARE(empty.capacity(), std::size_t{0});
}
void insertValueTest()
{
{
// Copy
const auto data = Constructible{};
auto v = ArrayVector<Constructible, 1>{};
v.insert(v.cbegin(), data);
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Copy);
}
{
// Move
auto v = ArrayVector<Constructible, 1>{};
v.insert(v.cbegin(), Constructible{});
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Move);
}
{
// Ensure the correct value is used (copy)
const auto data = 1;
auto v = ArrayVector<int, 5>{2, 3};
v.insert(v.cbegin(), data);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
{
// Ensure the correct value is used (move)
auto v = ArrayVector<int, 5>{2, 3};
v.insert(v.cbegin(), 1);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void insertFillValueTest()
{
{
// Insertion should copy construct
auto v = ArrayVector<Constructible, 5>{};
v.insert(v.cbegin(), 3, {});
QCOMPARE(v.size(), std::size_t{3});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure correct value is filled
auto v = ArrayVector<int, 5>{1, 3};
v.insert(v.cbegin() + 1, 3, 2);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 2, 2, 3}));
}
}
void insertRangeTest()
{
{
// Insertion should copy construct
const auto data = std::array{Constructible{}};
auto v = ArrayVector<Constructible, 2>{};
v.insert(v.cbegin(), data.begin(), data.end());
QCOMPARE(v.size(), std::size_t{1});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure correct value is filled
const auto data = std::array{2, 3};
auto v = ArrayVector<int, 5>{1, 4};
v.insert(v.cbegin() + 1, data.begin(), data.end());
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3, 4}));
}
}
void insertInitializerListTest()
{
{
// Insertion should copy construct
auto v = ArrayVector<Constructible, 2>{};
v.insert(v.cbegin(), {Constructible{}});
QCOMPARE(v.size(), std::size_t{1});
for (const auto& element : v) {
QCOMPARE(element.construction, Construction::Copy);
}
}
{
// Ensure corresponding elements are used
auto v = ArrayVector<int, 5>{1, 4};
v.insert(v.cbegin() + 1, {2, 3});
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3, 4}));
}
}
void emplaceTest()
{
{
// Ensure the value is constructed in-place
auto v = ArrayVector<Constructible, 1>{};
v.emplace(v.cbegin());
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Default);
}
{
// Ensure the correct value is used (move)
auto v = ArrayVector<int, 5>{2, 3};
v.emplace(v.cbegin(), 1);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void eraseTest()
{
{
// Ensure destructors are run
auto destructed = false;
auto v = ArrayVector<DestructorCheck, 1>{{&destructed}};
v.erase(v.cbegin());
QVERIFY(destructed);
}
{
// Ensure the result is correct
auto v = ArrayVector<int, 5>{10, 1, 2, 3};
v.erase(v.cbegin());
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void eraseRangeTest()
{
{
// Ensure destructors are run
auto destructed = false;
auto v = ArrayVector<DestructorCheck, 1>{{&destructed}};
v.erase(v.cbegin(), v.cend());
QVERIFY(destructed);
}
{
// Ensure the result is correct
auto v = ArrayVector<int, 5>{1, 20, 21, 2, 3};
v.erase(v.cbegin() + 1, v.cbegin() + 3);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void pushBackTest()
{
{
// Copy
const auto data = Constructible{};
auto v = ArrayVector<Constructible, 1>{};
v.push_back(data);
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Copy);
}
{
// Move
auto v = ArrayVector<Constructible, 1>{};
v.push_back({});
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Move);
}
{
// Ensure the correct value is used (copy)
const auto data = 3;
auto v = ArrayVector<int, 5>{1, 2};
v.push_back(data);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
{
// Ensure the correct value is used (move)
auto v = ArrayVector<int, 5>{1, 2};
v.push_back(3);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void emplaceBackTest()
{
{
// Ensure the value is constructed in-place
auto v = ArrayVector<Constructible, 1>{};
v.emplace_back();
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Default);
}
{
// Ensure the correct value is used (move)
auto v = ArrayVector<int, 5>{1, 2};
v.emplace_back(3);
QCOMPARE(v, (ArrayVector<int, 5>{1, 2, 3}));
}
}
void popBackTest()
{
{
// Ensure destructors are run
auto destructed = false;
auto v = ArrayVector<DestructorCheck, 1>{{&destructed}};
v.pop_back();
QVERIFY(destructed);
}
{
// Ensure the result is correct
auto v = ArrayVector<int, 5>{1, 2, 3};
v.pop_back();
QCOMPARE(v, (ArrayVector<int, 5>{1, 2}));
}
}
void resizeDefaultTest()
{
{
// Smaller
auto destructed = false;
auto v = ArrayVector<DestructorCheck, 1>{{&destructed}};
QCOMPARE(v.size(), std::size_t{1});
v.resize(0);
QCOMPARE(v.size(), std::size_t{0});
QVERIFY(destructed);
}
{
// Bigger
auto v = ArrayVector<Constructible, 1>{};
QCOMPARE(v.size(), std::size_t{0});
v.resize(1);
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Default);
}
{
// Too big
auto v = ArrayVector<int, 1>{};
QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error);
}
}
void resizeValueTest()
{
{
// Smaller
auto dummy = false;
auto destructed = false;
auto v = ArrayVector<DestructorCheck, 1>{{&destructed}};
QCOMPARE(v.size(), std::size_t{1});
v.resize(0, {&dummy});
QCOMPARE(v.size(), std::size_t{0});
QVERIFY(destructed);
}
{
// Bigger
auto v = ArrayVector<Constructible, 1>{};
QCOMPARE(v.size(), std::size_t{0});
v.resize(1, {});
QCOMPARE(v.size(), std::size_t{1});
QCOMPARE(v[0].construction, Construction::Copy);
}
{
// Too big
auto v = ArrayVector<int, 1>{};
QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error);
}
{
// Ensure the correct value is used
auto v = ArrayVector<int, 1>{};
v.resize(1, 1);
QCOMPARE(v, (ArrayVector<int, 1>{1}));
}
}
void clearTest()
{
{
// Ensure destructors are run
auto destructed = false;
auto v = ArrayVector<DestructorCheck, 1>{{&destructed}};
v.clear();
QVERIFY(destructed);
}
{
// Ensure the result is correct
auto v = ArrayVector<int, 5>{1, 2, 3};
v.clear();
QCOMPARE(v, (ArrayVector<int, 5>{}));
}
}
void memberSwapTest()
{
auto a = ArrayVector<int, 5>{1, 2, 3, 4};
auto b = ArrayVector<int, 5>{2, 4, 6};
const auto aOriginal = a;
const auto bOriginal = b;
a.swap(b);
QCOMPARE(a, bOriginal);
QCOMPARE(b, aOriginal);
}
void freeSwapTest()
{
auto a = ArrayVector<int, 5>{1, 2, 3, 4};
auto b = ArrayVector<int, 5>{2, 4, 6};
const auto aOriginal = a;
const auto bOriginal = b;
swap(a, b);
QCOMPARE(a, bOriginal);
QCOMPARE(b, aOriginal);
}
void comparisonTest()
{
const auto v = ArrayVector<int, 5>{1, 2, 3};
const auto l = ArrayVector<int, 5>{1, 2, 2};
const auto e = ArrayVector<int, 5>{1, 2, 3};
const auto g = ArrayVector<int, 5>{1, 3, 3};
QVERIFY(l < v);
QVERIFY(!(e < v));
QVERIFY(!(g < v));
QVERIFY(l <= v);
QVERIFY(e <= v);
QVERIFY(!(g <= v));
QVERIFY(!(l > v));
QVERIFY(!(e > v));
QVERIFY(g > v);
QVERIFY(!(l >= v));
QVERIFY(e >= v);
QVERIFY(g >= v);
QVERIFY(!(l == v));
QVERIFY(e == v);
QVERIFY(!(g == v));
QVERIFY(l != v);
QVERIFY(!(e != v));
QVERIFY(g != v);
}
} ArrayVectorTests;
#include "ArrayVectorTest.moc"