/*
  ==============================================================================

   This file is part of the JUCE examples.
   Copyright (c) 2017 - ROLI Ltd.

   The code included in this file is provided under the terms of the ISC license
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
   To use, copy, modify, and/or distribute this software for any purpose with or
   without fee is hereby granted provided that the above copyright notice and
   this permission notice appear in all copies.

   THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
   WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
   PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

/*******************************************************************************
 The block below describes the properties of this PIP. A PIP is a short snippet
 of code that can be read by the Projucer and used to generate a JUCE project.

 BEGIN_JUCE_PIP_METADATA

 name:             SamplerPlugin
 version:          1.0.0
 vendor:           JUCE
 website:          http://juce.com
 description:      Sampler audio plugin.

 dependencies:     juce_audio_basics, juce_audio_devices, juce_audio_formats,
                   juce_audio_plugin_client, juce_audio_processors,
                   juce_audio_utils, juce_core, juce_data_structures,
                   juce_events, juce_graphics, juce_gui_basics, juce_gui_extra
 exporters:        xcode_mac, vs2019

 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1

 type:             AudioProcessor
 mainClass:        SamplerAudioProcessor

 useLocalCopy:     1

 END_JUCE_PIP_METADATA

*******************************************************************************/

#pragma once

#include "../Assets/DemoUtilities.h"

#include <array>
#include <atomic>
#include <memory>
#include <vector>
#include <tuple>
#include <iomanip>
#include <sstream>
#include <functional>
#include <mutex>

namespace IDs
{

#define DECLARE_ID(name) const juce::Identifier name (#name);

DECLARE_ID (DATA_MODEL)
DECLARE_ID (sampleReader)
DECLARE_ID (centreFrequencyHz)
DECLARE_ID (loopMode)
DECLARE_ID (loopPointsSeconds)

DECLARE_ID (MPE_SETTINGS)
DECLARE_ID (synthVoices)
DECLARE_ID (voiceStealingEnabled)
DECLARE_ID (legacyModeEnabled)
DECLARE_ID (mpeZoneLayout)
DECLARE_ID (legacyFirstChannel)
DECLARE_ID (legacyLastChannel)
DECLARE_ID (legacyPitchbendRange)

DECLARE_ID (VISIBLE_RANGE)
DECLARE_ID (totalRange)
DECLARE_ID (visibleRange)

#undef DECLARE_ID

} // namespace IDs

enum class LoopMode
{
    none,
    forward,
    pingpong
};

template <typename Movable>
class MoveOnlyFifo final
{
public:
    explicit MoveOnlyFifo (int size)
        : buffer ((size_t) size),
          abstractFifo (size)
    {}

    MoveOnlyFifo()
        : MoveOnlyFifo (1024)
    {}

    template <typename Convertible>
    Convertible push (Convertible item) noexcept
    {
        auto writer = abstractFifo.write (1);

        if (writer.blockSize1 == 1)
        {
            buffer[(size_t) writer.startIndex1] = move (item);
            item = {};
        }
        else if (writer.blockSize2 == 1)
        {
            buffer[(size_t) writer.startIndex2] = move (item);
            item = {};
        }

        return item;
    }

    Movable pop() noexcept
    {
        auto reader = abstractFifo.read (1);

        if (reader.blockSize1 == 1)
            return move (buffer[(size_t) reader.startIndex1]);

        if (reader.blockSize2 == 1)
            return move (buffer[(size_t) reader.startIndex2]);

        return {};
    }

private:
    std::vector<Movable> buffer;
    AbstractFifo abstractFifo;
};

//==============================================================================
// Represents the constant parts of an audio sample: its name, sample rate,
// length, and the audio sample data itself.
// Samples might be pretty big, so we'll keep shared_ptrs to them most of the
// time, to reduce duplication and copying.
class Sample final
{
public:
    Sample (AudioFormatReader& source, double maxSampleLengthSecs)
        : sourceSampleRate (source.sampleRate),
          length (jmin (int (source.lengthInSamples),
                        int (maxSampleLengthSecs * sourceSampleRate))),
          data (jmin (2, int (source.numChannels)), length + 4)
    {
        if (length == 0)
            throw std::runtime_error ("Unable to load sample");

        source.read (&data, 0, length + 4, 0, true, true);
    }

    double getSampleRate() const                    { return sourceSampleRate; }
    int getLength() const                           { return length; }
    const AudioBuffer<float>& getBuffer() const     { return data; }

private:
    double sourceSampleRate;
    int length;
    AudioBuffer<float> data;
};

//==============================================================================
// A class which contains all the information related to sample-playback, such
// as sample data, loop points, and loop kind.
// It is expected that multiple sampler voices will maintain pointers to a
// single instance of this class, to avoid redundant duplication of sample
// data in memory.
class MPESamplerSound final
{
public:
    void setSample (std::unique_ptr<Sample> value)
    {
        sample = move (value);
        setLoopPointsInSeconds (loopPoints);
    }

    Sample* getSample() const
    {
        return sample.get();
    }

    void setLoopPointsInSeconds (Range<double> value)
    {
        loopPoints = sample == nullptr ? value
                                       : Range<double> (0, sample->getLength() / sample->getSampleRate())
                                                        .constrainRange (value);
    }

    Range<double> getLoopPointsInSeconds() const
    {
        return loopPoints;
    }

    void setCentreFrequencyInHz (double centre)
    {
        centreFrequencyInHz = centre;
    }

    double getCentreFrequencyInHz() const
    {
        return centreFrequencyInHz;
    }

    void setLoopMode (LoopMode type)
    {
        loopMode = type;
    }

    LoopMode getLoopMode() const
    {
        return loopMode;
    }

private:
    std::unique_ptr<Sample> sample;
    double centreFrequencyInHz { 440.0 };
    Range<double> loopPoints;
    LoopMode loopMode { LoopMode::none };
};

//==============================================================================
class MPESamplerVoice  : public MPESynthesiserVoice
{
public:
    explicit MPESamplerVoice (std::shared_ptr<const MPESamplerSound> sound)
        : samplerSound (move (sound))
    {
        jassert (samplerSound != nullptr);
    }

    void noteStarted() override
    {
        jassert (currentlyPlayingNote.isValid());
        jassert (currentlyPlayingNote.keyState == MPENote::keyDown
              || currentlyPlayingNote.keyState == MPENote::keyDownAndSustained);

        level    .setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
        frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());

        auto loopPoints = samplerSound->getLoopPointsInSeconds();
        loopBegin.setTargetValue (loopPoints.getStart() * samplerSound->getSample()->getSampleRate());
        loopEnd  .setTargetValue (loopPoints.getEnd()   * samplerSound->getSample()->getSampleRate());

        for (auto smoothed : { &level, &frequency, &loopBegin, &loopEnd })
            smoothed->reset (currentSampleRate, smoothingLengthInSeconds);

        currentSamplePos = 0.0;
        tailOff          = 0.0;
    }

    void noteStopped (bool allowTailOff) override
    {
        jassert (currentlyPlayingNote.keyState == MPENote::off);

        if (allowTailOff && tailOff == 0.0)
            tailOff = 1.0;
        else
            stopNote();
    }

    void notePressureChanged() override
    {
        level.setTargetValue (currentlyPlayingNote.pressure.asUnsignedFloat());
    }

    void notePitchbendChanged() override
    {
        frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
    }

    void noteTimbreChanged()   override {}
    void noteKeyStateChanged() override {}

    void renderNextBlock (AudioBuffer<float>& outputBuffer,
                          int startSample,
                          int numSamples) override
    {
        jassert (samplerSound->getSample() != nullptr);

        auto loopPoints = samplerSound->getLoopPointsInSeconds();
        loopBegin.setTargetValue (loopPoints.getStart() * samplerSound->getSample()->getSampleRate());
        loopEnd  .setTargetValue (loopPoints.getEnd()   * samplerSound->getSample()->getSampleRate());

        auto& data = samplerSound->getSample()->getBuffer();

        auto inL = data.getReadPointer (0);
        auto inR = data.getNumChannels() > 1 ? data.getReadPointer (1) : nullptr;

        auto outL = outputBuffer.getWritePointer (0, startSample);

        if (outL == nullptr)
            return;

        auto outR = outputBuffer.getNumChannels() > 1 ? outputBuffer.getWritePointer (1, startSample)
                                                      : nullptr;

        size_t writePos = 0;

        while (--numSamples >= 0 && renderNextSample (inL, inR, outL, outR, writePos))
            writePos += 1;
    }

    double getCurrentSamplePosition() const
    {
        return currentSamplePos;
    }

