2612 lines
90 KiB
C
2612 lines
90 KiB
C
|
/*
|
||
|
==============================================================================
|
||
|
|
||
|
This file is part of the JUCE examples.
|
||
|
Copyright (c) 2020 - Raw Material Software Limited
|
||
|
|
||
|
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
|
||
|
|
||
|
pluginCharacteristics: pluginIsSynth, pluginWantsMidiIn
|
||
|
|
||
|
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
|
||
|
};
|
||
|
|
||
|
// 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.
|
||
|
template <typename Proc>
|
||
|
struct Command
|
||
|
{
|
||
|
virtual ~Command() noexcept = default;
|
||
|
virtual void run (Proc& proc) = 0;
|
||
|
};
|
||
|
|
||
|
template <typename Proc, typename Func>
|
||
|
class TemplateCommand : public Command<Proc>,
|
||
|
private Func
|
||
|
{
|
||
|
public:
|
||
|
template <typename FuncPrime>
|
||
|
explicit TemplateCommand (FuncPrime&& funcPrime)
|
||
|
: Func (std::forward<FuncPrime> (funcPrime))
|
||
|
{}
|
||
|
|
||
|
void run (Proc& proc) override { (*this) (proc); }
|
||
|
};
|
||
|
|
||
|
template <typename Proc>
|
||
|
class CommandFifo final
|
||
|
{
|
||
|
public:
|
||
|
explicit CommandFifo (int size)
|
||
|
: buffer ((size_t) size),
|
||
|
abstractFifo (size)
|
||
|
{}
|
||
|
|
||
|
CommandFifo()
|
||
|
: CommandFifo (1024)
|
||
|
{}
|
||
|
|
||
|
template <typename Item>
|
||
|
void push (Item&& item) noexcept
|
||
|
{
|
||
|
auto command = makeCommand (std::forward<Item> (item));
|
||
|
|
||
|
abstractFifo.write (1).forEach ([&] (int index)
|
||
|
{
|
||
|
buffer[size_t (index)] = std::move (command);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void call (Proc& proc) noexcept
|
||
|
{
|
||
|
abstractFifo.read (abstractFifo.getNumReady()).forEach ([&] (int index)
|
||
|
{
|
||
|
buffer[size_t (index)]->run (proc);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
template <typename Func>
|
||
|
static std::unique_ptr<Command<Proc>> makeCommand (Func&& func)
|
||
|
{
|
||
|
using Decayed = typename std::decay<Func>::type;
|
||
|
return std::make_unique<TemplateCommand<Proc, Decayed>> (std::forward<Func> (func));
|
||
|
}
|
||
|
|
||
|
std::vector<std::unique_ptr<Command<Proc>>> 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 (std::move (sound))
|
||
|
{
|
||
|
jassert (samplerSound != nullptr);
|
||
|
}
|
||
|
|
||
|
void noteStarted() override
|
||
|
{
|
||
|
jassert (currentlyPlayingNote.isValid());
|
||
|
jassert (currentlyPlayingNote.keyState == MPENote::keyDown
|
||
|
|| currentlyPlayingNote.keyState == MPENote::keyDownAndSustained);
|
||
|
|
||
|
level .setTargetValue (currentlyPlayingNote.noteOnVelocity.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);
|
||
|
|
||
|
previousPressure = currentlyPlayingNote.pressure.asUnsignedFloat();
|
||
|
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
|
||
|
{
|
||
|
const auto currentPressure = static_cast<double> (currentlyPlayingNote.pressure.asUnsignedFloat());
|
||
|
const auto deltaPressure = currentPressure - previousPressure;
|
||
|
level.setTargetValue (jlimit (0.0, 1.0, level.getCurrentValue() + deltaPressure));
|
||
|
previousPressure = currentPressure;
|
||
|
}
|
||
|
|
||
|
void notePitchbendChanged() override
|
||
|
{
|
||
|
frequency.setTargetValue (currentlyPlayingNote.getFrequencyInHertz());
|
||
|
}
|
||
|
|
||
|
void noteTimbreChanged() override {}
|
||
|
void noteKeyStateChanged() override {}
|
||
|
|
||
|
void renderNextBlock (AudioBuffer<float>& outputBuffer,
|
||
|
int startSample,
|
||
|
int numSamples) override
|
||
|
{
|
||
|
render (outputBuffer, startSample, numSamples);
|
||
|
}
|
||
|
|
||
|
void renderNextBlock (AudioBuffer<double>& outputBuffer,
|
||
|
int startSample,
|
||
|
int numSamples) override
|
||
|
{
|
||
|
render (outputBuffer, startSample, numSamples);
|
||
|
}
|
||
|
|
||
|
double getCurrentSamplePosition() const
|
||
|
{
|
||
|
return currentSamplePos;
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
template <typename Element>
|
||
|
void render (AudioBuffer<Element>& outputBuffer, int startSample, int numSamples)
|
||
|
{
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
template <typename Element>
|
||
|
bool renderNextSample (const float* inL,
|
||
|
const float* inR,
|
||
|
Element* outL,
|
||
|
Element* 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 = (Element) (currentSamplePos - pos);
|
||
|
auto invAlpha = 1.0f - alpha;
|
||
|
|
||
|
// just using a very simple linear interpolation here..
|
||
|
auto l = static_cast<Element> (currentLevel * (inL[pos] * invAlpha + inL[nextPos] * alpha));
|
||
|
auto r = static_cast<Element> ((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;
|
||
|
|
||
|
default:
|
||
|
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 std::tuple<double, Direction> (nextSamplePos, nextDirection);
|
||
|
}
|
||
|
|
||
|
if (samplerSound->getLoopMode() == LoopMode::none)
|
||
|
return std::tuple<double, Direction> (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 std::tuple<double, Direction> (nextSamplePos, nextDirection);
|
||
|
}
|
||
|
|
||
|
std::shared_ptr<const MPESamplerSound> samplerSound;
|
||
|
SmoothedValue<double> level { 0 };
|
||
|
SmoothedValue<double> frequency { 0 };
|
||
|
SmoothedValue<double> loopBegin;
|
||
|
SmoothedValue<double> loopEnd;
|
||
|
double previousPressure { 0 };
|
||
|
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 (std::make_unique<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* sampleDataIn, size_t dataSizeIn)
|
||
|
: sampleData (sampleDataIn),
|
||
|
dataSize (dataSizeIn)
|
||
|
{}
|
||
|
|
||
|
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 fileIn)
|
||
|
: file (std::move (fileIn))
|
||
|
{}
|
||
|
|
||
|
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 : private 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 : private 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 : private 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& audioFormatManagerIn)
|
||
|
: DataModel (audioFormatManagerIn, ValueTree (IDs::DATA_MODEL))
|
||
|
{}
|
||
|
|
||
|
DataModel (AudioFormatManager& audioFormatManagerIn, const ValueTree& vt)
|
||
|
: audioFormatManager (&audioFormatManagerIn),
|
||
|
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 (double) 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,
|
||
|
private 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,
|
||
|
private 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, ¬ePitchbendRange })
|
||
|
{
|
||
|
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,
|
||
|
private 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 onMouseDownIn,
|
||
|
MouseCallback onMouseDragIn,
|
||
|
MouseCallback onMouseUpIn)
|
||
|
: text (std::move (marker)),
|
||
|
onMouseDown (std::move (onMouseDownIn)),
|
||
|
onMouseDrag (std::move (onMouseDragIn)),
|
||
|
onMouseUp (std::move (onMouseUpIn))
|
||
|
{
|
||
|
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,
|
||
|
private 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 = (float) 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 outStream;
|
||
|
outStream << std::setprecision (roundToInt (precision)) << time;
|
||
|
|
||
|
const auto bounds = Rectangle<int> (Point<int> (roundToInt (xPos) + 3, 0),
|
||
|
Point<int> (roundToInt (xPos + minDivisionWidth), getHeight()));
|
||
|
|
||
|
g.drawText (outStream.str(), bounds, 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, (float) 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,
|
||
|
private DataModel::Listener,
|
||
|
private VisibleRangeDataModel::Listener
|
||
|
{
|
||
|
public:
|
||
|
LoopPointsOverlay (const DataModel& dModel,
|
||
|
const VisibleRangeDataModel& vModel,
|
||
|
UndoManager& undoManagerIn)
|
||
|
: 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 (&undoManagerIn)
|
||
|
{
|
||
|
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,
|
||
|
private Timer,
|
||
|
private VisibleRangeDataModel::Listener
|
||
|
{
|
||
|
public:
|
||
|
using Provider = std::function<std::vector<float>()>;
|
||
|
PlaybackPositionOverlay (const VisibleRangeDataModel& model,
|
||
|
Provider providerIn)
|
||
|
: visibleRange (model),
|
||
|
provider (std::move (providerIn))
|
||
|
{
|
||
|
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,
|
||
|
private ChangeListener,
|
||
|
private DataModel::Listener,
|
||
|
private 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)
|
||
|
{
|
||
|
if (auto reader = value->make (dataModel.getAudioFormatManager()))
|
||
|
{
|
||
|
thumbnail.setReader (reader.release(), currentHashCode);
|
||
|
currentHashCode += 1;
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
thumbnail.clear();
|
||
|
}
|
||
|
|
||
|
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,
|
||
|
private 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>) 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,
|
||
|
private DataModel::Listener,
|
||
|
private ChangeListener
|
||
|
{
|
||
|
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);
|
||
|
addAndMakeVisible (undoButton);
|
||
|
addAndMakeVisible (redoButton);
|
||
|
|
||
|
auto setReader = [this] (const FileChooser& fc)
|
||
|
{
|
||
|
const auto result = fc.getResult();
|
||
|
|
||
|
if (result != File())
|
||
|
{
|
||
|
undoManager.beginNewTransaction();
|
||
|
auto readerFactory = new FileAudioFormatReaderFactory (result);
|
||
|
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);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
undoButton.onClick = [this] { undoManager.undo(); };
|
||
|
redoButton.onClick = [this] { undoManager.redo(); };
|
||
|
|
||
|
addAndMakeVisible (centreFrequencyLabel);
|
||
|
addAndMakeVisible (loopKindLabel);
|
||
|
|
||
|
changeListenerCallback (&undoManager);
|
||
|
undoManager.addChangeListener (this);
|
||
|
}
|
||
|
|
||
|
~MainSamplerView() override
|
||
|
{
|
||
|
undoManager.removeChangeListener (this);
|
||
|
}
|
||
|
|
||
|
private:
|
||
|
void changeListenerCallback (ChangeBroadcaster* source) override
|
||
|
{
|
||
|
if (source == &undoManager)
|
||
|
{
|
||
|
undoButton.setEnabled (undoManager.canUndo());
|
||
|
redoButton.setEnabled (undoManager.canRedo());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void resized() override
|
||
|
{
|
||
|
auto bounds = getLocalBounds();
|
||
|
|
||
|
auto topBar = bounds.removeFromTop (50);
|
||
|
auto padding = 4;
|
||
|
loadNewSampleButton .setBounds (topBar.removeFromRight (100).reduced (padding));
|
||
|
redoButton .setBounds (topBar.removeFromRight (100).reduced (padding));
|
||
|
undoButton .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;
|
||
|
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void centreFrequencyHzChanged (double value) override
|
||
|
{
|
||
|
centreFrequency.setValue (value, dontSendNotification);
|
||
|
}
|
||
|
|
||
|
DataModel dataModel;
|
||
|
WaveformEditor waveformEditor;
|
||
|
TextButton loadNewSampleButton { "Load New Sample" };
|
||
|
TextButton undoButton { "Undo" };
|
||
|
TextButton redoButton { "Redo" };
|
||
|
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;
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
class SamplerAudioProcessor : public AudioProcessor
|
||
|
{
|
||
|
public:
|
||
|
SamplerAudioProcessor()
|
||
|
: AudioProcessor (BusesProperties().withOutput ("Output", AudioChannelSet::stereo(), true))
|
||
|
{
|
||
|
if (auto inputStream = createAssetInputStream ("cello.wav"))
|
||
|
{
|
||
|
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);
|
||
|
jassert (reader != nullptr); // Failed to load resource!
|
||
|
|
||
|
auto sound = samplerSound;
|
||
|
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.
|
||
|
SpinLock::ScopedLockType 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;
|
||
|
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 "SamplerPlugin"; }
|
||
|
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 "None"; }
|
||
|
void changeProgramName (int, const String&) override {}
|
||
|
|
||
|
//==============================================================================
|
||
|
void getStateInformation (MemoryBlock&) override {}
|
||
|
void setStateInformation (const void*, int) override {}
|
||
|
|
||
|
//==============================================================================
|
||
|
void processBlock (AudioBuffer<float>& buffer, MidiBuffer& midi) override
|
||
|
{
|
||
|
process (buffer, midi);
|
||
|
}
|
||
|
|
||
|
void processBlock (AudioBuffer<double>& buffer, MidiBuffer& midi) override
|
||
|
{
|
||
|
process (buffer, midi);
|
||
|
}
|
||
|
|
||
|
// 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> sampleIn,
|
||
|
std::vector<std::unique_ptr<MPESamplerVoice>> newVoicesIn)
|
||
|
: readerFactory (std::move (r)),
|
||
|
sample (std::move (sampleIn)),
|
||
|
newVoices (std::move (newVoicesIn))
|
||
|
{}
|
||
|
|
||
|
void operator() (SamplerAudioProcessor& proc)
|
||
|
{
|
||
|
proc.readerFactory = move (readerFactory);
|
||
|
auto sound = proc.samplerSound;
|
||
|
sound->setSample (std::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;
|
||
|
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)
|
||
|
{
|
||
|
commands.push (SetSampleCommand (move (fact),
|
||
|
nullptr,
|
||
|
move (newSamplerVoices)));
|
||
|
}
|
||
|
else if (auto reader = fact->make (formatManager))
|
||
|
{
|
||
|
commands.push (SetSampleCommand (move (fact),
|
||
|
std::unique_ptr<Sample> (new Sample (*reader, 10.0)),
|
||
|
move (newSamplerVoices)));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void setCentreFrequency (double centreFrequency)
|
||
|
{
|
||
|
commands.push ([centreFrequency] (SamplerAudioProcessor& proc)
|
||
|
{
|
||
|
auto loaded = proc.samplerSound;
|
||
|
if (loaded != nullptr)
|
||
|
loaded->setCentreFrequencyInHz (centreFrequency);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void setLoopMode (LoopMode loopMode)
|
||
|
{
|
||
|
commands.push ([loopMode] (SamplerAudioProcessor& proc)
|
||
|
{
|
||
|
auto loaded = proc.samplerSound;
|
||
|
if (loaded != nullptr)
|
||
|
loaded->setLoopMode (loopMode);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void setLoopPoints (Range<double> loopPoints)
|
||
|
{
|
||
|
commands.push ([loopPoints] (SamplerAudioProcessor& proc)
|
||
|
{
|
||
|
auto loaded = proc.samplerSound;
|
||
|
if (loaded != nullptr)
|
||
|
loaded->setLoopPointsInSeconds (loopPoints);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void setMPEZoneLayout (MPEZoneLayout layout)
|
||
|
{
|
||
|
commands.push ([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)
|
||
|
{
|
||
|
commands.push ([pitchbendRange, channelRange] (SamplerAudioProcessor& proc)
|
||
|
{
|
||
|
proc.synthesiser.enableLegacyMode (pitchbendRange, channelRange);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void setVoiceStealingEnabled (bool voiceStealingEnabled)
|
||
|
{
|
||
|
commands.push ([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>> newVoicesIn)
|
||
|
: newVoices (std::move (newVoicesIn))
|
||
|
{}
|
||
|
|
||
|
void operator() (SamplerAudioProcessor& proc)
|
||
|
{
|
||
|
if ((int) newVoices.size() < 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 ((int) maxVoices, numberOfVoices);
|
||
|
auto loadedSamplerSound = samplerSound;
|
||
|
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));
|
||
|
|
||
|
commands.push (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,
|
||
|
private DataModel::Listener,
|
||
|
private MPESettingsDataModel::Listener
|
||
|
{
|
||
|
public:
|
||
|
SamplerAudioProcessorEditor (SamplerAudioProcessor& p, ProcessorState state)
|
||
|
: AudioProcessorEditor (&p),
|
||
|
samplerAudioProcessor (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
|
||
|
{
|
||
|
samplerAudioProcessor.setSample (value == nullptr ? nullptr : value->clone(),
|
||
|
dataModel.getAudioFormatManager());
|
||
|
}
|
||
|
|
||
|
void centreFrequencyHzChanged (double value) override
|
||
|
{
|
||
|
samplerAudioProcessor.setCentreFrequency (value);
|
||
|
}
|
||
|
|
||
|
void loopPointsSecondsChanged (Range<double> value) override
|
||
|
{
|
||
|
samplerAudioProcessor.setLoopPoints (value);
|
||
|
}
|
||
|
|
||
|
void loopModeChanged (LoopMode value) override
|
||
|
{
|
||
|
samplerAudioProcessor.setLoopMode (value);
|
||
|
}
|
||
|
|
||
|
void synthVoicesChanged (int value) override
|
||
|
{
|
||
|
samplerAudioProcessor.setNumberOfVoices (value);
|
||
|
}
|
||
|
|
||
|
void voiceStealingEnabledChanged (bool value) override
|
||
|
{
|
||
|
samplerAudioProcessor.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()
|
||
|
{
|
||
|
samplerAudioProcessor.setLegacyModeEnabled (mpeSettings.getLegacyPitchbendRange(),
|
||
|
Range<int> (mpeSettings.getLegacyFirstChannel(),
|
||
|
mpeSettings.getLegacyLastChannel()));
|
||
|
}
|
||
|
|
||
|
void setProcessorMPEMode()
|
||
|
{
|
||
|
samplerAudioProcessor.setMPEZoneLayout (mpeSettings.getMPEZoneLayout());
|
||
|
}
|
||
|
|
||
|
SamplerAudioProcessor& samplerAudioProcessor;
|
||
|
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)
|
||
|
};
|
||
|
|
||
|
//==============================================================================
|
||
|
template <typename Element>
|
||
|
void process (AudioBuffer<Element>& buffer, MidiBuffer& midiMessages)
|
||
|
{
|
||
|
// 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.
|
||
|
const GenericScopedTryLock<SpinLock> lock (commandQueueMutex);
|
||
|
|
||
|
if (lock.isLocked())
|
||
|
commands.call (*this);
|
||
|
|
||
|
synthesiser.renderNextBlock (buffer, midiMessages, 0, buffer.getNumSamples());
|
||
|
|
||
|
auto loadedSamplerSound = samplerSound;
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
CommandFifo<SamplerAudioProcessor> commands;
|
||
|
|
||
|
MemoryBlock mb;
|
||
|
std::unique_ptr<AudioFormatReaderFactory> readerFactory;
|
||
|
std::shared_ptr<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.
|
||
|
SpinLock commandQueueMutex;
|
||
|
|
||
|
enum { 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)
|
||
|
};
|