private:
    bool renderNextSample (const float* inL,
                           const float* inR,
                           float* outL,
                           float* outR,
                           size_t writePos)
    {
        auto currentLevel     = level.getNextValue();
        auto currentFrequency = frequency.getNextValue();
        auto currentLoopBegin = loopBegin.getNextValue();
        auto currentLoopEnd   = loopEnd.getNextValue();

        if (isTailingOff())
        {
            currentLevel *= tailOff;
            tailOff *= 0.9999;

            if (tailOff < 0.005)
            {
                stopNote();
                return false;
            }
        }

        auto pos      = (int) currentSamplePos;
        auto nextPos  = pos + 1;
        auto alpha    = (float) (currentSamplePos - pos);
        auto invAlpha = 1.0f - alpha;

        // just using a very simple linear interpolation here..
        auto l = static_cast<float> (currentLevel * (inL[pos] * invAlpha + inL[nextPos] * alpha));
        auto r = static_cast<float> ((inR != nullptr) ? currentLevel * (inR[pos] * invAlpha + inR[nextPos] * alpha)
                                                      : l);

        if (outR != nullptr)
        {
            outL[writePos] += l;
            outR[writePos] += r;
        }
        else
        {
            outL[writePos] += (l + r) * 0.5f;
        }

        std::tie (currentSamplePos, currentDirection) = getNextState (currentFrequency,
                                                                      currentLoopBegin,
                                                                      currentLoopEnd);

        if (currentSamplePos > samplerSound->getSample()->getLength())
        {
            stopNote();
            return false;
        }

        return true;
    }

    double getSampleValue() const;

    bool isTailingOff() const
    {
        return tailOff != 0.0;
    }

    void stopNote()
    {
        clearCurrentNote();
        currentSamplePos = 0.0;
    }

    enum class Direction
    {
        forward,
        backward
    };

    std::tuple<double, Direction> getNextState (double freq,
                                                double begin,
                                                double end) const
    {
        auto nextPitchRatio = freq / samplerSound->getCentreFrequencyInHz();

        auto nextSamplePos = currentSamplePos;
        auto nextDirection = currentDirection;

        // Move the current sample pos in the correct direction
        switch (currentDirection)
        {
            case Direction::forward:
                nextSamplePos += nextPitchRatio;
                break;

            case Direction::backward:
                nextSamplePos -= nextPitchRatio;
                break;
        }

        // Update current sample position, taking loop mode into account
        // If the loop mode was changed while we were travelling backwards, deal
        // with it gracefully.
        if (nextDirection == Direction::backward && nextSamplePos < begin)
        {
            nextSamplePos = begin;
            nextDirection = Direction::forward;

            return { nextSamplePos, nextDirection };
        }

        if (samplerSound->getLoopMode() == LoopMode::none)
            return { nextSamplePos, nextDirection };

        if (nextDirection == Direction::forward && end < nextSamplePos && !isTailingOff())
        {
            if (samplerSound->getLoopMode() == LoopMode::forward)
                nextSamplePos = begin;
            else if (samplerSound->getLoopMode() == LoopMode::pingpong)
            {
                nextSamplePos = end;
                nextDirection = Direction::backward;
            }
        }
        return { nextSamplePos, nextDirection };
    }

    std::shared_ptr<const MPESamplerSound> samplerSound;
    SmoothedValue<double> level { 0 };
    SmoothedValue<double> frequency { 0 };
    SmoothedValue<double> loopBegin;
    SmoothedValue<double> loopEnd;
    double currentSamplePos { 0 };
    double tailOff { 0 };
    Direction currentDirection { Direction::forward };
    double smoothingLengthInSeconds { 0.01 };
};

template <typename Contents>
class ReferenceCountingAdapter  : public ReferenceCountedObject
{
public:
    template <typename... Args>
    explicit ReferenceCountingAdapter (Args&&... args)
        : contents (std::forward<Args> (args)...)
    {}

    const Contents& get() const
    {
        return contents;
    }

    Contents& get()
    {
        return contents;
    }

private:
    Contents contents;
};

template <typename Contents, typename... Args>
std::unique_ptr<ReferenceCountingAdapter<Contents>>
make_reference_counted (Args&&... args)
{
    auto adapter = new ReferenceCountingAdapter<Contents> (std::forward<Args> (args)...);
    return std::unique_ptr<ReferenceCountingAdapter<Contents>> (adapter);
}

//==============================================================================
inline std::unique_ptr<AudioFormatReader> makeAudioFormatReader (AudioFormatManager& manager,
                                                                 const void* sampleData,
                                                                 size_t dataSize)
{
    return std::unique_ptr<AudioFormatReader> (manager.createReaderFor (new MemoryInputStream (sampleData,
                                                                                               dataSize,
                                                                                               false)));
}

inline std::unique_ptr<AudioFormatReader> makeAudioFormatReader (AudioFormatManager& manager,
                                                                 const File& file)
{
    return std::unique_ptr<AudioFormatReader> (manager.createReaderFor (file));
}

//==============================================================================
class AudioFormatReaderFactory
{
public:
    virtual ~AudioFormatReaderFactory() noexcept = default;
    virtual std::unique_ptr<AudioFormatReader> make (AudioFormatManager&) const = 0;
    virtual std::unique_ptr<AudioFormatReaderFactory> clone() const = 0;
};

//==============================================================================
class MemoryAudioFormatReaderFactory  : public AudioFormatReaderFactory
{
public:
    MemoryAudioFormatReaderFactory (const void* sampleData, size_t dataSize)
        : sampleData (sampleData),
          dataSize (dataSize)
    {}

    std::unique_ptr<AudioFormatReader> make (AudioFormatManager&manager ) const override
    {
        return makeAudioFormatReader (manager, sampleData, dataSize);
    }

    std::unique_ptr<AudioFormatReaderFactory> clone() const override
    {
        return std::unique_ptr<AudioFormatReaderFactory> (new MemoryAudioFormatReaderFactory (*this));
    }

private:
    const void* sampleData;
    size_t dataSize;
};

//==============================================================================
class FileAudioFormatReaderFactory  : public AudioFormatReaderFactory
{
public:
    explicit FileAudioFormatReaderFactory (File file)
        : file (std::move (file))
    {}

    std::unique_ptr<AudioFormatReader> make (AudioFormatManager& manager) const override
    {
        return makeAudioFormatReader (manager, file);
    }

    std::unique_ptr<AudioFormatReaderFactory> clone() const override
    {
        return std::unique_ptr<AudioFormatReaderFactory> (new FileAudioFormatReaderFactory (*this));
    }

private:
    File file;
};

namespace juce
{

bool operator== (const MPEZoneLayout& a, const MPEZoneLayout& b)
{
    if (a.getLowerZone() != b.getLowerZone())
        return false;

    if (a.getUpperZone() != b.getUpperZone())
        return false;

    return true;
}

bool operator!= (const MPEZoneLayout& a, const MPEZoneLayout& b)
{
    return ! (a == b);
}

template<>
struct VariantConverter<LoopMode>
{
    static LoopMode fromVar (const var& v)
    {
        return static_cast<LoopMode> (int (v));
    }

    static var toVar (LoopMode loopMode)
    {
        return static_cast<int> (loopMode);
    }
};

template <typename Wrapped>
struct GenericVariantConverter
{
    static Wrapped fromVar (const var& v)
    {
        auto cast = dynamic_cast<ReferenceCountingAdapter<Wrapped>*> (v.getObject());
        jassert (cast != nullptr);
        return cast->get();
    }

    static var toVar (Wrapped range)
    {
        return { make_reference_counted<Wrapped> (std::move (range)).release() };
    }
};

template <typename Numeric>
struct VariantConverter<Range<Numeric>>  : GenericVariantConverter<Range<Numeric>> {};

template<>
struct VariantConverter<MPEZoneLayout>  : GenericVariantConverter<MPEZoneLayout> {};

template<>
struct VariantConverter<std::shared_ptr<AudioFormatReaderFactory>>
    : GenericVariantConverter<std::shared_ptr<AudioFormatReaderFactory>>
{};

} // namespace juce

//==============================================================================
class VisibleRangeDataModel  : public ValueTree::Listener
{
public:
    class Listener
    {
    public:
        virtual ~Listener() noexcept = default;
        virtual void totalRangeChanged   (Range<double>) {}
        virtual void visibleRangeChanged (Range<double>) {}
    };

    VisibleRangeDataModel()
        : VisibleRangeDataModel (ValueTree (IDs::VISIBLE_RANGE))
    {}

    explicit VisibleRangeDataModel (const ValueTree& vt)
        : valueTree (vt),
          totalRange   (valueTree, IDs::totalRange,   nullptr),
          visibleRange (valueTree, IDs::visibleRange, nullptr)
    {
        jassert (valueTree.hasType (IDs::VISIBLE_RANGE));
        valueTree.addListener (this);
    }

    VisibleRangeDataModel (const VisibleRangeDataModel& other)
        : VisibleRangeDataModel (other.valueTree)
    {}

    VisibleRangeDataModel& operator= (const VisibleRangeDataModel& other)
    {
        auto copy (other);
        swap (copy);
        return *this;
    }

    Range<double> getTotalRange() const
    {
        return totalRange;
    }

    void setTotalRange (Range<double> value, UndoManager* undoManager)
    {
        totalRange.setValue (value, undoManager);
        setVisibleRange (visibleRange, undoManager);
    }

    Range<double> getVisibleRange() const
    {
        return visibleRange;
    }

    void setVisibleRange (Range<double> value, UndoManager* undoManager)
    {
        visibleRange.setValue (totalRange.get().constrainRange (value), undoManager);
    }

    void addListener (Listener& listener)
    {
        listenerList.add (&listener);
    }

    void removeListener (Listener& listener)
    {
        listenerList.remove (&listener);
    }

    void swap (VisibleRangeDataModel& other) noexcept
    {
        using std::swap;
        swap (other.valueTree, valueTree);
    }

private:
    void valueTreePropertyChanged (ValueTree&, const Identifier& property) override
    {
        if (property == IDs::totalRange)
        {
            totalRange.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.totalRangeChanged (totalRange); });
        }
        else if (property == IDs::visibleRange)
        {
            visibleRange.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.visibleRangeChanged (visibleRange); });
        }
    }

    void valueTreeChildAdded        (ValueTree&, ValueTree&)      override { jassertfalse; }
    void valueTreeChildRemoved      (ValueTree&, ValueTree&, int) override { jassertfalse; }
    void valueTreeChildOrderChanged (ValueTree&, int, int)        override { jassertfalse; }
    void valueTreeParentChanged     (ValueTree&)                  override { jassertfalse; }

    ValueTree valueTree;

    CachedValue<Range<double>> totalRange;
    CachedValue<Range<double>> visibleRange;

    ListenerList<Listener> listenerList;
};

//==============================================================================
class MPESettingsDataModel  : public ValueTree::Listener
{
public:
    class Listener
    {
    public:
        virtual ~Listener() noexcept = default;
        virtual void synthVoicesChanged (int) {}
        virtual void voiceStealingEnabledChanged (bool) {}
        virtual void legacyModeEnabledChanged (bool) {}
        virtual void mpeZoneLayoutChanged (const MPEZoneLayout&) {}
        virtual void legacyFirstChannelChanged (int) {}
        virtual void legacyLastChannelChanged (int) {}
        virtual void legacyPitchbendRangeChanged (int) {}
    };

    MPESettingsDataModel()
        : MPESettingsDataModel (ValueTree (IDs::MPE_SETTINGS))
    {}

    explicit MPESettingsDataModel (const ValueTree& vt)
        : valueTree (vt),
          synthVoices          (valueTree, IDs::synthVoices,          nullptr, 15),
          voiceStealingEnabled (valueTree, IDs::voiceStealingEnabled, nullptr, false),
          legacyModeEnabled    (valueTree, IDs::legacyModeEnabled,    nullptr, true),
          mpeZoneLayout        (valueTree, IDs::mpeZoneLayout,        nullptr, {}),
          legacyFirstChannel   (valueTree, IDs::legacyFirstChannel,   nullptr, 1),
          legacyLastChannel    (valueTree, IDs::legacyLastChannel,    nullptr, 15),
          legacyPitchbendRange (valueTree, IDs::legacyPitchbendRange, nullptr, 48)
    {
        jassert (valueTree.hasType (IDs::MPE_SETTINGS));
        valueTree.addListener (this);
    }

    MPESettingsDataModel (const MPESettingsDataModel& other)
        : MPESettingsDataModel (other.valueTree)
    {}

    MPESettingsDataModel& operator= (const MPESettingsDataModel& other)
    {
        auto copy (other);
        swap (copy);
        return *this;
    }

    int getSynthVoices() const
    {
        return synthVoices;
    }

    void setSynthVoices (int value, UndoManager* undoManager)
    {
        synthVoices.setValue (Range<int> (1, 20).clipValue (value), undoManager);
    }

    bool getVoiceStealingEnabled() const
    {
        return voiceStealingEnabled;
    }

    void setVoiceStealingEnabled (bool value, UndoManager* undoManager)
    {
        voiceStealingEnabled.setValue (value, undoManager);
    }

    bool getLegacyModeEnabled() const
    {
        return legacyModeEnabled;
    }

    void setLegacyModeEnabled (bool value, UndoManager* undoManager)
    {
        legacyModeEnabled.setValue (value, undoManager);
    }

    MPEZoneLayout getMPEZoneLayout() const
    {
        return mpeZoneLayout;
    }

    void setMPEZoneLayout (MPEZoneLayout value, UndoManager* undoManager)
    {
        mpeZoneLayout.setValue (value, undoManager);
    }

    int getLegacyFirstChannel() const
    {
        return legacyFirstChannel;
    }

    void setLegacyFirstChannel (int value, UndoManager* undoManager)
    {
        legacyFirstChannel.setValue (Range<int> (1, legacyLastChannel).clipValue (value), undoManager);
    }

    int getLegacyLastChannel() const
    {
        return legacyLastChannel;
    }

    void setLegacyLastChannel (int value, UndoManager* undoManager)
    {
        legacyLastChannel.setValue (Range<int> (legacyFirstChannel, 15).clipValue (value), undoManager);
    }

    int getLegacyPitchbendRange() const
    {
        return legacyPitchbendRange;
    }

    void setLegacyPitchbendRange (int value, UndoManager* undoManager)
    {
        legacyPitchbendRange.setValue (Range<int> (0, 95).clipValue (value), undoManager);
    }

    void addListener (Listener& listener)
    {
        listenerList.add (&listener);
    }

    void removeListener (Listener& listener)
    {
        listenerList.remove (&listener);
    }

    void swap (MPESettingsDataModel& other) noexcept
    {
        using std::swap;
        swap (other.valueTree, valueTree);
    }

private:
    void valueTreePropertyChanged (ValueTree&, const Identifier& property) override
    {
        if (property == IDs::synthVoices)
        {
            synthVoices.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.synthVoicesChanged (synthVoices); });
        }
        else if (property == IDs::voiceStealingEnabled)
        {
            voiceStealingEnabled.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.voiceStealingEnabledChanged (voiceStealingEnabled); });
        }
        else if (property == IDs::legacyModeEnabled)
        {
            legacyModeEnabled.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.legacyModeEnabledChanged (legacyModeEnabled); });
        }
        else if (property == IDs::mpeZoneLayout)
        {
            mpeZoneLayout.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.mpeZoneLayoutChanged (mpeZoneLayout); });
        }
        else if (property == IDs::legacyFirstChannel)
        {
            legacyFirstChannel.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.legacyFirstChannelChanged (legacyFirstChannel); });
        }
        else if (property == IDs::legacyLastChannel)
        {
            legacyLastChannel.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.legacyLastChannelChanged (legacyLastChannel); });
        }
        else if (property == IDs::legacyPitchbendRange)
        {
            legacyPitchbendRange.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.legacyPitchbendRangeChanged (legacyPitchbendRange); });
        }
    }

    void valueTreeChildAdded        (ValueTree&, ValueTree&)      override { jassertfalse; }
    void valueTreeChildRemoved      (ValueTree&, ValueTree&, int) override { jassertfalse; }
    void valueTreeChildOrderChanged (ValueTree&, int, int)        override { jassertfalse; }
    void valueTreeParentChanged     (ValueTree&)                  override { jassertfalse; }

    ValueTree valueTree;

    CachedValue<int> synthVoices;
    CachedValue<bool> voiceStealingEnabled;
    CachedValue<bool> legacyModeEnabled;
    CachedValue<MPEZoneLayout> mpeZoneLayout;
    CachedValue<int> legacyFirstChannel;
    CachedValue<int> legacyLastChannel;
    CachedValue<int> legacyPitchbendRange;

    ListenerList<Listener> listenerList;
};

//==============================================================================
class DataModel  : public ValueTree::Listener
{
public:
    class Listener
    {
    public:
        virtual ~Listener() noexcept = default;
        virtual void sampleReaderChanged (std::shared_ptr<AudioFormatReaderFactory>) {}
        virtual void centreFrequencyHzChanged (double) {}
        virtual void loopModeChanged (LoopMode) {}
        virtual void loopPointsSecondsChanged (Range<double>) {}
    };

    explicit DataModel (AudioFormatManager& audioFormatManager)
        : DataModel (audioFormatManager, ValueTree (IDs::DATA_MODEL))
    {}

    DataModel (AudioFormatManager& audioFormatManager, const ValueTree& vt)
        : audioFormatManager (&audioFormatManager),
          valueTree (vt),
          sampleReader      (valueTree, IDs::sampleReader,      nullptr),
          centreFrequencyHz (valueTree, IDs::centreFrequencyHz, nullptr),
          loopMode          (valueTree, IDs::loopMode,          nullptr, LoopMode::none),
          loopPointsSeconds (valueTree, IDs::loopPointsSeconds, nullptr)
    {
        jassert (valueTree.hasType (IDs::DATA_MODEL));
        valueTree.addListener (this);
    }

    DataModel (const DataModel& other)
        : DataModel (*other.audioFormatManager, other.valueTree)
    {}

    DataModel& operator= (const DataModel& other)
    {
        auto copy (other);
        swap (copy);
        return *this;
    }

    std::unique_ptr<AudioFormatReader> getSampleReader() const
    {
        return sampleReader != nullptr ? sampleReader.get()->make (*audioFormatManager) : nullptr;
    }

    void setSampleReader (std::unique_ptr<AudioFormatReaderFactory> readerFactory,
                          UndoManager* undoManager)
    {
        sampleReader.setValue (move (readerFactory), undoManager);
        setLoopPointsSeconds (Range<double> (0, getSampleLengthSeconds()).constrainRange (loopPointsSeconds),
                              undoManager);
    }

    double getSampleLengthSeconds() const
    {
        if (auto r = getSampleReader())
            return r->lengthInSamples / r->sampleRate;

        return 1.0;
    }

    double getCentreFrequencyHz() const
    {
        return centreFrequencyHz;
    }

    void setCentreFrequencyHz (double value, UndoManager* undoManager)
    {
        centreFrequencyHz.setValue (Range<double> (20, 20000).clipValue (value),
                                    undoManager);
    }

    LoopMode getLoopMode() const
    {
        return loopMode;
    }

    void setLoopMode (LoopMode value, UndoManager* undoManager)
    {
        loopMode.setValue (value, undoManager);
    }

    Range<double> getLoopPointsSeconds() const
    {
        return loopPointsSeconds;
    }

    void setLoopPointsSeconds (Range<double> value, UndoManager* undoManager)
    {
        loopPointsSeconds.setValue (Range<double> (0, getSampleLengthSeconds()).constrainRange (value),
                                    undoManager);
    }

    MPESettingsDataModel mpeSettings()
    {
        return MPESettingsDataModel (valueTree.getOrCreateChildWithName (IDs::MPE_SETTINGS, nullptr));
    }

    void addListener (Listener& listener)
    {
        listenerList.add (&listener);
    }

    void removeListener (Listener& listener)
    {
        listenerList.remove (&listener);
    }

    void swap (DataModel& other) noexcept
    {
        using std::swap;
        swap (other.valueTree, valueTree);
    }

    AudioFormatManager& getAudioFormatManager() const
    {
        return *audioFormatManager;
    }

private:
    void valueTreePropertyChanged (ValueTree&, const Identifier& property) override
    {
        if (property == IDs::sampleReader)
        {
            sampleReader.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.sampleReaderChanged (sampleReader); });
        }
        else if (property == IDs::centreFrequencyHz)
        {
            centreFrequencyHz.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.centreFrequencyHzChanged (centreFrequencyHz); });
        }
        else if (property == IDs::loopMode)
        {
            loopMode.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.loopModeChanged (loopMode); });
        }
        else if (property == IDs::loopPointsSeconds)
        {
            loopPointsSeconds.forceUpdateOfCachedValue();
            listenerList.call ([this] (Listener& l) { l.loopPointsSecondsChanged (loopPointsSeconds); });
        }
    }

    void valueTreeChildAdded        (ValueTree&, ValueTree&)      override {}
    void valueTreeChildRemoved      (ValueTree&, ValueTree&, int) override { jassertfalse; }
    void valueTreeChildOrderChanged (ValueTree&, int, int)        override { jassertfalse; }
    void valueTreeParentChanged     (ValueTree&)                  override { jassertfalse; }

    AudioFormatManager* audioFormatManager;

    ValueTree valueTree;

    CachedValue<std::shared_ptr<AudioFormatReaderFactory>> sampleReader;
    CachedValue<double> centreFrequencyHz;
    CachedValue<LoopMode> loopMode;
    CachedValue<Range<double>> loopPointsSeconds;

    ListenerList<Listener> listenerList;
};

namespace
{
void initialiseComboBoxWithConsecutiveIntegers (Component& owner,
                                                ComboBox& comboBox,
                                                Label& label,
                                                int firstValue,
                                                int numValues,
                                                int valueToSelect)
{
    for (auto i = 0; i < numValues; ++i)
        comboBox.addItem (String (i + firstValue), i + 1);

    comboBox.setSelectedId (valueToSelect - firstValue + 1);

    label.attachToComponent (&comboBox, true);
    owner.addAndMakeVisible (comboBox);
}

constexpr int controlHeight     = 24;
constexpr int controlSeparation = 6;

} // namespace

//==============================================================================
class MPELegacySettingsComponent final  : public Component,
                                          public MPESettingsDataModel::Listener
{
public:
    explicit MPELegacySettingsComponent (const MPESettingsDataModel& model,
                                         UndoManager& um)
        : dataModel (model),
          undoManager (&um)
    {
        dataModel.addListener (*this);

        initialiseComboBoxWithConsecutiveIntegers (*this, legacyStartChannel, legacyStartChannelLabel, 1, 16, 1);
        initialiseComboBoxWithConsecutiveIntegers (*this, legacyEndChannel, legacyEndChannelLabel, 1, 16, 16);
        initialiseComboBoxWithConsecutiveIntegers (*this, legacyPitchbendRange, legacyPitchbendRangeLabel, 0, 96, 2);

        legacyStartChannel.onChange = [this]
        {
            if (isLegacyModeValid())
            {
                undoManager->beginNewTransaction();
                dataModel.setLegacyFirstChannel (getFirstChannel(), undoManager);
            }
        };

        legacyEndChannel.onChange = [this]
        {
            if (isLegacyModeValid())
            {
                undoManager->beginNewTransaction();
                dataModel.setLegacyLastChannel (getLastChannel(), undoManager);
            }
        };

        legacyPitchbendRange.onChange = [this]
        {
            if (isLegacyModeValid())
            {
                undoManager->beginNewTransaction();
                dataModel.setLegacyPitchbendRange (legacyPitchbendRange.getText().getIntValue(), undoManager);
            }
        };
    }

    int getMinHeight() const
    {
        return (controlHeight * 3) + (controlSeparation * 2);
    }

private:
    void resized() override
    {
        Rectangle<int> r (proportionOfWidth (0.65f), 0, proportionOfWidth (0.25f), getHeight());

        for (auto& comboBox : { &legacyStartChannel, &legacyEndChannel, &legacyPitchbendRange })
        {
            comboBox->setBounds (r.removeFromTop (controlHeight));
            r.removeFromTop (controlSeparation);
        }
    }

    bool isLegacyModeValid() const
    {
        if (! areLegacyModeParametersValid())
        {
            handleInvalidLegacyModeParameters();
            return false;
        }

        return true;
    }

    void legacyFirstChannelChanged (int value) override
    {
        legacyStartChannel.setSelectedId (value, dontSendNotification);
    }

    void legacyLastChannelChanged (int value) override
    {
        legacyEndChannel.setSelectedId (value, dontSendNotification);
    }

    void legacyPitchbendRangeChanged (int value) override
    {
        legacyPitchbendRange.setSelectedId (value + 1, dontSendNotification);
    }

    int getFirstChannel() const
    {
        return legacyStartChannel.getText().getIntValue();
    }

    int getLastChannel() const
    {
        return legacyEndChannel.getText().getIntValue();
    }

    bool areLegacyModeParametersValid() const
    {
        return getFirstChannel() <= getLastChannel();
    }

    void handleInvalidLegacyModeParameters() const
    {
        AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon,
                                          "Invalid legacy mode channel layout",
                                          "Cannot set legacy mode start/end channel:\n"
                                          "The end channel must not be less than the start channel!",
                                          "Got it");
    }

    MPESettingsDataModel dataModel;

    ComboBox legacyStartChannel, legacyEndChannel, legacyPitchbendRange;

    Label legacyStartChannelLabel   { {}, "First channel" },
          legacyEndChannelLabel     { {}, "Last channel" },
          legacyPitchbendRangeLabel { {}, "Pitchbend range (semitones)" };

    UndoManager* undoManager;
};

//==============================================================================
class MPENewSettingsComponent final  : public Component,
                                       public MPESettingsDataModel::Listener
{
public:
    MPENewSettingsComponent (const MPESettingsDataModel& model,
                             UndoManager& um)
        : dataModel (model),
          undoManager (&um)
    {
        dataModel.addListener (*this);

        addAndMakeVisible (isLowerZoneButton);
        isLowerZoneButton.setToggleState (true, NotificationType::dontSendNotification);

        initialiseComboBoxWithConsecutiveIntegers (*this, memberChannels, memberChannelsLabel, 0, 16, 15);
        initialiseComboBoxWithConsecutiveIntegers (*this, masterPitchbendRange, masterPitchbendRangeLabel, 0, 96, 2);
        initialiseComboBoxWithConsecutiveIntegers (*this, notePitchbendRange, notePitchbendRangeLabel, 0, 96, 48);

        for (auto& button : { &setZoneButton, &clearAllZonesButton })
            addAndMakeVisible (button);

        setZoneButton.onClick = [this]
        {
            auto isLowerZone = isLowerZoneButton.getToggleState();
            auto numMemberChannels = memberChannels.getText().getIntValue();
            auto perNotePb = notePitchbendRange.getText().getIntValue();
            auto masterPb = masterPitchbendRange.getText().getIntValue();

            if (isLowerZone)
                zoneLayout.setLowerZone (numMemberChannels, perNotePb, masterPb);
            else
                zoneLayout.setUpperZone (numMemberChannels, perNotePb, masterPb);

            undoManager->beginNewTransaction();
            dataModel.setMPEZoneLayout (zoneLayout, undoManager);
        };

        clearAllZonesButton.onClick = [this]
        {
            zoneLayout.clearAllZones();
            undoManager->beginNewTransaction();
            dataModel.setMPEZoneLayout (zoneLayout, undoManager);
        };
    }

    int getMinHeight() const
    {
        return (controlHeight * 6) + (controlSeparation * 6);
    }

private:
    void resized() override
    {
        Rectangle<int> r (proportionOfWidth (0.65f), 0, proportionOfWidth (0.25f), getHeight());

        isLowerZoneButton.setBounds (r.removeFromTop (controlHeight));
        r.removeFromTop (controlSeparation);

        for (auto& comboBox : { &memberChannels, &masterPitchbendRange, &notePitchbendRange })
        {
            comboBox->setBounds (r.removeFromTop (controlHeight));
            r.removeFromTop (controlSeparation);
        }

        r.removeFromTop (controlSeparation);

        auto buttonLeft = proportionOfWidth (0.5f);

        setZoneButton.setBounds (r.removeFromTop (controlHeight).withLeft (buttonLeft));
        r.removeFromTop (controlSeparation);
        clearAllZonesButton.setBounds (r.removeFromTop (controlHeight).withLeft (buttonLeft));
    }

    void mpeZoneLayoutChanged (const MPEZoneLayout& value) override
    {
        zoneLayout = value;
    }

    MPESettingsDataModel dataModel;
    MPEZoneLayout zoneLayout;

    ComboBox memberChannels, masterPitchbendRange, notePitchbendRange;

    ToggleButton isLowerZoneButton  { "Lower zone" };

    Label memberChannelsLabel       { {}, "Nr. of member channels" },
          masterPitchbendRangeLabel { {}, "Master pitchbend range (semitones)" },
          notePitchbendRangeLabel   { {}, "Note pitchbend range (semitones)" };

    TextButton setZoneButton       { "Set zone" },
               clearAllZonesButton { "Clear all zones" };

    UndoManager* undoManager;
};

//==============================================================================
class MPESettingsComponent final  : public Component,
                                    public MPESettingsDataModel::Listener
{
public:
    MPESettingsComponent (const MPESettingsDataModel& model,
                          UndoManager& um)
        : dataModel (model),
          legacySettings (dataModel, um),
          newSettings (dataModel, um),
          undoManager (&um)
    {
        dataModel.addListener (*this);

        addAndMakeVisible (newSettings);
        addChildComponent (legacySettings);

        initialiseComboBoxWithConsecutiveIntegers (*this, numberOfVoices, numberOfVoicesLabel, 1, 20, 15);
        numberOfVoices.onChange = [this]
        {
            undoManager->beginNewTransaction();
            dataModel.setSynthVoices (numberOfVoices.getText().getIntValue(), undoManager);
        };

        for (auto& button : { &legacyModeEnabledToggle, &voiceStealingEnabledToggle })
        {
            addAndMakeVisible (button);
        }

        legacyModeEnabledToggle.onClick = [this]
        {
            undoManager->beginNewTransaction();
            dataModel.setLegacyModeEnabled (legacyModeEnabledToggle.getToggleState(), undoManager);
        };

        voiceStealingEnabledToggle.onClick = [this]
        {
            undoManager->beginNewTransaction();
            dataModel.setVoiceStealingEnabled (voiceStealingEnabledToggle.getToggleState(), undoManager);
        };
    }

private:
    void resized() override
    {
        auto topHeight = jmax (legacySettings.getMinHeight(), newSettings.getMinHeight());
        auto r = getLocalBounds();
        r.removeFromTop (15);
        auto top = r.removeFromTop (topHeight);
        legacySettings.setBounds (top);
        newSettings.setBounds (top);

        r.removeFromLeft (proportionOfWidth (0.65f));
        r = r.removeFromLeft (proportionOfWidth (0.25f));

        auto toggleLeft = proportionOfWidth (0.25f);

        legacyModeEnabledToggle.setBounds (r.removeFromTop (controlHeight).withLeft (toggleLeft));
        r.removeFromTop (controlSeparation);
        voiceStealingEnabledToggle.setBounds (r.removeFromTop (controlHeight).withLeft (toggleLeft));
        r.removeFromTop (controlSeparation);
        numberOfVoices.setBounds (r.removeFromTop (controlHeight));
    }

    void legacyModeEnabledChanged (bool value) override
    {
        legacySettings.setVisible (value);
        newSettings.setVisible (! value);
        legacyModeEnabledToggle.setToggleState (value, dontSendNotification);
    }

    void voiceStealingEnabledChanged (bool value) override
    {
        voiceStealingEnabledToggle.setToggleState (value, dontSendNotification);
    }

    void synthVoicesChanged (int value) override
    {
        numberOfVoices.setSelectedId (value, dontSendNotification);
    }

    MPESettingsDataModel dataModel;
    MPELegacySettingsComponent legacySettings;
    MPENewSettingsComponent newSettings;

    ToggleButton legacyModeEnabledToggle    { "Enable Legacy Mode" },
                 voiceStealingEnabledToggle { "Enable synth voice stealing" };

    ComboBox numberOfVoices;
    Label numberOfVoicesLabel { {}, "Number of synth voices" };

    UndoManager* undoManager;
};

//==============================================================================
class LoopPointMarker  : public Component
{
public:
    using MouseCallback = std::function<void (LoopPointMarker&, const MouseEvent&)>;

    LoopPointMarker (String marker,
                     MouseCallback onMouseDown,
                     MouseCallback onMouseDrag,
                     MouseCallback onMouseUp)
        : text (std::move (marker)),
          onMouseDown (move (onMouseDown)),
          onMouseDrag (move (onMouseDrag)),
          onMouseUp (move (onMouseUp))
    {
        setMouseCursor (MouseCursor::LeftRightResizeCursor);
    }

private:
    void resized() override
    {
        auto height = 20;
        auto triHeight = 6;

        auto bounds = getLocalBounds();
        Path newPath;
        newPath.addRectangle (bounds.removeFromBottom (height));

        newPath.startNewSubPath (bounds.getBottomLeft().toFloat());
        newPath.lineTo (bounds.getBottomRight().toFloat());
        Point<float> apex (static_cast<float> (bounds.getX() + (bounds.getWidth() / 2)),
                           static_cast<float> (bounds.getBottom() - triHeight));
        newPath.lineTo (apex);
        newPath.closeSubPath();

        newPath.addLineSegment (Line<float> (apex, Point<float> (apex.getX(), 0)), 1);

        path = newPath;
    }

    void paint (Graphics& g) override
    {
        g.setColour (Colours::deepskyblue);
        g.fillPath (path);

        auto height = 20;
        g.setColour (Colours::white);
        g.drawText (text, getLocalBounds().removeFromBottom (height), Justification::centred);
    }

    bool hitTest (int x, int y) override
    {
        return path.contains ((float) x, (float) y);
    }

    void mouseDown (const MouseEvent& e) override
    {
        onMouseDown (*this, e);
    }

    void mouseDrag (const MouseEvent& e) override
    {
        onMouseDrag (*this, e);
    }

    void mouseUp (const MouseEvent& e) override
    {
        onMouseUp (*this, e);
    }

    String text;
    Path path;
    MouseCallback onMouseDown;
    MouseCallback onMouseDrag;
    MouseCallback onMouseUp;
};

//==============================================================================
class Ruler  : public Component,
               public VisibleRangeDataModel::Listener
{
public:
    explicit Ruler (const VisibleRangeDataModel& model)
        : visibleRange (model)
    {
        visibleRange.addListener (*this);
        setMouseCursor (MouseCursor::LeftRightResizeCursor);
    }

private:
    void paint (Graphics& g) override
    {
        auto minDivisionWidth = 50.0f;
        auto maxDivisions     = getWidth() / minDivisionWidth;

        auto lookFeel = dynamic_cast<LookAndFeel_V4*> (&getLookAndFeel());
        auto bg = lookFeel->getCurrentColourScheme()
                           .getUIColour (LookAndFeel_V4::ColourScheme::UIColour::widgetBackground);

        g.setGradientFill (ColourGradient (bg.brighter(),
                                           0,
                                           0,
                                           bg.darker(),
                                           0,
                                           (float) getHeight(),
                                           false));

        g.fillAll();
        g.setColour (bg.brighter());
        g.drawHorizontalLine (0, 0.0f, (float) getWidth());
        g.setColour (bg.darker());
        g.drawHorizontalLine (1, 0.0f, (float) getWidth());
        g.setColour (Colours::lightgrey);

        auto minLog = std::ceil (std::log10 (visibleRange.getVisibleRange().getLength() / maxDivisions));
        auto precision = 2 + std::abs (minLog);
        auto divisionMagnitude = std::pow (10, minLog);
        auto startingDivision = std::ceil (visibleRange.getVisibleRange().getStart() / divisionMagnitude);

        for (auto div = startingDivision; div * divisionMagnitude < visibleRange.getVisibleRange().getEnd(); ++div)
        {
            auto time = div * divisionMagnitude;
            auto xPos = (time - visibleRange.getVisibleRange().getStart()) * getWidth()
                              / visibleRange.getVisibleRange().getLength();

            std::ostringstream out_stream;
            out_stream << std::setprecision (roundToInt (precision)) << roundToInt (time);

            g.drawText (out_stream.str(),
                        Rectangle<int> (Point<int> (roundToInt (xPos) + 3, 0),
                                        Point<int> (roundToInt (xPos + minDivisionWidth), getHeight())),
                        Justification::centredLeft,
                        false);

            g.drawVerticalLine (roundToInt (xPos), 2.0f, (float) getHeight());
        }
    }

    void mouseDown (const MouseEvent& e) override
    {
        visibleRangeOnMouseDown = visibleRange.getVisibleRange();
        timeOnMouseDown = visibleRange.getVisibleRange().getStart()
                       + (visibleRange.getVisibleRange().getLength() * e.getMouseDownX()) / getWidth();
    }

    void mouseDrag (const MouseEvent& e) override
    {
        // Work out the scale of the new range
        auto unitDistance = 100.0f;
        auto scaleFactor  = 1.0 / std::pow (2, e.getDistanceFromDragStartY() / unitDistance);

        // Now position it so that the mouse continues to point at the same
        // place on the ruler.
        auto visibleLength = std::max (0.12, visibleRangeOnMouseDown.getLength() * scaleFactor);
        auto rangeBegin = timeOnMouseDown - visibleLength * e.x / getWidth();
        const Range<double> range (rangeBegin, rangeBegin + visibleLength);
        visibleRange.setVisibleRange (range, nullptr);
    }

    void visibleRangeChanged (Range<double>) override
    {
        repaint();
    }

    VisibleRangeDataModel visibleRange;
    Range<double> visibleRangeOnMouseDown;
    double timeOnMouseDown;
};

//==============================================================================
class LoopPointsOverlay  : public Component,
                           public DataModel::Listener,
                           public VisibleRangeDataModel::Listener
{
public:
    LoopPointsOverlay (const DataModel& dModel,
                       const VisibleRangeDataModel& vModel,
                       UndoManager& undoManager)
        : dataModel (dModel),
          visibleRange (vModel),
          beginMarker ("B",
                       [this] (LoopPointMarker& m, const MouseEvent& e) { this->loopPointMouseDown (m, e); },
                       [this] (LoopPointMarker& m, const MouseEvent& e) { this->loopPointDragged   (m, e); },
                       [this] (LoopPointMarker& m, const MouseEvent& e) { this->loopPointMouseUp   (m, e); }),
          endMarker   ("E",
                       [this] (LoopPointMarker& m, const MouseEvent& e) { this->loopPointMouseDown (m, e); },
                       [this] (LoopPointMarker& m, const MouseEvent& e) { this->loopPointDragged   (m, e); },
                       [this] (LoopPointMarker& m, const MouseEvent& e) { this->loopPointMouseUp   (m, e); }),
          undoManager (&undoManager)
    {
        dataModel   .addListener (*this);
        visibleRange.addListener (*this);

        for (auto ptr : { &beginMarker, &endMarker })
            addAndMakeVisible (ptr);
    }

private:
    void resized() override
    {
        positionLoopPointMarkers();
    }

    void loopPointMouseDown (LoopPointMarker&, const MouseEvent&)
    {
        loopPointsOnMouseDown = dataModel.getLoopPointsSeconds();
        undoManager->beginNewTransaction();
    }

    void loopPointDragged (LoopPointMarker& marker, const MouseEvent& e)
    {
        auto x = xPositionToTime (e.getEventRelativeTo (this).position.x);
        const Range<double> newLoopRange (&marker == &beginMarker ? x : loopPointsOnMouseDown.getStart(),
                                          &marker == &endMarker   ? x : loopPointsOnMouseDown.getEnd());

        dataModel.setLoopPointsSeconds (newLoopRange, undoManager);
    }

    void loopPointMouseUp (LoopPointMarker& marker, const MouseEvent& e)
    {
        auto x = xPositionToTime (e.getEventRelativeTo (this).position.x);
        const Range<double> newLoopRange (&marker == &beginMarker ? x : loopPointsOnMouseDown.getStart(),
                                          &marker == &endMarker   ? x : loopPointsOnMouseDown.getEnd());

        dataModel.setLoopPointsSeconds (newLoopRange, undoManager);
    }

    void loopPointsSecondsChanged (Range<double>) override
    {
        positionLoopPointMarkers();
    }

    void visibleRangeChanged (Range<double>) override
    {
        positionLoopPointMarkers();
    }

    double timeToXPosition (double time) const
    {
        return (time - visibleRange.getVisibleRange().getStart()) * getWidth()
                     / visibleRange.getVisibleRange().getLength();
    }

    double xPositionToTime (double xPosition) const
    {
        return ((xPosition * visibleRange.getVisibleRange().getLength()) / getWidth())
                           + visibleRange.getVisibleRange().getStart();
    }

    void positionLoopPointMarkers()
    {
        auto halfMarkerWidth = 7;

        for (auto tup : { std::make_tuple (&beginMarker, dataModel.getLoopPointsSeconds().getStart()),
                          std::make_tuple (&endMarker,   dataModel.getLoopPointsSeconds().getEnd()) })
        {
            auto ptr  = std::get<0> (tup);
            auto time = std::get<1> (tup);
            ptr->setSize (halfMarkerWidth * 2, getHeight());
            ptr->setTopLeftPosition (roundToInt (timeToXPosition (time) - halfMarkerWidth), 0);
        }
    }

    DataModel dataModel;
    VisibleRangeDataModel visibleRange;
    Range<double> loopPointsOnMouseDown;
    LoopPointMarker beginMarker, endMarker;
    UndoManager* undoManager;
};

//==============================================================================
class PlaybackPositionOverlay  : public Component,
                                 public Timer,
                                 public VisibleRangeDataModel::Listener
{
public:
    using Provider = std::function<std::vector<float>()>;
    PlaybackPositionOverlay (const VisibleRangeDataModel& model,
                             Provider provider)
        : visibleRange (model),
          provider (move (provider))
    {
        visibleRange.addListener (*this);
        startTimer (16);
    }

private:
    void paint (Graphics& g) override
    {
        g.setColour (Colours::red);

        for (auto position : provider())
        {
            g.drawVerticalLine (roundToInt (timeToXPosition (position)), 0.0f, (float) getHeight());
        }
    }

    void timerCallback() override
    {
        repaint();
    }

    void visibleRangeChanged (Range<double>) override
    {
        repaint();
    }

    double timeToXPosition (double time) const
    {
        return (time - visibleRange.getVisibleRange().getStart()) * getWidth()
                     / visibleRange.getVisibleRange().getLength();
    }

    VisibleRangeDataModel visibleRange;
    Provider provider;
};

//==============================================================================
class WaveformView  : public Component,
                      public ChangeListener,
                      public DataModel::Listener,
                      public VisibleRangeDataModel::Listener
{
public:
    WaveformView (const DataModel& model,
                  const VisibleRangeDataModel& vr)
        : dataModel (model),
          visibleRange (vr),
          thumbnailCache (4),
          thumbnail (4, dataModel.getAudioFormatManager(), thumbnailCache)
    {
        dataModel   .addListener (*this);
        visibleRange.addListener (*this);
        thumbnail   .addChangeListener (this);
    }

private:
    void paint (Graphics& g) override
    {
        // Draw the waveforms
        g.fillAll (Colours::black);
        auto numChannels = thumbnail.getNumChannels();

        if (numChannels == 0)
        {
            g.setColour (Colours::white);
            g.drawFittedText ("No File Loaded", getLocalBounds(), Justification::centred, 1);
            return;
        }

        auto bounds = getLocalBounds();
        auto channelHeight = bounds.getHeight() / numChannels;

        for (auto i = 0; i != numChannels; ++i)
        {
            drawChannel (g, i, bounds.removeFromTop (channelHeight));
        }
    }

    void changeListenerCallback (ChangeBroadcaster* source) override
    {
        if (source == &thumbnail)
            repaint();
    }

    void sampleReaderChanged (std::shared_ptr<AudioFormatReaderFactory> value) override
    {
        if (value == nullptr)
            thumbnail.clear();
        else
        {
            auto reader = value->make (dataModel.getAudioFormatManager());
            thumbnail.setReader (reader.release(), currentHashCode);
            currentHashCode += 1;
        }
    }

    void visibleRangeChanged (Range<double>) override
    {
        repaint();
    }

    void drawChannel (Graphics& g, int channel, Rectangle<int> bounds)
    {
        g.setGradientFill (ColourGradient (Colours::lightblue,
                                           bounds.getTopLeft().toFloat(),
                                           Colours::darkgrey,
                                           bounds.getBottomLeft().toFloat(),
                                           false));
        thumbnail.drawChannel (g,
                               bounds,
                               visibleRange.getVisibleRange().getStart(),
                               visibleRange.getVisibleRange().getEnd(),
                               channel,
                               1.0f);
    }

    DataModel dataModel;
    VisibleRangeDataModel visibleRange;
    AudioThumbnailCache thumbnailCache;
    AudioThumbnail thumbnail;
    int64 currentHashCode = 0;
};

//==============================================================================
class WaveformEditor  : public Component,
                        public DataModel::Listener
{
public:
    WaveformEditor (const DataModel& model,
                    PlaybackPositionOverlay::Provider provider,
                    UndoManager& undoManager)
        : dataModel (model),
          waveformView (model, visibleRange),
          playbackOverlay (visibleRange, move (provider)),
          loopPoints (dataModel, visibleRange, undoManager),
          ruler (visibleRange)
    {
        dataModel.addListener (*this);

        addAndMakeVisible (waveformView);
        addAndMakeVisible (playbackOverlay);
        addChildComponent (loopPoints);
        loopPoints.setAlwaysOnTop (true);

        waveformView.toBack();

        addAndMakeVisible (ruler);
    }

private:
    void resized() override
    {
        auto bounds = getLocalBounds();
        ruler          .setBounds (bounds.removeFromTop (25));
        waveformView   .setBounds (bounds);
        playbackOverlay.setBounds (bounds);
        loopPoints     .setBounds (bounds);
    }

    void loopModeChanged (LoopMode value) override
    {
        loopPoints.setVisible (value != LoopMode::none);
    }

    void sampleReaderChanged (std::shared_ptr<AudioFormatReaderFactory> value) override
    {
        auto lengthInSeconds = dataModel.getSampleLengthSeconds();
        visibleRange.setTotalRange   (Range<double> (0, lengthInSeconds), nullptr);
        visibleRange.setVisibleRange (Range<double> (0, lengthInSeconds), nullptr);
    }

    DataModel dataModel;
    VisibleRangeDataModel visibleRange;
    WaveformView waveformView;
    PlaybackPositionOverlay playbackOverlay;
    LoopPointsOverlay loopPoints;
    Ruler ruler;
};

//==============================================================================
class MainSamplerView  : public Component,
                         public DataModel::Listener
{
public:
    MainSamplerView (const DataModel& model,
                     PlaybackPositionOverlay::Provider provider,
                     UndoManager& um)
        : dataModel (model),
          waveformEditor (dataModel, move (provider), um),
          undoManager (&um)
    {
        dataModel.addListener (*this);

        addAndMakeVisible (waveformEditor);
        addAndMakeVisible (loadNewSampleButton);

        auto setReader = [this] (const FileChooser& fc)
        {
            undoManager->beginNewTransaction();
            auto readerFactory = new FileAudioFormatReaderFactory (fc.getResult());
            dataModel.setSampleReader (std::unique_ptr<AudioFormatReaderFactory> (readerFactory),
                                       undoManager);
        };

        loadNewSampleButton.onClick = [this, setReader]
        {
            fileChooser.launchAsync (FileBrowserComponent::FileChooserFlags::openMode |
                                     FileBrowserComponent::FileChooserFlags::canSelectFiles,
                                     setReader);
        };

        addAndMakeVisible (centreFrequency);
        centreFrequency.onValueChange = [this]
        {
            undoManager->beginNewTransaction();
            dataModel.setCentreFrequencyHz (centreFrequency.getValue(),
                                            centreFrequency.isMouseButtonDown() ? nullptr : undoManager);
        };

        centreFrequency.setRange (20, 20000, 1);
        centreFrequency.setSliderStyle (Slider::SliderStyle::IncDecButtons);
        centreFrequency.setIncDecButtonsMode (Slider::IncDecButtonMode::incDecButtonsDraggable_Vertical);

        auto radioGroupId = 1;

        for (auto buttonPtr : { &loopKindNone, &loopKindForward, &loopKindPingpong })
        {
            addAndMakeVisible (buttonPtr);
            buttonPtr->setRadioGroupId (radioGroupId, dontSendNotification);
            buttonPtr->setClickingTogglesState (true);
        }

        loopKindNone.onClick = [this]
        {
            if (loopKindNone.getToggleState())
            {
                undoManager->beginNewTransaction();
                dataModel.setLoopMode (LoopMode::none, undoManager);
            }
        };

        loopKindForward.onClick = [this]
        {
            if (loopKindForward.getToggleState())
            {
                undoManager->beginNewTransaction();
                dataModel.setLoopMode (LoopMode::forward, undoManager);
            }
        };

        loopKindPingpong.onClick = [this]
        {
            if (loopKindPingpong.getToggleState())
            {
                undoManager->beginNewTransaction();
                dataModel.setLoopMode (LoopMode::pingpong, undoManager);
            }
        };

        addAndMakeVisible (centreFrequencyLabel);
        addAndMakeVisible (loopKindLabel);
    }

private:
    void resized() override
    {
        auto bounds = getLocalBounds();

        auto topBar = bounds.removeFromTop (50);
        auto padding = 4;
        loadNewSampleButton .setBounds (topBar.removeFromRight (100).reduced (padding));
        centreFrequencyLabel.setBounds (topBar.removeFromLeft  (100).reduced (padding));
        centreFrequency     .setBounds (topBar.removeFromLeft  (100).reduced (padding));

        auto bottomBar = bounds.removeFromBottom (50);
        loopKindLabel   .setBounds (bottomBar.removeFromLeft (100).reduced (padding));
        loopKindNone    .setBounds (bottomBar.removeFromLeft (80) .reduced (padding));
        loopKindForward .setBounds (bottomBar.removeFromLeft (80) .reduced (padding));
        loopKindPingpong.setBounds (bottomBar.removeFromLeft (80) .reduced (padding));

        waveformEditor.setBounds (bounds);
    }

    void loopModeChanged (LoopMode value) override
    {
        switch (value)
        {
            case LoopMode::none:
                loopKindNone.setToggleState (true, dontSendNotification);
                break;
            case LoopMode::forward:
                loopKindForward.setToggleState (true, dontSendNotification);
                break;
            case LoopMode::pingpong:
                loopKindPingpong.setToggleState (true, dontSendNotification);
                break;
        }
    }

    void centreFrequencyHzChanged (double value) override
    {
        centreFrequency.setValue (value, dontSendNotification);
    }

    DataModel dataModel;
    WaveformEditor waveformEditor;
    TextButton loadNewSampleButton { "Load New Sample" };
    Slider centreFrequency;

    TextButton loopKindNone        { "None" },
               loopKindForward     { "Forward" },
               loopKindPingpong    { "Ping Pong" };

    Label centreFrequencyLabel     { {}, "Sample Centre Freq / Hz" },
          loopKindLabel            { {}, "Looping Mode" };


    FileChooser fileChooser { "Select a file to load...", File(),
                              dataModel.getAudioFormatManager().getWildcardForAllFormats() };

    UndoManager* undoManager;
};

//==============================================================================
struct ProcessorState
{
    int synthVoices;
    bool legacyModeEnabled;
    Range<int> legacyChannels;
    int legacyPitchbendRange;
    bool voiceStealingEnabled;
    MPEZoneLayout mpeZoneLayout;
    std::unique_ptr<AudioFormatReaderFactory> readerFactory;
    Range<double> loopPointsSeconds;
    double centreFrequencyHz;
    LoopMode loopMode;
};

//==============================================================================
// We store the current sampler sound in a shared_ptr. Although we never
// call mutating member functions on this shared_ptr, we do read from it on
// both the audio and gui threads. Such concurrent reads should be safe
// without using atomic methods, but we use a tiny wrapper to enforce atomic
// accesses anyway - if nothing else, this wrapper enforces and documents that
// we never mutate the shared_ptr in a way which could cause a data race.
template <typename Contents>
class AtomicSharedPtr final
{
public:
    AtomicSharedPtr() = default;
    explicit AtomicSharedPtr (std::shared_ptr<Contents> contents)
        : contents (move (contents))
    {}

    AtomicSharedPtr (const AtomicSharedPtr& other) = delete;
    AtomicSharedPtr& operator= (const AtomicSharedPtr& other) = delete;

    std::shared_ptr<Contents> load() const
    {
        return atomic_load (&contents);
    }

private:
    std::shared_ptr<Contents> contents;
};

//==============================================================================
class SamplerAudioProcessor  : public AudioProcessor
{
public:
    SamplerAudioProcessor()
        : AudioProcessor (BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true))
    {
        if (auto* asset = createAssetInputStream ("cello.wav"))
        {
            std::unique_ptr<InputStream> inputStream (asset);
            inputStream->readIntoMemoryBlock (mb);

            readerFactory.reset (new MemoryAudioFormatReaderFactory (mb.getData(), mb.getSize()));
        }

        // Set up initial sample, which we load from a binary resource
        AudioFormatManager manager;
        manager.registerBasicFormats();
        auto reader = readerFactory->make (manager);
        auto sound = samplerSound.load();
        auto sample = std::unique_ptr<Sample> (new Sample (*reader, 10.0));
        auto lengthInSeconds = sample->getLength() / sample->getSampleRate();
        sound->setLoopPointsInSeconds ({lengthInSeconds * 0.1, lengthInSeconds * 0.9 });
        sound->setSample (move (sample));

        // Start with the max number of voices
        for (auto i = 0; i != maxVoices; ++i)
            synthesiser.addVoice (new MPESamplerVoice (sound));
    }

    void prepareToPlay (double sampleRate, int) override
    {
        synthesiser.setCurrentPlaybackSampleRate (sampleRate);
    }

    void releaseResources() override {}

    bool isBusesLayoutSupported (const BusesLayout& layouts) const override
    {
        return layouts.getMainOutputChannelSet() == AudioChannelSet::mono()
            || layouts.getMainOutputChannelSet() == AudioChannelSet::stereo();
    }

    //==============================================================================
    AudioProcessorEditor* createEditor() override
    {
        // This function will be called from the message thread. We lock the command
        // queue to ensure that no messages are processed for the duration of this
        // call.
        std::lock_guard<std::mutex> lock (commandQueueMutex);

        ProcessorState state;
        state.synthVoices          = synthesiser.getNumVoices();
        state.legacyModeEnabled    = synthesiser.isLegacyModeEnabled();
        state.legacyChannels       = synthesiser.getLegacyModeChannelRange();
        state.legacyPitchbendRange = synthesiser.getLegacyModePitchbendRange();
        state.voiceStealingEnabled = synthesiser.isVoiceStealingEnabled();
        state.mpeZoneLayout        = synthesiser.getZoneLayout();
        state.readerFactory        = readerFactory == nullptr ? nullptr : readerFactory->clone();

        auto sound = samplerSound.load();
        state.loopPointsSeconds = sound->getLoopPointsInSeconds();
        state.centreFrequencyHz = sound->getCentreFrequencyInHz();
        state.loopMode          = sound->getLoopMode();

        return new SamplerAudioProcessorEditor (*this, std::move (state));
    }

    bool hasEditor() const override                                       { return true; }

    //==============================================================================
    const String getName() const override                                 { return JucePlugin_Name; }
    bool acceptsMidi() const override                                     { return true; }
    bool producesMidi() const override                                    { return false; }
    bool isMidiEffect() const override                                    { return false; }
    double getTailLengthSeconds() const override                          { return 0.0; }

    //==============================================================================
    int getNumPrograms() override                                         { return 1; }
    int getCurrentProgram() override                                      { return 0; }
    void setCurrentProgram (int) override                                 {}
    const String getProgramName (int) override                            { return {}; }
    void changeProgramName (int, const String&) override                  {}

    //==============================================================================
    void getStateInformation (MemoryBlock&) override                      {}
    void setStateInformation (const void*, int) override                  {}

    //==============================================================================
    void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midiMessages) override
    {
        // Try to acquire a lock on the command queue.
        // If we were successful, we pop all pending commands off the queue and
        // apply them to the processor.
        // If we weren't able to acquire the lock, it's because someone called
        // createEditor, which requires that the processor data model stays in
        // a valid state for the duration of the call.
        std::unique_lock<std::mutex> lock (commandQueueMutex, std::try_to_lock);

        if (lock.owns_lock())
        {
            while (auto command = incomingCommands.pop())
            {
                command->run (*this);
                // We push the command onto the outgoing buffer, as long as it has
                // room. If it doesn't have room for some reason, we'll delete
                // the command right here on this thread, which might take a while
                // and cause the audio to glitch, so I hope the buffer size is big
                // enough!
                outgoingCommands.push (move (command));
            }
        }

        synthesiser.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples());

        auto loadedSamplerSound = samplerSound.load();

        if (loadedSamplerSound->getSample() == nullptr)
            return;

        auto numVoices = synthesiser.getNumVoices();

        // Update the current playback positions
        for (auto i = 0; i < maxVoices; ++i)
        {
            auto* voicePtr = dynamic_cast<MPESamplerVoice*> (synthesiser.getVoice (i));

            if (i < numVoices && voicePtr != nullptr)
                playbackPositions[(size_t) i] = static_cast<float> (voicePtr->getCurrentSamplePosition() / loadedSamplerSound->getSample()->getSampleRate());
            else
                playbackPositions[(size_t) i] = 0.0f;
        }
    }

    // These should be called from the GUI thread, and will block until the
    // command buffer has enough room to accept a command.
    void setSample (std::unique_ptr<AudioFormatReaderFactory> fact, AudioFormatManager& formatManager)
    {
        class SetSampleCommand
        {
        public:
            SetSampleCommand (std::unique_ptr<AudioFormatReaderFactory> r,
                              std::unique_ptr<Sample> sample,
                              std::vector<std::unique_ptr<MPESamplerVoice>> newVoices)
                : readerFactory (move (r)),
                  sample (move (sample)),
                  newVoices (move (newVoices))
            {}

            void operator() (SamplerAudioProcessor& proc)
            {
                proc.readerFactory = move (readerFactory);
                auto sound = proc.samplerSound.load();
                sound->setSample (move (sample));
                auto numberOfVoices = proc.synthesiser.getNumVoices();
                proc.synthesiser.clearVoices();

                for (auto it = begin (newVoices); proc.synthesiser.getNumVoices() < numberOfVoices; ++it)
                {
                    proc.synthesiser.addVoice (it->release());
                }
            }

        private:
            std::unique_ptr<AudioFormatReaderFactory> readerFactory;
            std::unique_ptr<Sample> sample;
            std::vector<std::unique_ptr<MPESamplerVoice>> newVoices;
        };

        // Note that all allocation happens here, on the main message thread. Then,
        // we transfer ownership across to the audio thread.
        auto loadedSamplerSound = samplerSound.load();
        std::vector<std::unique_ptr<MPESamplerVoice>> newSamplerVoices;
        newSamplerVoices.reserve (maxVoices);

        for (auto i = 0; i != maxVoices; ++i)
            newSamplerVoices.emplace_back (new MPESamplerVoice (loadedSamplerSound));

        if (fact == nullptr)
        {
            pushCommand (SetSampleCommand (move (fact),
                                           nullptr,
                                           move (newSamplerVoices)));
        }
        else
        {
            auto reader = fact->make (formatManager);
            pushCommand (SetSampleCommand (move (fact),
                                           std::unique_ptr<Sample> (new Sample (*reader, 10.0)),
                                           move (newSamplerVoices)));
        }
    }

    void setCentreFrequency (double centreFrequency)
    {
        pushCommand ([centreFrequency] (SamplerAudioProcessor& proc)
                     {
                         auto loaded = proc.samplerSound.load();
                         if (loaded != nullptr)
                             loaded->setCentreFrequencyInHz (centreFrequency);
                     });
    }

    void setLoopMode (LoopMode loopMode)
    {
        pushCommand ([loopMode] (SamplerAudioProcessor& proc)
                     {
                         auto loaded = proc.samplerSound.load();
                         if (loaded != nullptr)
                             loaded->setLoopMode (loopMode);
                     });
    }

    void setLoopPoints (Range<double> loopPoints)
    {
        pushCommand ([loopPoints] (SamplerAudioProcessor& proc)
                     {
                         auto loaded = proc.samplerSound.load();
                         if (loaded != nullptr)
                             loaded->setLoopPointsInSeconds (loopPoints);
                     });
    }

    void setMPEZoneLayout (MPEZoneLayout layout)
    {
        pushCommand ([layout] (SamplerAudioProcessor& proc)
                     {
                         // setZoneLayout will lock internally, so we don't care too much about
                         // ensuring that the layout doesn't get copied or destroyed on the
                         // audio thread. If the audio glitches while updating midi settings
                         // it doesn't matter too much.
                         proc.synthesiser.setZoneLayout (layout);
                     });
    }

    void setLegacyModeEnabled (int pitchbendRange, Range<int> channelRange)
    {
        pushCommand ([pitchbendRange, channelRange] (SamplerAudioProcessor& proc)
                     {
                         proc.synthesiser.enableLegacyMode (pitchbendRange, channelRange);
                     });
    }

    void setVoiceStealingEnabled (bool voiceStealingEnabled)
    {
        pushCommand ([voiceStealingEnabled] (SamplerAudioProcessor& proc)
                     {
                         proc.synthesiser.setVoiceStealingEnabled (voiceStealingEnabled);
                     });
    }

    void setNumberOfVoices (int numberOfVoices)
    {
        // We don't want to call 'new' on the audio thread. Normally, we'd
        // construct things here, on the GUI thread, and then move them into the
        // command lambda. Unfortunately, C++11 doesn't have extended lambda
        // capture, so we use a custom struct instead.

        class SetNumVoicesCommand
        {
        public:
            SetNumVoicesCommand (std::vector<std::unique_ptr<MPESamplerVoice>> newVoices)
                : newVoices (move (newVoices))
            {}

            void operator() (SamplerAudioProcessor& proc)
            {
                if (newVoices.size() < (size_t) proc.synthesiser.getNumVoices())
                    proc.synthesiser.reduceNumVoices (int (newVoices.size()));
                else
                    for (auto it = begin (newVoices); (size_t) proc.synthesiser.getNumVoices() < newVoices.size(); ++it)
                        proc.synthesiser.addVoice (it->release());
            }

        private:
            std::vector<std::unique_ptr<MPESamplerVoice>> newVoices;
        };

        numberOfVoices = std::min (maxVoices, numberOfVoices);
        auto loadedSamplerSound = samplerSound.load();
        std::vector<std::unique_ptr<MPESamplerVoice>> newSamplerVoices;
        newSamplerVoices.reserve ((size_t) numberOfVoices);

        for (auto i = 0; i != numberOfVoices; ++i)
            newSamplerVoices.emplace_back (new MPESamplerVoice (loadedSamplerSound));

        pushCommand (SetNumVoicesCommand (move (newSamplerVoices)));
    }

    // These accessors are just for an 'overview' and won't give the exact
    // state of the audio engine at a particular point in time.
    // If you call getNumVoices(), get the result '10', and then call
    // getPlaybackPosiiton(9), there's a chance the audio engine will have
    // been updated to remove some voices in the meantime, so the returned
    // value won't correspond to an existing voice.
    int getNumVoices() const                    { return synthesiser.getNumVoices(); }
    float getPlaybackPosition (int voice) const { return playbackPositions.at ((size_t) voice); }

private:
    //==============================================================================
    class SamplerAudioProcessorEditor  : public AudioProcessorEditor,
                                         public FileDragAndDropTarget,
                                         public DataModel::Listener,
                                         public MPESettingsDataModel::Listener
    {
    public:
        SamplerAudioProcessorEditor (SamplerAudioProcessor& p, ProcessorState state)
            : AudioProcessorEditor (&p),
              processor (p),
              mainSamplerView (dataModel,
                               [&p]
                               {
                                   std::vector<float> ret;
                                   auto voices = p.getNumVoices();
                                   ret.reserve ((size_t) voices);

                                   for (auto i = 0; i != voices; ++i)
                                       ret.emplace_back (p.getPlaybackPosition (i));

                                   return ret;
                               },
                               undoManager)
        {
            dataModel.addListener (*this);
            mpeSettings.addListener (*this);

            formatManager.registerBasicFormats();

            addAndMakeVisible (tabbedComponent);

            auto lookFeel = dynamic_cast<LookAndFeel_V4*> (&getLookAndFeel());
            auto bg = lookFeel->getCurrentColourScheme()
                               .getUIColour (LookAndFeel_V4::ColourScheme::UIColour::widgetBackground);

            tabbedComponent.addTab ("Sample Editor", bg, &mainSamplerView, false);
            tabbedComponent.addTab ("MPE Settings", bg, &settingsComponent, false);

            mpeSettings.setSynthVoices          (state.synthVoices,               nullptr);
            mpeSettings.setLegacyModeEnabled    (state.legacyModeEnabled,         nullptr);
            mpeSettings.setLegacyFirstChannel   (state.legacyChannels.getStart(), nullptr);
            mpeSettings.setLegacyLastChannel    (state.legacyChannels.getEnd(),   nullptr);
            mpeSettings.setLegacyPitchbendRange (state.legacyPitchbendRange,      nullptr);
            mpeSettings.setVoiceStealingEnabled (state.voiceStealingEnabled,      nullptr);
            mpeSettings.setMPEZoneLayout        (state.mpeZoneLayout,             nullptr);

            dataModel.setSampleReader (move (state.readerFactory),    nullptr);
            dataModel.setLoopPointsSeconds  (state.loopPointsSeconds, nullptr);
            dataModel.setCentreFrequencyHz  (state.centreFrequencyHz, nullptr);
            dataModel.setLoopMode           (state.loopMode,          nullptr);

            // Make sure that before the constructor has finished, you've set the
            // editor's size to whatever you need it to be.
            setResizable (true, true);
            setResizeLimits (640, 480, 2560, 1440);
            setSize (640, 480);
        }

    private:
        void resized() override
        {
            tabbedComponent.setBounds (getLocalBounds());
        }

        bool keyPressed (const KeyPress& key) override
        {
            if (key == KeyPress ('z', ModifierKeys::commandModifier, 0))
            {
                undoManager.undo();
                return true;
            }

            if (key == KeyPress ('z', ModifierKeys::commandModifier | ModifierKeys::shiftModifier, 0))
            {
                undoManager.redo();
                return true;
            }

            return Component::keyPressed (key);
        }

        bool isInterestedInFileDrag (const StringArray& files) override
        {
            WildcardFileFilter filter (formatManager.getWildcardForAllFormats(), {}, "Known Audio Formats");
            return files.size() == 1 && filter.isFileSuitable (files[0]);
        }

        void filesDropped (const StringArray& files, int, int) override
        {
            jassert (files.size() == 1);
            undoManager.beginNewTransaction();
            auto r = new FileAudioFormatReaderFactory (files[0]);
            dataModel.setSampleReader (std::unique_ptr<AudioFormatReaderFactory> (r),
                                       &undoManager);

        }

        void sampleReaderChanged (std::shared_ptr<AudioFormatReaderFactory> value) override
        {
            processor.setSample (value == nullptr ? nullptr : value->clone(),
                                 dataModel.getAudioFormatManager());
        }

        void centreFrequencyHzChanged (double value) override
        {
            processor.setCentreFrequency (value);
        }

        void loopPointsSecondsChanged (Range<double> value) override
        {
            processor.setLoopPoints (value);
        }

        void loopModeChanged (LoopMode value) override
        {
            processor.setLoopMode (value);
        }

        void synthVoicesChanged (int value) override
        {
            processor.setNumberOfVoices (value);
        }

        void voiceStealingEnabledChanged (bool value) override
        {
            processor.setVoiceStealingEnabled (value);
        }

        void legacyModeEnabledChanged (bool value) override
        {
            if (value)
                setProcessorLegacyMode();
            else
                setProcessorMPEMode();
        }

        void mpeZoneLayoutChanged (const MPEZoneLayout&) override
        {
            setProcessorMPEMode();
        }

        void legacyFirstChannelChanged (int) override
        {
            setProcessorLegacyMode();
        }

        void legacyLastChannelChanged (int) override
        {
            setProcessorLegacyMode();
        }

        void legacyPitchbendRangeChanged (int) override
        {
            setProcessorLegacyMode();
        }

        void setProcessorLegacyMode()
        {
            processor.setLegacyModeEnabled (mpeSettings.getLegacyPitchbendRange(),
                                            Range<int> (mpeSettings.getLegacyFirstChannel(),
                                            mpeSettings.getLegacyLastChannel()));
        }

        void setProcessorMPEMode()
        {
            processor.setMPEZoneLayout (mpeSettings.getMPEZoneLayout());
        }

        SamplerAudioProcessor& processor;
        AudioFormatManager formatManager;
        DataModel dataModel { formatManager };
        UndoManager undoManager;
        MPESettingsDataModel mpeSettings { dataModel.mpeSettings() };
        TabbedComponent tabbedComponent { TabbedButtonBar::Orientation::TabsAtTop };
        MPESettingsComponent settingsComponent { dataModel.mpeSettings(), undoManager };
        MainSamplerView mainSamplerView;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SamplerAudioProcessorEditor)
    };

    //==============================================================================
    // We want to send type-erased commands to the audio thread, but we also
    // want those commands to contain move-only resources, so that we can
    // construct resources on the gui thread, and then transfer ownership
    // cheaply to the audio thread. We can't do this with std::function
    // because it enforces that functions are copy-constructible.
    // Therefore, we use a very simple templated type-eraser here.
    struct Command
    {
        virtual ~Command() noexcept                    = default;
        virtual void run (SamplerAudioProcessor& proc) = 0;
    };

    template <typename Func>
    class TemplateCommand  : public Command,
                             public Func
    {
    public:
        template <typename FuncPrime>
        explicit TemplateCommand (FuncPrime&& funcPrime)
            : Func (std::forward<FuncPrime> (funcPrime))
        {}

        void run (SamplerAudioProcessor& proc) override
        {
            (*this) (proc);
        }
    };

    template <typename Func>
    static std::unique_ptr<Command> make_command (Func&& func)
    {
        return std::unique_ptr<TemplateCommand<Func>> (new TemplateCommand<Func> (std::forward<Func> (func)));
    }

    using CommandFifo = MoveOnlyFifo<std::unique_ptr<Command>>;

    class OutgoingBufferCleaner  : public Timer
    {
    public:
        explicit OutgoingBufferCleaner (CommandFifo& bufferToEmpty)
            : buffer (bufferToEmpty)
        {
            startTimer (500);
        }

    private:
        void timerCallback() override
        {
            while (auto command = buffer.pop())
                command = {};
        }

        CommandFifo& buffer;
    };

    // Spin, trying to post a command to the sampler sound, until there's
    // enough room in the command buffer to accept the new command.
    template <typename Func>
    void pushCommand (Func&& func)
    {
        auto command = make_command (std::forward<Func> (func));
        while (command)
            command = incomingCommands.push (move (command));
    }

    // We have an incoming and an outgoing command queue. The incoming commands
    // are used to update the sampler sound in a thread-safe way, without
    // blocking. Once we've consumed a command, we push it back onto the
    // outgoing command queue, which is cleaned up periodically by the
    // outgoingBufferCleaner.
    CommandFifo incomingCommands;
    CommandFifo outgoingCommands;
    OutgoingBufferCleaner outgoingBufferCleaner { outgoingCommands };

    MemoryBlock mb;
    std::unique_ptr<AudioFormatReaderFactory> readerFactory;
    AtomicSharedPtr<MPESamplerSound> samplerSound { std::make_shared<MPESamplerSound>() };
    MPESynthesiser synthesiser;

    // This mutex is used to ensure we don't modify the processor state during
    // a call to createEditor, which would cause the UI to become desynched
    // with the real state of the processor.
    std::mutex commandQueueMutex;

    static const int maxVoices { 20 };

    // This is used for visualising the current playback position of each voice.
    std::array<std::atomic<float>, maxVoices> playbackPositions;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (SamplerAudioProcessor)
};

const int SamplerAudioProcessor::maxVoices;
