migrating to the latest JUCE version

This commit is contained in:
2022-11-04 23:11:33 +01:00
committed by Nikolai Rodionov
parent 4257a0f8ba
commit faf8f18333
2796 changed files with 888518 additions and 784244 deletions

View File

@@ -1,15 +1,15 @@
# ==============================================================================
#
# This file is part of the JUCE library.
# Copyright (c) 2020 - Raw Material Software Limited
# Copyright (c) 2022 - Raw Material Software Limited
#
# JUCE is an open source library subject to commercial or open-source
# licensing.
#
# By using JUCE, you agree to the terms of both the JUCE 6 End-User License
# Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
# By using JUCE, you agree to the terms of both the JUCE 7 End-User License
# Agreement and JUCE Privacy Policy.
#
# End User License Agreement: www.juce.com/juce-6-licence
# End User License Agreement: www.juce.com/juce-7-licence
# Privacy Policy: www.juce.com/juce-privacy-policy
#
# Or: You may also use this code under the terms of the GPL v3 (see
@@ -44,3 +44,5 @@ juce_add_modules(
juce_osc
juce_product_unlocking
juce_video)
add_subdirectory(juce_audio_plugin_client)

View File

@@ -1,77 +1,77 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
void Analytics::addDestination (AnalyticsDestination* destination)
{
destinations.add (destination);
}
OwnedArray<AnalyticsDestination>& Analytics::getDestinations()
{
return destinations;
}
void Analytics::setUserId (String newUserId)
{
userId = newUserId;
}
void Analytics::setUserProperties (StringPairArray properties)
{
userProperties = properties;
}
void Analytics::logEvent (const String& eventName,
const StringPairArray& parameters,
int eventType)
{
if (! isSuspended)
{
AnalyticsDestination::AnalyticsEvent event
{
eventName,
eventType,
Time::getMillisecondCounter(),
parameters,
userId,
userProperties
};
for (auto* destination : destinations)
destination->logEvent (event);
}
}
void Analytics::setSuspended (bool shouldBeSuspended)
{
isSuspended = shouldBeSuspended;
}
JUCE_IMPLEMENT_SINGLETON (Analytics)
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
void Analytics::addDestination (AnalyticsDestination* destination)
{
destinations.add (destination);
}
OwnedArray<AnalyticsDestination>& Analytics::getDestinations()
{
return destinations;
}
void Analytics::setUserId (String newUserId)
{
userId = newUserId;
}
void Analytics::setUserProperties (StringPairArray properties)
{
userProperties = properties;
}
void Analytics::logEvent (const String& eventName,
const StringPairArray& parameters,
int eventType)
{
if (! isSuspended)
{
AnalyticsDestination::AnalyticsEvent event
{
eventName,
eventType,
Time::getMillisecondCounter(),
parameters,
userId,
userProperties
};
for (auto* destination : destinations)
destination->logEvent (event);
}
}
void Analytics::setSuspended (bool shouldBeSuspended)
{
isSuspended = shouldBeSuspended;
}
JUCE_IMPLEMENT_SINGLETON (Analytics)
}

View File

@@ -1,116 +1,116 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A singleton class to manage analytics data.
Use an Analytics object to manage sending analytics data to one or more
AnalyticsDestinations.
@see AnalyticsDestination, ThreadedAnalyticsDestination,
AnalyticsDestination::AnalyticsEvent
@tags{Analytics}
*/
class JUCE_API Analytics : public DeletedAtShutdown
{
public:
//==============================================================================
/** Adds an AnalyticsDestination to the list of AnalyticsDestinations
managed by this Analytics object.
The Analytics class will take ownership of the AnalyticsDestination
passed to this function.
@param destination the AnalyticsDestination to manage
*/
void addDestination (AnalyticsDestination* destination);
/** Returns the array of AnalyticsDestinations managed by this class.
If you have added any subclasses of ThreadedAnalyticsDestination to
this class then you can remove them from this list to force them to
flush any pending events.
*/
OwnedArray<AnalyticsDestination>& getDestinations();
/** Sets a user ID that will be added to all AnalyticsEvents sent to
AnalyticsDestinations.
@param newUserId the userId to add to AnalyticsEvents
*/
void setUserId (String newUserId);
/** Sets some user properties that will be added to all AnalyticsEvents sent
to AnalyticsDestinations.
@param properties the userProperties to add to AnalyticsEvents
*/
void setUserProperties (StringPairArray properties);
/** Sends an AnalyticsEvent to all AnalyticsDestinations.
The AnalyticsEvent will be timestamped, and will have the userId and
userProperties populated by values previously set by calls to
setUserId and setUserProperties. The name, parameters and type will be
populated by the arguments supplied to this function.
@param eventName the event name
@param parameters the event parameters
@param eventType (optional) an integer to indicate the event
type, which will be set to 0 if not supplied.
*/
void logEvent (const String& eventName, const StringPairArray& parameters, int eventType = 0);
/** Suspends analytics submissions to AnalyticsDestinations.
@param shouldBeSuspended if event submission should be suspended
*/
void setSuspended (bool shouldBeSuspended);
#ifndef DOXYGEN
JUCE_DECLARE_SINGLETON (Analytics, false)
#endif
private:
//==============================================================================
Analytics() = default;
~Analytics() override { clearSingletonInstance(); }
String userId;
StringPairArray userProperties;
bool isSuspended = false;
OwnedArray<AnalyticsDestination> destinations;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Analytics)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A singleton class to manage analytics data.
Use an Analytics object to manage sending analytics data to one or more
AnalyticsDestinations.
@see AnalyticsDestination, ThreadedAnalyticsDestination,
AnalyticsDestination::AnalyticsEvent
@tags{Analytics}
*/
class JUCE_API Analytics : public DeletedAtShutdown
{
public:
//==============================================================================
/** Adds an AnalyticsDestination to the list of AnalyticsDestinations
managed by this Analytics object.
The Analytics class will take ownership of the AnalyticsDestination
passed to this function.
@param destination the AnalyticsDestination to manage
*/
void addDestination (AnalyticsDestination* destination);
/** Returns the array of AnalyticsDestinations managed by this class.
If you have added any subclasses of ThreadedAnalyticsDestination to
this class then you can remove them from this list to force them to
flush any pending events.
*/
OwnedArray<AnalyticsDestination>& getDestinations();
/** Sets a user ID that will be added to all AnalyticsEvents sent to
AnalyticsDestinations.
@param newUserId the userId to add to AnalyticsEvents
*/
void setUserId (String newUserId);
/** Sets some user properties that will be added to all AnalyticsEvents sent
to AnalyticsDestinations.
@param properties the userProperties to add to AnalyticsEvents
*/
void setUserProperties (StringPairArray properties);
/** Sends an AnalyticsEvent to all AnalyticsDestinations.
The AnalyticsEvent will be timestamped, and will have the userId and
userProperties populated by values previously set by calls to
setUserId and setUserProperties. The name, parameters and type will be
populated by the arguments supplied to this function.
@param eventName the event name
@param parameters the event parameters
@param eventType (optional) an integer to indicate the event
type, which will be set to 0 if not supplied.
*/
void logEvent (const String& eventName, const StringPairArray& parameters, int eventType = 0);
/** Suspends analytics submissions to AnalyticsDestinations.
@param shouldBeSuspended if event submission should be suspended
*/
void setSuspended (bool shouldBeSuspended);
#ifndef DOXYGEN
JUCE_DECLARE_SINGLETON (Analytics, false)
#endif
private:
//==============================================================================
Analytics() = default;
~Analytics() override { clearSingletonInstance(); }
String userId;
StringPairArray userProperties;
bool isSuspended = false;
OwnedArray<AnalyticsDestination> destinations;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Analytics)
};
} // namespace juce

View File

@@ -1,60 +1,60 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ButtonTracker::ButtonTracker (Button& buttonToTrack,
const String& triggeredEventName,
const StringPairArray& triggeredEventParameters,
int triggeredEventType)
: button (buttonToTrack),
eventName (triggeredEventName),
eventParameters (triggeredEventParameters),
eventType (triggeredEventType)
{
button.addListener (this);
}
ButtonTracker::~ButtonTracker()
{
button.removeListener (this);
}
void ButtonTracker::buttonClicked (Button* b)
{
if (b == &button)
{
auto params = eventParameters;
if (button.getClickingTogglesState())
params.set ("ButtonState", button.getToggleState() ? "On" : "Off");
Analytics::getInstance()->logEvent (eventName, params, eventType);
}
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ButtonTracker::ButtonTracker (Button& buttonToTrack,
const String& triggeredEventName,
const StringPairArray& triggeredEventParameters,
int triggeredEventType)
: button (buttonToTrack),
eventName (triggeredEventName),
eventParameters (triggeredEventParameters),
eventType (triggeredEventType)
{
button.addListener (this);
}
ButtonTracker::~ButtonTracker()
{
button.removeListener (this);
}
void ButtonTracker::buttonClicked (Button* b)
{
if (b == &button)
{
auto params = eventParameters;
if (button.getClickingTogglesState())
params.set ("ButtonState", button.getToggleState() ? "On" : "Off");
Analytics::getInstance()->logEvent (eventName, params, eventType);
}
}
} // namespace juce

View File

@@ -1,79 +1,79 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A class that automatically sends analytics events to the Analytics singleton
when a button is clicked.
@see Analytics, AnalyticsDestination::AnalyticsEvent
@tags{Analytics}
*/
class JUCE_API ButtonTracker : private Button::Listener
{
public:
//==============================================================================
/**
Creating one of these automatically sends analytics events to the Analytics
singleton when the corresponding button is clicked.
The name and parameters of the analytics event will be populated from the
variables supplied here. If clicking changes the button's state then the
parameters will have a {"ButtonState", "On"/"Off"} entry added.
@param buttonToTrack the button to track
@param triggeredEventName the name of the generated event
@param triggeredEventParameters the parameters to add to the generated
event
@param triggeredEventType (optional) an integer to indicate the event
type, which will be set to 0 if not supplied.
@see Analytics, AnalyticsDestination::AnalyticsEvent
*/
ButtonTracker (Button& buttonToTrack,
const String& triggeredEventName,
const StringPairArray& triggeredEventParameters = {},
int triggeredEventType = 0);
/** Destructor. */
~ButtonTracker() override;
private:
/** @internal */
void buttonClicked (Button*) override;
Button& button;
const String eventName;
const StringPairArray eventParameters;
const int eventType;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonTracker)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A class that automatically sends analytics events to the Analytics singleton
when a button is clicked.
@see Analytics, AnalyticsDestination::AnalyticsEvent
@tags{Analytics}
*/
class JUCE_API ButtonTracker : private Button::Listener
{
public:
//==============================================================================
/**
Creating one of these automatically sends analytics events to the Analytics
singleton when the corresponding button is clicked.
The name and parameters of the analytics event will be populated from the
variables supplied here. If clicking changes the button's state then the
parameters will have a {"ButtonState", "On"/"Off"} entry added.
@param buttonToTrack the button to track
@param triggeredEventName the name of the generated event
@param triggeredEventParameters the parameters to add to the generated
event
@param triggeredEventType (optional) an integer to indicate the event
type, which will be set to 0 if not supplied.
@see Analytics, AnalyticsDestination::AnalyticsEvent
*/
ButtonTracker (Button& buttonToTrack,
const String& triggeredEventName,
const StringPairArray& triggeredEventParameters = {},
int triggeredEventType = 0);
/** Destructor. */
~ButtonTracker() override;
private:
/** @internal */
void buttonClicked (Button*) override;
Button& button;
const String eventName;
const StringPairArray eventParameters;
const int eventType;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ButtonTracker)
};
} // namespace juce

View File

@@ -1,97 +1,97 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An interface for handling analytics events collected by an Analytics object.
For basic analytics logging you can implement this interface and add your
class to an Analytics object.
For more advanced logging you may want to subclass
ThreadedAnalyticsDestination instead, which is more suitable for interacting
with web servers and other time consuming destinations.
@see Analytics, ThreadedAnalyticsDestination
@tags{Analytics}
*/
struct JUCE_API AnalyticsDestination
{
/** Contains information about an event to be logged. */
struct AnalyticsEvent
{
/** The name of the event. */
String name;
/** An optional integer representing the type of the event. You can use
this to indicate if the event was a screenview, session start,
exception, etc.
*/
int eventType;
/**
The timestamp of the event.
Timestamps are automatically applied by an Analytics object and are
derived from Time::getMillisecondCounter(). As such these timestamps
do not represent absolute times, but relative timings of events for
each user in each session will be accurate.
*/
uint32 timestamp;
/** The parameters of the event. */
StringPairArray parameters;
/** The user ID associated with the event. */
String userID;
/** Properties associated with the user. */
StringPairArray userProperties;
};
/** Constructor. */
AnalyticsDestination() = default;
/** Destructor. */
virtual ~AnalyticsDestination() = default;
/**
When an AnalyticsDestination is added to an Analytics object this method
is called when an analytics event is triggered from the Analytics
object.
Override this method to log the event information somewhere useful.
*/
virtual void logEvent (const AnalyticsEvent& event) = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyticsDestination)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An interface for handling analytics events collected by an Analytics object.
For basic analytics logging you can implement this interface and add your
class to an Analytics object.
For more advanced logging you may want to subclass
ThreadedAnalyticsDestination instead, which is more suitable for interacting
with web servers and other time consuming destinations.
@see Analytics, ThreadedAnalyticsDestination
@tags{Analytics}
*/
struct JUCE_API AnalyticsDestination
{
/** Contains information about an event to be logged. */
struct AnalyticsEvent
{
/** The name of the event. */
String name;
/** An optional integer representing the type of the event. You can use
this to indicate if the event was a screenview, session start,
exception, etc.
*/
int eventType;
/**
The timestamp of the event.
Timestamps are automatically applied by an Analytics object and are
derived from Time::getMillisecondCounter(). As such these timestamps
do not represent absolute times, but relative timings of events for
each user in each session will be accurate.
*/
uint32 timestamp;
/** The parameters of the event. */
StringPairArray parameters;
/** The user ID associated with the event. */
String userID;
/** Properties associated with the user. */
StringPairArray userProperties;
};
/** Constructor. */
AnalyticsDestination() = default;
/** Destructor. */
virtual ~AnalyticsDestination() = default;
/**
When an AnalyticsDestination is added to an Analytics object this method
is called when an analytics event is triggered from the Analytics
object.
Override this method to log the event information somewhere useful.
*/
virtual void logEvent (const AnalyticsEvent& event) = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyticsDestination)
};
} // namespace juce

View File

@@ -1,291 +1,291 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ThreadedAnalyticsDestination::ThreadedAnalyticsDestination (const String& threadName)
: dispatcher (threadName, *this)
{}
ThreadedAnalyticsDestination::~ThreadedAnalyticsDestination()
{
// If you hit this assertion then the analytics thread has not been shut down
// before this class is destroyed. Call stopAnalyticsThread() in your destructor!
jassert (! dispatcher.isThreadRunning());
}
void ThreadedAnalyticsDestination::setBatchPeriod (int newBatchPeriodMilliseconds)
{
dispatcher.batchPeriodMilliseconds = newBatchPeriodMilliseconds;
}
void ThreadedAnalyticsDestination::logEvent (const AnalyticsEvent& event)
{
dispatcher.addToQueue (event);
}
void ThreadedAnalyticsDestination::startAnalyticsThread (int initialBatchPeriodMilliseconds)
{
setBatchPeriod (initialBatchPeriodMilliseconds);
dispatcher.startThread();
}
void ThreadedAnalyticsDestination::stopAnalyticsThread (int timeout)
{
dispatcher.signalThreadShouldExit();
stopLoggingEvents();
dispatcher.stopThread (timeout);
if (dispatcher.eventQueue.size() > 0)
saveUnloggedEvents (dispatcher.eventQueue);
}
ThreadedAnalyticsDestination::EventDispatcher::EventDispatcher (const String& dispatcherThreadName,
ThreadedAnalyticsDestination& destination)
: Thread (dispatcherThreadName),
parent (destination)
{}
void ThreadedAnalyticsDestination::EventDispatcher::run()
{
// We may have inserted some events into the queue (on the message thread)
// before this thread has started, so make sure the old events are at the
// front of the queue.
{
std::deque<AnalyticsEvent> restoredEventQueue;
parent.restoreUnloggedEvents (restoredEventQueue);
const ScopedLock lock (queueAccess);
for (auto rit = restoredEventQueue.rbegin(); rit != restoredEventQueue.rend(); ++rit)
eventQueue.push_front (*rit);
}
const int maxBatchSize = parent.getMaximumBatchSize();
while (! threadShouldExit())
{
{
const ScopedLock lock (queueAccess);
const auto numEventsInBatch = eventsToSend.size();
const auto freeBatchCapacity = maxBatchSize - numEventsInBatch;
if (freeBatchCapacity > 0)
{
const auto numNewEvents = (int) eventQueue.size() - numEventsInBatch;
if (numNewEvents > 0)
{
const auto numEventsToAdd = jmin (numNewEvents, freeBatchCapacity);
const auto newBatchSize = numEventsInBatch + numEventsToAdd;
for (auto i = numEventsInBatch; i < newBatchSize; ++i)
eventsToSend.add (eventQueue[(size_t) i]);
}
}
}
const auto submissionTime = Time::getMillisecondCounter();
if (! eventsToSend.isEmpty())
{
if (parent.logBatchedEvents (eventsToSend))
{
const ScopedLock lock (queueAccess);
for (auto i = 0; i < eventsToSend.size(); ++i)
eventQueue.pop_front();
eventsToSend.clearQuick();
}
}
while (Time::getMillisecondCounter() - submissionTime < (uint32) batchPeriodMilliseconds.get())
{
if (threadShouldExit())
return;
Thread::sleep (100);
}
}
}
void ThreadedAnalyticsDestination::EventDispatcher::addToQueue (const AnalyticsEvent& event)
{
const ScopedLock lock (queueAccess);
eventQueue.push_back (event);
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
namespace DestinationTestHelpers
{
//==============================================================================
struct BasicDestination : public ThreadedAnalyticsDestination
{
BasicDestination (std::deque<AnalyticsEvent>& loggedEvents,
std::deque<AnalyticsEvent>& unloggedEvents)
: ThreadedAnalyticsDestination ("ThreadedAnalyticsDestinationTest"),
loggedEventQueue (loggedEvents),
unloggedEventStore (unloggedEvents)
{
startAnalyticsThread (20);
}
~BasicDestination() override
{
stopAnalyticsThread (1000);
}
int getMaximumBatchSize() override
{
return 5;
}
void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) override
{
unloggedEventStore = eventsToSave;
}
void restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) override
{
restoredEventQueue = unloggedEventStore;
}
bool logBatchedEvents (const Array<AnalyticsEvent>& events) override
{
jassert (events.size() <= getMaximumBatchSize());
if (loggingIsEnabled)
{
const ScopedLock lock (eventQueueChanging);
for (auto& event : events)
loggedEventQueue.push_back (event);
return true;
}
return false;
}
void stopLoggingEvents() override {}
void setLoggingEnabled (bool shouldLogEvents)
{
loggingIsEnabled = shouldLogEvents;
}
std::deque<AnalyticsEvent>& loggedEventQueue;
std::deque<AnalyticsEvent>& unloggedEventStore;
bool loggingIsEnabled = true;
CriticalSection eventQueueChanging;
};
}
//==============================================================================
struct ThreadedAnalyticsDestinationTests : public UnitTest
{
ThreadedAnalyticsDestinationTests()
: UnitTest ("ThreadedAnalyticsDestination", UnitTestCategories::analytics)
{}
void compareEventQueues (const std::deque<AnalyticsDestination::AnalyticsEvent>& a,
const std::deque<AnalyticsDestination::AnalyticsEvent>& b)
{
const auto numEntries = a.size();
expectEquals ((int) b.size(), (int) numEntries);
for (size_t i = 0; i < numEntries; ++i)
{
expectEquals (a[i].name, b[i].name);
expect (a[i].timestamp == b[i].timestamp);
}
}
void runTest() override
{
std::deque<AnalyticsDestination::AnalyticsEvent> testEvents;
for (int i = 0; i < 7; ++i)
testEvents.push_back ({ String (i), 0, Time::getMillisecondCounter(), {}, "TestUser", {} });
std::deque<AnalyticsDestination::AnalyticsEvent> loggedEvents, unloggedEvents;
beginTest ("New events");
{
DestinationTestHelpers::BasicDestination destination (loggedEvents, unloggedEvents);
for (auto& event : testEvents)
destination.logEvent (event);
size_t waitTime = 0, numLoggedEvents = 0;
while (numLoggedEvents < testEvents.size())
{
if (waitTime > 4000)
{
expect (waitTime < 4000);
break;
}
Thread::sleep (40);
waitTime += 40;
const ScopedLock lock (destination.eventQueueChanging);
numLoggedEvents = loggedEvents.size();
}
}
compareEventQueues (loggedEvents, testEvents);
expect (unloggedEvents.size() == 0);
loggedEvents.clear();
beginTest ("Unlogged events");
{
DestinationTestHelpers::BasicDestination destination (loggedEvents, unloggedEvents);
destination.setLoggingEnabled (false);
for (auto& event : testEvents)
destination.logEvent (event);
}
compareEventQueues (unloggedEvents, testEvents);
expect (loggedEvents.size() == 0);
}
};
static ThreadedAnalyticsDestinationTests threadedAnalyticsDestinationTests;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ThreadedAnalyticsDestination::ThreadedAnalyticsDestination (const String& threadName)
: dispatcher (threadName, *this)
{}
ThreadedAnalyticsDestination::~ThreadedAnalyticsDestination()
{
// If you hit this assertion then the analytics thread has not been shut down
// before this class is destroyed. Call stopAnalyticsThread() in your destructor!
jassert (! dispatcher.isThreadRunning());
}
void ThreadedAnalyticsDestination::setBatchPeriod (int newBatchPeriodMilliseconds)
{
dispatcher.batchPeriodMilliseconds = newBatchPeriodMilliseconds;
}
void ThreadedAnalyticsDestination::logEvent (const AnalyticsEvent& event)
{
dispatcher.addToQueue (event);
}
void ThreadedAnalyticsDestination::startAnalyticsThread (int initialBatchPeriodMilliseconds)
{
setBatchPeriod (initialBatchPeriodMilliseconds);
dispatcher.startThread();
}
void ThreadedAnalyticsDestination::stopAnalyticsThread (int timeout)
{
dispatcher.signalThreadShouldExit();
stopLoggingEvents();
dispatcher.stopThread (timeout);
if (dispatcher.eventQueue.size() > 0)
saveUnloggedEvents (dispatcher.eventQueue);
}
ThreadedAnalyticsDestination::EventDispatcher::EventDispatcher (const String& dispatcherThreadName,
ThreadedAnalyticsDestination& destination)
: Thread (dispatcherThreadName),
parent (destination)
{}
void ThreadedAnalyticsDestination::EventDispatcher::run()
{
// We may have inserted some events into the queue (on the message thread)
// before this thread has started, so make sure the old events are at the
// front of the queue.
{
std::deque<AnalyticsEvent> restoredEventQueue;
parent.restoreUnloggedEvents (restoredEventQueue);
const ScopedLock lock (queueAccess);
for (auto rit = restoredEventQueue.rbegin(); rit != restoredEventQueue.rend(); ++rit)
eventQueue.push_front (*rit);
}
const int maxBatchSize = parent.getMaximumBatchSize();
while (! threadShouldExit())
{
{
const ScopedLock lock (queueAccess);
const auto numEventsInBatch = eventsToSend.size();
const auto freeBatchCapacity = maxBatchSize - numEventsInBatch;
if (freeBatchCapacity > 0)
{
const auto numNewEvents = (int) eventQueue.size() - numEventsInBatch;
if (numNewEvents > 0)
{
const auto numEventsToAdd = jmin (numNewEvents, freeBatchCapacity);
const auto newBatchSize = numEventsInBatch + numEventsToAdd;
for (auto i = numEventsInBatch; i < newBatchSize; ++i)
eventsToSend.add (eventQueue[(size_t) i]);
}
}
}
const auto submissionTime = Time::getMillisecondCounter();
if (! eventsToSend.isEmpty())
{
if (parent.logBatchedEvents (eventsToSend))
{
const ScopedLock lock (queueAccess);
for (auto i = 0; i < eventsToSend.size(); ++i)
eventQueue.pop_front();
eventsToSend.clearQuick();
}
}
while (Time::getMillisecondCounter() - submissionTime < (uint32) batchPeriodMilliseconds.get())
{
if (threadShouldExit())
return;
Thread::sleep (100);
}
}
}
void ThreadedAnalyticsDestination::EventDispatcher::addToQueue (const AnalyticsEvent& event)
{
const ScopedLock lock (queueAccess);
eventQueue.push_back (event);
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
namespace DestinationTestHelpers
{
//==============================================================================
struct BasicDestination : public ThreadedAnalyticsDestination
{
BasicDestination (std::deque<AnalyticsEvent>& loggedEvents,
std::deque<AnalyticsEvent>& unloggedEvents)
: ThreadedAnalyticsDestination ("ThreadedAnalyticsDestinationTest"),
loggedEventQueue (loggedEvents),
unloggedEventStore (unloggedEvents)
{
startAnalyticsThread (20);
}
~BasicDestination() override
{
stopAnalyticsThread (1000);
}
int getMaximumBatchSize() override
{
return 5;
}
void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) override
{
unloggedEventStore = eventsToSave;
}
void restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) override
{
restoredEventQueue = unloggedEventStore;
}
bool logBatchedEvents (const Array<AnalyticsEvent>& events) override
{
jassert (events.size() <= getMaximumBatchSize());
if (loggingIsEnabled)
{
const ScopedLock lock (eventQueueChanging);
for (auto& event : events)
loggedEventQueue.push_back (event);
return true;
}
return false;
}
void stopLoggingEvents() override {}
void setLoggingEnabled (bool shouldLogEvents)
{
loggingIsEnabled = shouldLogEvents;
}
std::deque<AnalyticsEvent>& loggedEventQueue;
std::deque<AnalyticsEvent>& unloggedEventStore;
bool loggingIsEnabled = true;
CriticalSection eventQueueChanging;
};
}
//==============================================================================
struct ThreadedAnalyticsDestinationTests : public UnitTest
{
ThreadedAnalyticsDestinationTests()
: UnitTest ("ThreadedAnalyticsDestination", UnitTestCategories::analytics)
{}
void compareEventQueues (const std::deque<AnalyticsDestination::AnalyticsEvent>& a,
const std::deque<AnalyticsDestination::AnalyticsEvent>& b)
{
const auto numEntries = a.size();
expectEquals ((int) b.size(), (int) numEntries);
for (size_t i = 0; i < numEntries; ++i)
{
expectEquals (a[i].name, b[i].name);
expect (a[i].timestamp == b[i].timestamp);
}
}
void runTest() override
{
std::deque<AnalyticsDestination::AnalyticsEvent> testEvents;
for (int i = 0; i < 7; ++i)
testEvents.push_back ({ String (i), 0, Time::getMillisecondCounter(), {}, "TestUser", {} });
std::deque<AnalyticsDestination::AnalyticsEvent> loggedEvents, unloggedEvents;
beginTest ("New events");
{
DestinationTestHelpers::BasicDestination destination (loggedEvents, unloggedEvents);
for (auto& event : testEvents)
destination.logEvent (event);
size_t waitTime = 0, numLoggedEvents = 0;
while (numLoggedEvents < testEvents.size())
{
if (waitTime > 4000)
{
expect (waitTime < 4000);
break;
}
Thread::sleep (40);
waitTime += 40;
const ScopedLock lock (destination.eventQueueChanging);
numLoggedEvents = loggedEvents.size();
}
}
compareEventQueues (loggedEvents, testEvents);
expect (unloggedEvents.size() == 0);
loggedEvents.clear();
beginTest ("Unlogged events");
{
DestinationTestHelpers::BasicDestination destination (loggedEvents, unloggedEvents);
destination.setLoggingEnabled (false);
for (auto& event : testEvents)
destination.logEvent (event);
}
compareEventQueues (unloggedEvents, testEvents);
expect (loggedEvents.size() == 0);
}
};
static ThreadedAnalyticsDestinationTests threadedAnalyticsDestinationTests;
#endif
} // namespace juce

View File

@@ -1,217 +1,217 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A base class for dispatching analytics events on a dedicated thread.
This class is particularly useful for sending analytics events to a web
server without blocking the message thread. It can also save (and restore)
events that were not dispatched so no information is lost when an internet
connection is absent or something else prevents successful logging.
Once startAnalyticsThread is called the logBatchedEvents method is
periodically invoked on an analytics thread, with the period determined by
calls to setBatchPeriod. Here events are grouped together into batches, with
the maximum batch size set by the implementation of getMaximumBatchSize.
It's important to call stopAnalyticsThread in the destructor of your
subclass (or before then) to give the analytics thread time to shut down.
Calling stopAnalyticsThread will, in turn, call stopLoggingEvents, which
you should use to terminate the currently running logBatchedEvents call.
@see Analytics, AnalyticsDestination, AnalyticsDestination::AnalyticsEvent
@tags{Analytics}
*/
class JUCE_API ThreadedAnalyticsDestination : public AnalyticsDestination
{
public:
//==============================================================================
/**
Creates a ThreadedAnalyticsDestination.
@param threadName used to identify the analytics
thread in debug builds
*/
ThreadedAnalyticsDestination (const String& threadName = "Analytics thread");
/** Destructor. */
~ThreadedAnalyticsDestination() override;
//==============================================================================
/**
Override this method to provide the maximum batch size you can handle in
your subclass.
Calls to logBatchedEvents will contain no more than this number of events.
*/
virtual int getMaximumBatchSize() = 0;
/**
This method will be called periodically on the analytics thread.
If this method returns false then the subsequent call of this function will
contain the same events as previous call, plus any new events that have been
generated in the period between calls. The order of events will not be
changed. This allows you to retry logging events until they are logged
successfully.
@param events a list of events to be logged
@returns if the events were successfully logged
*/
virtual bool logBatchedEvents (const Array<AnalyticsEvent>& events) = 0;
/**
You must always call stopAnalyticsThread in the destructor of your subclass
(or before then) to give the analytics thread time to shut down.
Calling stopAnalyticsThread triggers a call to this method. At this point
you are guaranteed that logBatchedEvents has been called for the last time
and you should make sure that the current call to logBatchedEvents finishes
as quickly as possible. This method and a subsequent call to
saveUnloggedEvents must both complete before the timeout supplied to
stopAnalyticsThread.
In a normal use case stopLoggingEvents will be called on the message thread
from the destructor of your ThreadedAnalyticsDestination subclass, and must
stop the logBatchedEvents method which is running on the analytics thread.
@see stopAnalyticsThread
*/
virtual void stopLoggingEvents() = 0;
//==============================================================================
/**
Call this to set the period between logBatchedEvents invocations.
This method is thread safe and can be used to implements things like
exponential backoff in logBatchedEvents calls.
@param newSubmissionPeriodMilliseconds the new submission period to
use in milliseconds
*/
void setBatchPeriod (int newSubmissionPeriodMilliseconds);
/**
Adds an event to the queue, which will ultimately be submitted to
logBatchedEvents.
This method is thread safe.
@param event the analytics event to add to the queue
*/
void logEvent (const AnalyticsEvent& event) override final;
protected:
//==============================================================================
/**
Starts the analytics thread, with an initial event batching period.
@param initialBatchPeriodMilliseconds the initial event batching period
in milliseconds
*/
void startAnalyticsThread (int initialBatchPeriodMilliseconds);
//==============================================================================
/**
Triggers the shutdown of the analytics thread.
You must call this method in the destructor of your subclass (or before
then) to give the analytics thread time to shut down.
This method invokes stopLoggingEvents and you should ensure that both the
analytics thread and a call to saveUnloggedEvents are able to finish before
the supplied timeout. This timeout is important because on platforms like
iOS an app is killed if it takes too long to shut down.
@param timeoutMilliseconds the number of milliseconds before
the analytics thread is forcibly
terminated
*/
void stopAnalyticsThread (int timeoutMilliseconds);
private:
//==============================================================================
/**
This method will be called when the analytics thread is shut down,
giving you the chance to save any analytics events that could not be
logged. Once saved these events can be put back into the queue of events
when the ThreadedAnalyticsDestination is recreated via
restoreUnloggedEvents.
This method should return as quickly as possible, as both
stopLoggingEvents and this method need to complete inside the timeout
set in stopAnalyticsThread.
@param eventsToSave the events that could not be logged
@see stopAnalyticsThread, stopLoggingEvents, restoreUnloggedEvents
*/
virtual void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) = 0;
/**
The counterpart to saveUnloggedEvents.
Events added to the event queue provided by this method will be the
first events supplied to logBatchedEvents calls. Use this method to
restore any unlogged events previously stored in a call to
saveUnloggedEvents.
This method is called on the analytics thread.
@param restoredEventQueue place restored events into this queue
@see saveUnloggedEvents
*/
virtual void restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) = 0;
struct EventDispatcher : public Thread
{
EventDispatcher (const String& threadName, ThreadedAnalyticsDestination&);
void run() override;
void addToQueue (const AnalyticsEvent&);
ThreadedAnalyticsDestination& parent;
std::deque<AnalyticsEvent> eventQueue;
CriticalSection queueAccess;
Atomic<int> batchPeriodMilliseconds { 1000 };
Array<AnalyticsEvent> eventsToSend;
};
const String destinationName;
EventDispatcher dispatcher;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ThreadedAnalyticsDestination)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A base class for dispatching analytics events on a dedicated thread.
This class is particularly useful for sending analytics events to a web
server without blocking the message thread. It can also save (and restore)
events that were not dispatched so no information is lost when an internet
connection is absent or something else prevents successful logging.
Once startAnalyticsThread is called the logBatchedEvents method is
periodically invoked on an analytics thread, with the period determined by
calls to setBatchPeriod. Here events are grouped together into batches, with
the maximum batch size set by the implementation of getMaximumBatchSize.
It's important to call stopAnalyticsThread in the destructor of your
subclass (or before then) to give the analytics thread time to shut down.
Calling stopAnalyticsThread will, in turn, call stopLoggingEvents, which
you should use to terminate the currently running logBatchedEvents call.
@see Analytics, AnalyticsDestination, AnalyticsDestination::AnalyticsEvent
@tags{Analytics}
*/
class JUCE_API ThreadedAnalyticsDestination : public AnalyticsDestination
{
public:
//==============================================================================
/**
Creates a ThreadedAnalyticsDestination.
@param threadName used to identify the analytics
thread in debug builds
*/
ThreadedAnalyticsDestination (const String& threadName = "Analytics thread");
/** Destructor. */
~ThreadedAnalyticsDestination() override;
//==============================================================================
/**
Override this method to provide the maximum batch size you can handle in
your subclass.
Calls to logBatchedEvents will contain no more than this number of events.
*/
virtual int getMaximumBatchSize() = 0;
/**
This method will be called periodically on the analytics thread.
If this method returns false then the subsequent call of this function will
contain the same events as previous call, plus any new events that have been
generated in the period between calls. The order of events will not be
changed. This allows you to retry logging events until they are logged
successfully.
@param events a list of events to be logged
@returns if the events were successfully logged
*/
virtual bool logBatchedEvents (const Array<AnalyticsEvent>& events) = 0;
/**
You must always call stopAnalyticsThread in the destructor of your subclass
(or before then) to give the analytics thread time to shut down.
Calling stopAnalyticsThread triggers a call to this method. At this point
you are guaranteed that logBatchedEvents has been called for the last time
and you should make sure that the current call to logBatchedEvents finishes
as quickly as possible. This method and a subsequent call to
saveUnloggedEvents must both complete before the timeout supplied to
stopAnalyticsThread.
In a normal use case stopLoggingEvents will be called on the message thread
from the destructor of your ThreadedAnalyticsDestination subclass, and must
stop the logBatchedEvents method which is running on the analytics thread.
@see stopAnalyticsThread
*/
virtual void stopLoggingEvents() = 0;
//==============================================================================
/**
Call this to set the period between logBatchedEvents invocations.
This method is thread safe and can be used to implements things like
exponential backoff in logBatchedEvents calls.
@param newSubmissionPeriodMilliseconds the new submission period to
use in milliseconds
*/
void setBatchPeriod (int newSubmissionPeriodMilliseconds);
/**
Adds an event to the queue, which will ultimately be submitted to
logBatchedEvents.
This method is thread safe.
@param event the analytics event to add to the queue
*/
void logEvent (const AnalyticsEvent& event) override final;
protected:
//==============================================================================
/**
Starts the analytics thread, with an initial event batching period.
@param initialBatchPeriodMilliseconds the initial event batching period
in milliseconds
*/
void startAnalyticsThread (int initialBatchPeriodMilliseconds);
//==============================================================================
/**
Triggers the shutdown of the analytics thread.
You must call this method in the destructor of your subclass (or before
then) to give the analytics thread time to shut down.
This method invokes stopLoggingEvents and you should ensure that both the
analytics thread and a call to saveUnloggedEvents are able to finish before
the supplied timeout. This timeout is important because on platforms like
iOS an app is killed if it takes too long to shut down.
@param timeoutMilliseconds the number of milliseconds before
the analytics thread is forcibly
terminated
*/
void stopAnalyticsThread (int timeoutMilliseconds);
private:
//==============================================================================
/**
This method will be called when the analytics thread is shut down,
giving you the chance to save any analytics events that could not be
logged. Once saved these events can be put back into the queue of events
when the ThreadedAnalyticsDestination is recreated via
restoreUnloggedEvents.
This method should return as quickly as possible, as both
stopLoggingEvents and this method need to complete inside the timeout
set in stopAnalyticsThread.
@param eventsToSave the events that could not be logged
@see stopAnalyticsThread, stopLoggingEvents, restoreUnloggedEvents
*/
virtual void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) = 0;
/**
The counterpart to saveUnloggedEvents.
Events added to the event queue provided by this method will be the
first events supplied to logBatchedEvents calls. Use this method to
restore any unlogged events previously stored in a call to
saveUnloggedEvents.
This method is called on the analytics thread.
@param restoredEventQueue place restored events into this queue
@see saveUnloggedEvents
*/
virtual void restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) = 0;
struct EventDispatcher : public Thread
{
EventDispatcher (const String& threadName, ThreadedAnalyticsDestination&);
void run() override;
void addToQueue (const AnalyticsEvent&);
ThreadedAnalyticsDestination& parent;
std::deque<AnalyticsEvent> eventQueue;
CriticalSection queueAccess;
Atomic<int> batchPeriodMilliseconds { 1000 };
Array<AnalyticsEvent> eventsToSend;
};
const String destinationName;
EventDispatcher dispatcher;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ThreadedAnalyticsDestination)
};
} // namespace juce

View File

@@ -1,39 +1,39 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifdef JUCE_ANALYTICS_H_INCLUDED
/* When you add this cpp file to your project, you mustn't include it in a file where you've
already included any other headers - just put it inside a file on its own, possibly with your config
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
header files that the compiler may be using.
*/
#error "Incorrect use of JUCE cpp file"
#endif
#include "juce_analytics.h"
#include "destinations/juce_ThreadedAnalyticsDestination.cpp"
#include "analytics/juce_Analytics.cpp"
#include "analytics/juce_ButtonTracker.cpp"
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifdef JUCE_ANALYTICS_H_INCLUDED
/* When you add this cpp file to your project, you mustn't include it in a file where you've
already included any other headers - just put it inside a file on its own, possibly with your config
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
header files that the compiler may be using.
*/
#error "Incorrect use of JUCE cpp file"
#endif
#include "juce_analytics.h"
#include "destinations/juce_ThreadedAnalyticsDestination.cpp"
#include "analytics/juce_Analytics.cpp"
#include "analytics/juce_ButtonTracker.cpp"

View File

@@ -1,60 +1,60 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 6 End-User License
Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
End User License Agreement: www.juce.com/juce-6-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE 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 module, and is read by
the Projucer to automatically generate project code that uses it.
For details about the syntax and how to create or use a module, see the
JUCE Module Format.md file.
BEGIN_JUCE_MODULE_DECLARATION
ID: juce_analytics
vendor: juce
version: 6.1.2
name: JUCE analytics classes
description: Classes to collect analytics and send to destinations
website: http://www.juce.com/juce
license: GPL/Commercial
minimumCppStandard: 14
dependencies: juce_gui_basics
END_JUCE_MODULE_DECLARATION
*******************************************************************************/
#pragma once
#define JUCE_ANALYTICS_H_INCLUDED
#include <juce_gui_basics/juce_gui_basics.h>
#include "destinations/juce_AnalyticsDestination.h"
#include "destinations/juce_ThreadedAnalyticsDestination.h"
#include "analytics/juce_Analytics.h"
#include "analytics/juce_ButtonTracker.h"
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
By using JUCE, you agree to the terms of both the JUCE 7 End-User License
Agreement and JUCE Privacy Policy.
End User License Agreement: www.juce.com/juce-7-licence
Privacy Policy: www.juce.com/juce-privacy-policy
Or: You may also use this code under the terms of the GPL v3 (see
www.gnu.org/licenses).
JUCE 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 module, and is read by
the Projucer to automatically generate project code that uses it.
For details about the syntax and how to create or use a module, see the
JUCE Module Format.md file.
BEGIN_JUCE_MODULE_DECLARATION
ID: juce_analytics
vendor: juce
version: 7.0.2
name: JUCE analytics classes
description: Classes to collect analytics and send to destinations
website: http://www.juce.com/juce
license: GPL/Commercial
minimumCppStandard: 14
dependencies: juce_gui_basics
END_JUCE_MODULE_DECLARATION
*******************************************************************************/
#pragma once
#define JUCE_ANALYTICS_H_INCLUDED
#include <juce_gui_basics/juce_gui_basics.h>
#include "destinations/juce_AnalyticsDestination.h"
#include "destinations/juce_ThreadedAnalyticsDestination.h"
#include "analytics/juce_Analytics.h"
#include "analytics/juce_ButtonTracker.h"

View File

@@ -1,184 +1,593 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A subclass of AudioPlayHead can supply information about the position and
status of a moving play head during audio playback.
One of these can be supplied to an AudioProcessor object so that it can find
out about the position of the audio that it is rendering.
@see AudioProcessor::setPlayHead, AudioProcessor::getPlayHead
@tags{Audio}
*/
class JUCE_API AudioPlayHead
{
protected:
//==============================================================================
AudioPlayHead() = default;
public:
virtual ~AudioPlayHead() = default;
//==============================================================================
/** Frame rate types. */
enum FrameRateType
{
fps23976 = 0,
fps24 = 1,
fps25 = 2,
fps2997 = 3,
fps30 = 4,
fps2997drop = 5,
fps30drop = 6,
fps60 = 7,
fps60drop = 8,
fpsUnknown = 99
};
//==============================================================================
/** This structure is filled-in by the AudioPlayHead::getCurrentPosition() method.
*/
struct JUCE_API CurrentPositionInfo
{
/** The tempo in BPM */
double bpm = 120.0;
/** Time signature numerator, e.g. the 3 of a 3/4 time sig */
int timeSigNumerator = 4;
/** Time signature denominator, e.g. the 4 of a 3/4 time sig */
int timeSigDenominator = 4;
/** The current play position, in samples from the start of the timeline. */
int64 timeInSamples = 0;
/** The current play position, in seconds from the start of the timeline. */
double timeInSeconds = 0;
/** For timecode, the position of the start of the timeline, in seconds from 00:00:00:00. */
double editOriginTime = 0;
/** The current play position, in units of quarter-notes. */
double ppqPosition = 0;
/** The position of the start of the last bar, in units of quarter-notes.
This is the time from the start of the timeline to the start of the current
bar, in ppq units.
Note - this value may be unavailable on some hosts, e.g. Pro-Tools. If
it's not available, the value will be 0.
*/
double ppqPositionOfLastBarStart = 0;
/** The video frame rate, if applicable. */
FrameRateType frameRate = FrameRateType::fps23976;
/** True if the transport is currently playing. */
bool isPlaying = false;
/** True if the transport is currently recording.
(When isRecording is true, then isPlaying will also be true).
*/
bool isRecording = false;
/** The current cycle start position in units of quarter-notes.
Note that not all hosts or plugin formats may provide this value.
@see isLooping
*/
double ppqLoopStart = 0;
/** The current cycle end position in units of quarter-notes.
Note that not all hosts or plugin formats may provide this value.
@see isLooping
*/
double ppqLoopEnd = 0;
/** True if the transport is currently looping. */
bool isLooping = false;
//==============================================================================
bool operator== (const CurrentPositionInfo& other) const noexcept
{
auto tie = [] (const CurrentPositionInfo& i)
{
return std::tie (i.timeInSamples,
i.ppqPosition,
i.editOriginTime,
i.ppqPositionOfLastBarStart,
i.frameRate,
i.isPlaying,
i.isRecording,
i.bpm,
i.timeSigNumerator,
i.timeSigDenominator,
i.ppqLoopStart,
i.ppqLoopEnd,
i.isLooping);
};
return tie (*this) == tie (other);
}
bool operator!= (const CurrentPositionInfo& other) const noexcept
{
return ! operator== (other);
}
void resetToDefault()
{
*this = CurrentPositionInfo{};
}
};
//==============================================================================
/** Fills-in the given structure with details about the transport's
position at the start of the current processing block. If this method returns
false then the current play head position is not available and the given
structure will be undefined.
You can ONLY call this from your processBlock() method! Calling it at other
times will produce undefined behaviour, as the host may not have any context
in which a time would make sense, and some hosts will almost certainly have
multithreading issues if it's not called on the audio thread.
*/
virtual bool getCurrentPosition (CurrentPositionInfo& result) = 0;
/** Returns true if this object can control the transport. */
virtual bool canControlTransport() { return false; }
/** Starts or stops the audio. */
virtual void transportPlay (bool shouldStartPlaying) { ignoreUnused (shouldStartPlaying); }
/** Starts or stops recording the audio. */
virtual void transportRecord (bool shouldStartRecording) { ignoreUnused (shouldStartRecording); }
/** Rewinds the audio. */
virtual void transportRewind() {}
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A subclass of AudioPlayHead can supply information about the position and
status of a moving play head during audio playback.
One of these can be supplied to an AudioProcessor object so that it can find
out about the position of the audio that it is rendering.
@see AudioProcessor::setPlayHead, AudioProcessor::getPlayHead
@tags{Audio}
*/
class JUCE_API AudioPlayHead
{
protected:
//==============================================================================
AudioPlayHead() = default;
public:
virtual ~AudioPlayHead() = default;
//==============================================================================
/** Frame rate types. */
enum FrameRateType
{
fps23976 = 0,
fps24 = 1,
fps25 = 2,
fps2997 = 3,
fps30 = 4,
fps2997drop = 5,
fps30drop = 6,
fps60 = 7,
fps60drop = 8,
fpsUnknown = 99
};
/** More descriptive frame rate type. */
class JUCE_API FrameRate
{
public:
/** Creates a frame rate with a base rate of 0. */
FrameRate() = default;
/** Creates a FrameRate instance from a FrameRateType. */
FrameRate (FrameRateType type) : FrameRate (fromType (type)) {}
/** Gets the FrameRateType that matches the state of this FrameRate.
Returns fpsUnknown if this FrameRate cannot be represented by any of the
other enum fields.
*/
FrameRateType getType() const
{
switch (base)
{
case 24: return pulldown ? fps23976 : fps24;
case 25: return fps25;
case 30: return pulldown ? (drop ? fps2997drop : fps2997)
: (drop ? fps30drop : fps30);
case 60: return drop ? fps60drop : fps60;
}
return fpsUnknown;
}
/** Returns the plain rate, without taking pulldown into account. */
int getBaseRate() const { return base; }
/** Returns true if drop-frame timecode is in use. */
bool isDrop() const { return drop; }
/** Returns true if the effective framerate is actually equal to the base rate divided by 1.001 */
bool isPullDown() const { return pulldown; }
/** Returns the actual rate described by this object, taking pulldown into account. */
double getEffectiveRate() const { return pulldown ? (double) base / 1.001 : (double) base; }
/** Returns a copy of this object with the specified base rate. */
JUCE_NODISCARD FrameRate withBaseRate (int x) const { return with (&FrameRate::base, x); }
/** Returns a copy of this object with drop frames enabled or disabled, as specified. */
JUCE_NODISCARD FrameRate withDrop (bool x = true) const { return with (&FrameRate::drop, x); }
/** Returns a copy of this object with pulldown enabled or disabled, as specified. */
JUCE_NODISCARD FrameRate withPullDown (bool x = true) const { return with (&FrameRate::pulldown, x); }
/** Returns true if this instance is equal to other. */
bool operator== (const FrameRate& other) const
{
const auto tie = [] (const FrameRate& x) { return std::tie (x.base, x.drop, x.pulldown); };
return tie (*this) == tie (other);
}
/** Returns true if this instance is not equal to other. */
bool operator!= (const FrameRate& other) const { return ! (*this == other); }
private:
static FrameRate fromType (FrameRateType type)
{
switch (type)
{
case fps23976: return FrameRate().withBaseRate (24).withPullDown();
case fps24: return FrameRate().withBaseRate (24);
case fps25: return FrameRate().withBaseRate (25);
case fps2997: return FrameRate().withBaseRate (30).withPullDown();
case fps30: return FrameRate().withBaseRate (30);
case fps2997drop: return FrameRate().withBaseRate (30).withDrop().withPullDown();
case fps30drop: return FrameRate().withBaseRate (30).withDrop();
case fps60: return FrameRate().withBaseRate (60);
case fps60drop: return FrameRate().withBaseRate (60).withDrop();
case fpsUnknown: break;
}
return {};
}
template <typename Member, typename Value>
FrameRate with (Member&& member, Value&& value) const
{
auto copy = *this;
copy.*member = std::forward<Value> (value);
return copy;
}
int base = 0;
bool drop = false, pulldown = false;
};
/** Describes a musical time signature.
@see PositionInfo::getTimeSignature() PositionInfo::setTimeSignature()
*/
struct JUCE_API TimeSignature
{
/** Time signature numerator, e.g. the 3 of a 3/4 time sig */
int numerator = 4;
/** Time signature denominator, e.g. the 4 of a 3/4 time sig */
int denominator = 4;
bool operator== (const TimeSignature& other) const
{
const auto tie = [] (auto& x) { return std::tie (x.numerator, x.denominator); };
return tie (*this) == tie (other);
}
bool operator!= (const TimeSignature& other) const
{
return ! operator== (other);
}
};
/** Holds the begin and end points of a looped region.
@see PositionInfo::getIsLooping() PositionInfo::setIsLooping() PositionInfo::getLoopPoints() PositionInfo::setLoopPoints()
*/
struct JUCE_API LoopPoints
{
/** The current cycle start position in units of quarter-notes. */
double ppqStart = 0;
/** The current cycle end position in units of quarter-notes. */
double ppqEnd = 0;
bool operator== (const LoopPoints& other) const
{
const auto tie = [] (auto& x) { return std::tie (x.ppqStart, x.ppqEnd); };
return tie (*this) == tie (other);
}
bool operator!= (const LoopPoints& other) const
{
return ! operator== (other);
}
};
//==============================================================================
/** This type is deprecated; prefer PositionInfo instead.
Some position info may be unavailable, depending on the host or plugin format.
Unfortunately, CurrentPositionInfo doesn't have any way of differentiating between
default values and values that have been set explicitly.
*/
struct JUCE_API CurrentPositionInfo
{
/** The tempo in BPM */
double bpm = 120.0;
/** Time signature numerator, e.g. the 3 of a 3/4 time sig */
int timeSigNumerator = 4;
/** Time signature denominator, e.g. the 4 of a 3/4 time sig */
int timeSigDenominator = 4;
/** The current play position, in samples from the start of the timeline. */
int64 timeInSamples = 0;
/** The current play position, in seconds from the start of the timeline. */
double timeInSeconds = 0;
/** For timecode, the position of the start of the timeline, in seconds from 00:00:00:00. */
double editOriginTime = 0;
/** The current play position, in units of quarter-notes. */
double ppqPosition = 0;
/** The position of the start of the last bar, in units of quarter-notes.
This is the time from the start of the timeline to the start of the current
bar, in ppq units.
Note - this value may be unavailable on some hosts, e.g. Pro-Tools. If
it's not available, the value will be 0.
*/
double ppqPositionOfLastBarStart = 0;
/** The video frame rate, if applicable. */
FrameRate frameRate = FrameRateType::fps23976;
/** True if the transport is currently playing. */
bool isPlaying = false;
/** True if the transport is currently recording.
(When isRecording is true, then isPlaying will also be true).
*/
bool isRecording = false;
/** The current cycle start position in units of quarter-notes.
Note that not all hosts or plugin formats may provide this value.
@see isLooping
*/
double ppqLoopStart = 0;
/** The current cycle end position in units of quarter-notes.
Note that not all hosts or plugin formats may provide this value.
@see isLooping
*/
double ppqLoopEnd = 0;
/** True if the transport is currently looping. */
bool isLooping = false;
//==============================================================================
bool operator== (const CurrentPositionInfo& other) const noexcept
{
const auto tie = [] (const CurrentPositionInfo& i)
{
return std::tie (i.timeInSamples,
i.ppqPosition,
i.editOriginTime,
i.ppqPositionOfLastBarStart,
i.frameRate,
i.isPlaying,
i.isRecording,
i.bpm,
i.timeSigNumerator,
i.timeSigDenominator,
i.ppqLoopStart,
i.ppqLoopEnd,
i.isLooping);
};
return tie (*this) == tie (other);
}
bool operator!= (const CurrentPositionInfo& other) const noexcept
{
return ! operator== (other);
}
void resetToDefault()
{
*this = CurrentPositionInfo{};
}
};
//==============================================================================
/**
Describes the time at the start of the current audio callback.
Not all hosts and plugin formats can provide all of the possible time
information, so most of the getter functions in this class return
an Optional that will only be engaged if the host provides the corresponding
information. As a plugin developer, you should code defensively so that
the plugin behaves sensibly even when the host fails to provide timing
information.
A default-constructed instance of this class will return nullopt from
all functions that return an Optional.
*/
class PositionInfo
{
public:
/** Returns the number of samples that have elapsed. */
Optional<int64_t> getTimeInSamples() const { return getOptional (flagTimeSamples, timeInSamples); }
/** @see getTimeInSamples() */
void setTimeInSamples (Optional<int64_t> timeInSamplesIn) { setOptional (flagTimeSamples, timeInSamples, timeInSamplesIn); }
/** Returns the number of seconds that have elapsed. */
Optional<double> getTimeInSeconds() const { return getOptional (flagTimeSeconds, timeInSeconds); }
/** @see getTimeInSamples() */
void setTimeInSeconds (Optional<double> timeInSecondsIn) { setOptional (flagTimeSeconds, timeInSeconds, timeInSecondsIn); }
/** Returns the bpm, if available. */
Optional<double> getBpm() const { return getOptional (flagTempo, tempoBpm); }
/** @see getBpm() */
void setBpm (Optional<double> bpmIn) { setOptional (flagTempo, tempoBpm, bpmIn); }
/** Returns the time signature, if available. */
Optional<TimeSignature> getTimeSignature() const { return getOptional (flagTimeSignature, timeSignature); }
/** @see getTimeSignature() */
void setTimeSignature (Optional<TimeSignature> timeSignatureIn) { setOptional (flagTimeSignature, timeSignature, timeSignatureIn); }
/** Returns host loop points, if available. */
Optional<LoopPoints> getLoopPoints() const { return getOptional (flagLoopPoints, loopPoints); }
/** @see getLoopPoints() */
void setLoopPoints (Optional<LoopPoints> loopPointsIn) { setOptional (flagLoopPoints, loopPoints, loopPointsIn); }
/** The number of bars since the beginning of the timeline.
This value isn't available in all hosts or in all plugin formats.
*/
Optional<int64_t> getBarCount() const { return getOptional (flagBarCount, barCount); }
/** @see getBarCount() */
void setBarCount (Optional<int64_t> barCountIn) { setOptional (flagBarCount, barCount, barCountIn); }
/** The position of the start of the last bar, in units of quarter-notes.
This is the time from the start of the timeline to the start of the current
bar, in ppq units.
Note - this value may be unavailable on some hosts, e.g. Pro-Tools.
*/
Optional<double> getPpqPositionOfLastBarStart() const { return getOptional (flagLastBarStartPpq, lastBarStartPpq); }
/** @see getPpqPositionOfLastBarStart() */
void setPpqPositionOfLastBarStart (Optional<double> positionIn) { setOptional (flagLastBarStartPpq, lastBarStartPpq, positionIn); }
/** The video frame rate, if available. */
Optional<FrameRate> getFrameRate() const { return getOptional (flagFrameRate, frame); }
/** @see getFrameRate() */
void setFrameRate (Optional<FrameRate> frameRateIn) { setOptional (flagFrameRate, frame, frameRateIn); }
/** The current play position, in units of quarter-notes. */
Optional<double> getPpqPosition() const { return getOptional (flagPpqPosition, positionPpq); }
/** @see getPpqPosition() */
void setPpqPosition (Optional<double> ppqPositionIn) { setOptional (flagPpqPosition, positionPpq, ppqPositionIn); }
/** For timecode, the position of the start of the timeline, in seconds from 00:00:00:00. */
Optional<double> getEditOriginTime() const { return getOptional (flagOriginTime, originTime); }
/** @see getEditOriginTime() */
void setEditOriginTime (Optional<double> editOriginTimeIn) { setOptional (flagOriginTime, originTime, editOriginTimeIn); }
/** Get the host's callback time in nanoseconds, if available. */
Optional<uint64_t> getHostTimeNs() const { return getOptional (flagHostTimeNs, hostTimeNs); }
/** @see getHostTimeNs() */
void setHostTimeNs (Optional<uint64_t> hostTimeNsIn) { setOptional (flagHostTimeNs, hostTimeNs, hostTimeNsIn); }
/** True if the transport is currently playing. */
bool getIsPlaying() const { return getFlag (flagIsPlaying); }
/** @see getIsPlaying() */
void setIsPlaying (bool isPlayingIn) { setFlag (flagIsPlaying, isPlayingIn); }
/** True if the transport is currently recording.
(When isRecording is true, then isPlaying will also be true).
*/
bool getIsRecording() const { return getFlag (flagIsRecording); }
/** @see getIsRecording() */
void setIsRecording (bool isRecordingIn) { setFlag (flagIsRecording, isRecordingIn); }
/** True if the transport is currently looping. */
bool getIsLooping() const { return getFlag (flagIsLooping); }
/** @see getIsLooping() */
void setIsLooping (bool isLoopingIn) { setFlag (flagIsLooping, isLoopingIn); }
bool operator== (const PositionInfo& other) const noexcept
{
const auto tie = [] (const PositionInfo& i)
{
return std::make_tuple (i.getTimeInSamples(),
i.getTimeInSeconds(),
i.getPpqPosition(),
i.getEditOriginTime(),
i.getPpqPositionOfLastBarStart(),
i.getFrameRate(),
i.getBarCount(),
i.getTimeSignature(),
i.getBpm(),
i.getLoopPoints(),
i.getHostTimeNs(),
i.getIsPlaying(),
i.getIsRecording(),
i.getIsLooping());
};
return tie (*this) == tie (other);
}
bool operator!= (const PositionInfo& other) const noexcept
{
return ! operator== (other);
}
private:
bool getFlag (int64_t flagToCheck) const
{
return (flagToCheck & flags) != 0;
}
void setFlag (int64_t flagToCheck, bool value)
{
flags = (value ? flags | flagToCheck : flags & ~flagToCheck);
}
template <typename Value>
Optional<Value> getOptional (int64_t flagToCheck, Value value) const
{
return getFlag (flagToCheck) ? makeOptional (std::move (value)) : nullopt;
}
template <typename Value>
void setOptional (int64_t flagToCheck, Value& value, Optional<Value> opt)
{
if (opt.hasValue())
value = *opt;
setFlag (flagToCheck, opt.hasValue());
}
enum
{
flagTimeSignature = 1 << 0,
flagLoopPoints = 1 << 1,
flagFrameRate = 1 << 2,
flagTimeSeconds = 1 << 3,
flagLastBarStartPpq = 1 << 4,
flagPpqPosition = 1 << 5,
flagOriginTime = 1 << 6,
flagTempo = 1 << 7,
flagTimeSamples = 1 << 8,
flagBarCount = 1 << 9,
flagHostTimeNs = 1 << 10,
flagIsPlaying = 1 << 11,
flagIsRecording = 1 << 12,
flagIsLooping = 1 << 13
};
TimeSignature timeSignature;
LoopPoints loopPoints;
FrameRate frame = FrameRateType::fps23976;
double timeInSeconds = 0.0;
double lastBarStartPpq = 0.0;
double positionPpq = 0.0;
double originTime = 0.0;
double tempoBpm = 0.0;
int64_t timeInSamples = 0;
int64_t barCount = 0;
uint64_t hostTimeNs = 0;
int64_t flags = 0;
};
//==============================================================================
/** Deprecated, use getPosition() instead.
Fills-in the given structure with details about the transport's
position at the start of the current processing block. If this method returns
false then the current play head position is not available and the given
structure will be undefined.
You can ONLY call this from your processBlock() method! Calling it at other
times will produce undefined behaviour, as the host may not have any context
in which a time would make sense, and some hosts will almost certainly have
multithreading issues if it's not called on the audio thread.
*/
[[deprecated ("Use getPosition instead. Not all hosts are able to provide all time position information; getPosition differentiates clearly between set and unset fields.")]]
bool getCurrentPosition (CurrentPositionInfo& result)
{
if (const auto pos = getPosition())
{
result.resetToDefault();
if (const auto sig = pos->getTimeSignature())
{
result.timeSigNumerator = sig->numerator;
result.timeSigDenominator = sig->denominator;
}
if (const auto loop = pos->getLoopPoints())
{
result.ppqLoopStart = loop->ppqStart;
result.ppqLoopEnd = loop->ppqEnd;
}
if (const auto frame = pos->getFrameRate())
result.frameRate = *frame;
if (const auto timeInSeconds = pos->getTimeInSeconds())
result.timeInSeconds = *timeInSeconds;
if (const auto lastBarStartPpq = pos->getPpqPositionOfLastBarStart())
result.ppqPositionOfLastBarStart = *lastBarStartPpq;
if (const auto ppqPosition = pos->getPpqPosition())
result.ppqPosition = *ppqPosition;
if (const auto originTime = pos->getEditOriginTime())
result.editOriginTime = *originTime;
if (const auto bpm = pos->getBpm())
result.bpm = *bpm;
if (const auto timeInSamples = pos->getTimeInSamples())
result.timeInSamples = *timeInSamples;
result.isPlaying = pos->getIsPlaying();
result.isRecording = pos->getIsRecording();
result.isLooping = pos->getIsLooping();
return true;
}
return false;
}
/** Fetches details about the transport's position at the start of the current
processing block. If this method returns nullopt then the current play head
position is not available.
A non-null return value just indicates that the host was able to provide
*some* relevant timing information. Individual PositionInfo getters may
still return nullopt.
You can ONLY call this from your processBlock() method! Calling it at other
times will produce undefined behaviour, as the host may not have any context
in which a time would make sense, and some hosts will almost certainly have
multithreading issues if it's not called on the audio thread.
*/
virtual Optional<PositionInfo> getPosition() const = 0;
/** Returns true if this object can control the transport. */
virtual bool canControlTransport() { return false; }
/** Starts or stops the audio. */
virtual void transportPlay (bool shouldStartPlaying) { ignoreUnused (shouldStartPlaying); }
/** Starts or stops recording the audio. */
virtual void transportRecord (bool shouldStartRecording) { ignoreUnused (shouldStartRecording); }
/** Rewinds the audio. */
virtual void transportRewind() {}
};
} // namespace juce

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,87 +1,99 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
AudioProcessLoadMeasurer::AudioProcessLoadMeasurer() = default;
AudioProcessLoadMeasurer::~AudioProcessLoadMeasurer() = default;
void AudioProcessLoadMeasurer::reset()
{
reset (0, 0);
}
void AudioProcessLoadMeasurer::reset (double sampleRate, int blockSize)
{
cpuUsageProportion = 0;
xruns = 0;
if (sampleRate > 0.0 && blockSize > 0)
{
msPerSample = 1000.0 / sampleRate;
timeToCpuScale = (msPerSample > 0.0) ? (1.0 / msPerSample) : 0.0;
}
else
{
msPerSample = 0;
timeToCpuScale = 0;
}
}
void AudioProcessLoadMeasurer::registerBlockRenderTime (double milliseconds)
{
registerRenderTime (milliseconds, samplesPerBlock);
}
void AudioProcessLoadMeasurer::registerRenderTime (double milliseconds, int numSamples)
{
const auto maxMilliseconds = numSamples * msPerSample;
const auto usedProportion = milliseconds / maxMilliseconds;
const auto filterAmount = 0.2;
cpuUsageProportion += filterAmount * (usedProportion - cpuUsageProportion);
if (milliseconds > maxMilliseconds)
++xruns;
}
double AudioProcessLoadMeasurer::getLoadAsProportion() const { return jlimit (0.0, 1.0, cpuUsageProportion); }
double AudioProcessLoadMeasurer::getLoadAsPercentage() const { return 100.0 * getLoadAsProportion(); }
int AudioProcessLoadMeasurer::getXRunCount() const { return xruns; }
AudioProcessLoadMeasurer::ScopedTimer::ScopedTimer (AudioProcessLoadMeasurer& p)
: ScopedTimer (p, p.samplesPerBlock)
{
}
AudioProcessLoadMeasurer::ScopedTimer::ScopedTimer (AudioProcessLoadMeasurer& p, int numSamplesInBlock)
: owner (p), startTime (Time::getMillisecondCounterHiRes()), samplesInBlock (numSamplesInBlock)
{
}
AudioProcessLoadMeasurer::ScopedTimer::~ScopedTimer()
{
owner.registerRenderTime (Time::getMillisecondCounterHiRes() - startTime, samplesInBlock);
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
AudioProcessLoadMeasurer::AudioProcessLoadMeasurer() = default;
AudioProcessLoadMeasurer::~AudioProcessLoadMeasurer() = default;
void AudioProcessLoadMeasurer::reset()
{
reset (0, 0);
}
void AudioProcessLoadMeasurer::reset (double sampleRate, int blockSize)
{
const SpinLock::ScopedLockType lock (mutex);
cpuUsageProportion = 0;
xruns = 0;
samplesPerBlock = blockSize;
msPerSample = (sampleRate > 0.0 && blockSize > 0) ? 1000.0 / sampleRate : 0;
}
void AudioProcessLoadMeasurer::registerBlockRenderTime (double milliseconds)
{
const SpinLock::ScopedTryLockType lock (mutex);
if (lock.isLocked())
registerRenderTimeLocked (milliseconds, samplesPerBlock);
}
void AudioProcessLoadMeasurer::registerRenderTime (double milliseconds, int numSamples)
{
const SpinLock::ScopedTryLockType lock (mutex);
if (lock.isLocked())
registerRenderTimeLocked (milliseconds, numSamples);
}
void AudioProcessLoadMeasurer::registerRenderTimeLocked (double milliseconds, int numSamples)
{
if (msPerSample == 0)
return;
const auto maxMilliseconds = numSamples * msPerSample;
const auto usedProportion = milliseconds / maxMilliseconds;
const auto filterAmount = 0.2;
const auto proportion = cpuUsageProportion.load();
cpuUsageProportion = proportion + filterAmount * (usedProportion - proportion);
if (milliseconds > maxMilliseconds)
++xruns;
}
double AudioProcessLoadMeasurer::getLoadAsProportion() const { return jlimit (0.0, 1.0, cpuUsageProportion.load()); }
double AudioProcessLoadMeasurer::getLoadAsPercentage() const { return 100.0 * getLoadAsProportion(); }
int AudioProcessLoadMeasurer::getXRunCount() const { return xruns; }
AudioProcessLoadMeasurer::ScopedTimer::ScopedTimer (AudioProcessLoadMeasurer& p)
: ScopedTimer (p, p.samplesPerBlock)
{
}
AudioProcessLoadMeasurer::ScopedTimer::ScopedTimer (AudioProcessLoadMeasurer& p, int numSamplesInBlock)
: owner (p), startTime (Time::getMillisecondCounterHiRes()), samplesInBlock (numSamplesInBlock)
{
// numSamplesInBlock should never be zero. Did you remember to call AudioProcessLoadMeasurer::reset(),
// passing the expected samples per block?
jassert (numSamplesInBlock);
}
AudioProcessLoadMeasurer::ScopedTimer::~ScopedTimer()
{
owner.registerRenderTime (Time::getMillisecondCounterHiRes() - startTime, samplesInBlock);
}
} // namespace juce

View File

@@ -1,104 +1,109 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Maintains an ongoing measurement of the proportion of time which is being
spent inside an audio callback.
@tags{Audio}
*/
class JUCE_API AudioProcessLoadMeasurer
{
public:
/** */
AudioProcessLoadMeasurer();
/** Destructor. */
~AudioProcessLoadMeasurer();
//==============================================================================
/** Resets the state. */
void reset();
/** Resets the counter, in preparation for use with the given sample rate and block size. */
void reset (double sampleRate, int blockSize);
/** Returns the current load as a proportion 0 to 1.0 */
double getLoadAsProportion() const;
/** Returns the current load as a percentage 0 to 100.0 */
double getLoadAsPercentage() const;
/** Returns the number of over- (or under-) runs recorded since the state was reset. */
int getXRunCount() const;
//==============================================================================
/** This class measures the time between its construction and destruction and
adds it to an AudioProcessLoadMeasurer.
e.g.
@code
{
AudioProcessLoadMeasurer::ScopedTimer timer (myProcessLoadMeasurer);
myCallback->doTheCallback();
}
@endcode
@tags{Audio}
*/
struct JUCE_API ScopedTimer
{
ScopedTimer (AudioProcessLoadMeasurer&);
ScopedTimer (AudioProcessLoadMeasurer&, int numSamplesInBlock);
~ScopedTimer();
private:
AudioProcessLoadMeasurer& owner;
double startTime;
int samplesInBlock;
JUCE_DECLARE_NON_COPYABLE (ScopedTimer)
};
/** Can be called manually to add the time of a callback to the stats.
Normally you probably would never call this - it's simpler and more robust to
use a ScopedTimer to measure the time using an RAII pattern.
*/
void registerBlockRenderTime (double millisecondsTaken);
/** Can be called manually to add the time of a callback to the stats.
Normally you probably would never call this - it's simpler and more robust to
use a ScopedTimer to measure the time using an RAII pattern.
*/
void registerRenderTime (double millisecondsTaken, int numSamples);
private:
double cpuUsageProportion = 0, timeToCpuScale = 0, msPerSample = 0;
int xruns = 0, samplesPerBlock = 0;
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Maintains an ongoing measurement of the proportion of time which is being
spent inside an audio callback.
@tags{Audio}
*/
class JUCE_API AudioProcessLoadMeasurer
{
public:
/** */
AudioProcessLoadMeasurer();
/** Destructor. */
~AudioProcessLoadMeasurer();
//==============================================================================
/** Resets the state. */
void reset();
/** Resets the counter, in preparation for use with the given sample rate and block size. */
void reset (double sampleRate, int blockSize);
/** Returns the current load as a proportion 0 to 1.0 */
double getLoadAsProportion() const;
/** Returns the current load as a percentage 0 to 100.0 */
double getLoadAsPercentage() const;
/** Returns the number of over- (or under-) runs recorded since the state was reset. */
int getXRunCount() const;
//==============================================================================
/** This class measures the time between its construction and destruction and
adds it to an AudioProcessLoadMeasurer.
e.g.
@code
{
AudioProcessLoadMeasurer::ScopedTimer timer (myProcessLoadMeasurer);
myCallback->doTheCallback();
}
@endcode
@tags{Audio}
*/
struct JUCE_API ScopedTimer
{
ScopedTimer (AudioProcessLoadMeasurer&);
ScopedTimer (AudioProcessLoadMeasurer&, int numSamplesInBlock);
~ScopedTimer();
private:
AudioProcessLoadMeasurer& owner;
double startTime;
int samplesInBlock;
JUCE_DECLARE_NON_COPYABLE (ScopedTimer)
};
/** Can be called manually to add the time of a callback to the stats.
Normally you probably would never call this - it's simpler and more robust to
use a ScopedTimer to measure the time using an RAII pattern.
*/
void registerBlockRenderTime (double millisecondsTaken);
/** Can be called manually to add the time of a callback to the stats.
Normally you probably would never call this - it's simpler and more robust to
use a ScopedTimer to measure the time using an RAII pattern.
*/
void registerRenderTime (double millisecondsTaken, int numSamples);
private:
void registerRenderTimeLocked (double, int);
SpinLock mutex;
int samplesPerBlock = 0;
double msPerSample = 0;
std::atomic<double> cpuUsageProportion { 0 };
std::atomic<int> xruns { 0 };
};
} // namespace juce

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,257 +1,269 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
#ifndef JUCE_SNAP_TO_ZERO
#if JUCE_INTEL
#define JUCE_SNAP_TO_ZERO(n) if (! (n < -1.0e-8f || n > 1.0e-8f)) n = 0;
#else
#define JUCE_SNAP_TO_ZERO(n) ignoreUnused (n)
#endif
#endif
class ScopedNoDenormals;
//==============================================================================
/**
A collection of simple vector operations on arrays of floats, accelerated with
SIMD instructions where possible.
@tags{Audio}
*/
class JUCE_API FloatVectorOperations
{
public:
//==============================================================================
/** Clears a vector of floats. */
static void JUCE_CALLTYPE clear (float* dest, int numValues) noexcept;
/** Clears a vector of doubles. */
static void JUCE_CALLTYPE clear (double* dest, int numValues) noexcept;
/** Copies a repeated value into a vector of floats. */
static void JUCE_CALLTYPE fill (float* dest, float valueToFill, int numValues) noexcept;
/** Copies a repeated value into a vector of doubles. */
static void JUCE_CALLTYPE fill (double* dest, double valueToFill, int numValues) noexcept;
/** Copies a vector of floats. */
static void JUCE_CALLTYPE copy (float* dest, const float* src, int numValues) noexcept;
/** Copies a vector of doubles. */
static void JUCE_CALLTYPE copy (double* dest, const double* src, int numValues) noexcept;
/** Copies a vector of floats, multiplying each value by a given multiplier */
static void JUCE_CALLTYPE copyWithMultiply (float* dest, const float* src, float multiplier, int numValues) noexcept;
/** Copies a vector of doubles, multiplying each value by a given multiplier */
static void JUCE_CALLTYPE copyWithMultiply (double* dest, const double* src, double multiplier, int numValues) noexcept;
/** Adds a fixed value to the destination values. */
static void JUCE_CALLTYPE add (float* dest, float amountToAdd, int numValues) noexcept;
/** Adds a fixed value to the destination values. */
static void JUCE_CALLTYPE add (double* dest, double amountToAdd, int numValues) noexcept;
/** Adds a fixed value to each source value and stores it in the destination array. */
static void JUCE_CALLTYPE add (float* dest, const float* src, float amount, int numValues) noexcept;
/** Adds a fixed value to each source value and stores it in the destination array. */
static void JUCE_CALLTYPE add (double* dest, const double* src, double amount, int numValues) noexcept;
/** Adds the source values to the destination values. */
static void JUCE_CALLTYPE add (float* dest, const float* src, int numValues) noexcept;
/** Adds the source values to the destination values. */
static void JUCE_CALLTYPE add (double* dest, const double* src, int numValues) noexcept;
/** Adds each source1 value to the corresponding source2 value and stores the result in the destination array. */
static void JUCE_CALLTYPE add (float* dest, const float* src1, const float* src2, int num) noexcept;
/** Adds each source1 value to the corresponding source2 value and stores the result in the destination array. */
static void JUCE_CALLTYPE add (double* dest, const double* src1, const double* src2, int num) noexcept;
/** Subtracts the source values from the destination values. */
static void JUCE_CALLTYPE subtract (float* dest, const float* src, int numValues) noexcept;
/** Subtracts the source values from the destination values. */
static void JUCE_CALLTYPE subtract (double* dest, const double* src, int numValues) noexcept;
/** Subtracts each source2 value from the corresponding source1 value and stores the result in the destination array. */
static void JUCE_CALLTYPE subtract (float* dest, const float* src1, const float* src2, int num) noexcept;
/** Subtracts each source2 value from the corresponding source1 value and stores the result in the destination array. */
static void JUCE_CALLTYPE subtract (double* dest, const double* src1, const double* src2, int num) noexcept;
/** Multiplies each source value by the given multiplier, then adds it to the destination value. */
static void JUCE_CALLTYPE addWithMultiply (float* dest, const float* src, float multiplier, int numValues) noexcept;
/** Multiplies each source value by the given multiplier, then adds it to the destination value. */
static void JUCE_CALLTYPE addWithMultiply (double* dest, const double* src, double multiplier, int numValues) noexcept;
/** Multiplies each source1 value by the corresponding source2 value, then adds it to the destination value. */
static void JUCE_CALLTYPE addWithMultiply (float* dest, const float* src1, const float* src2, int num) noexcept;
/** Multiplies each source1 value by the corresponding source2 value, then adds it to the destination value. */
static void JUCE_CALLTYPE addWithMultiply (double* dest, const double* src1, const double* src2, int num) noexcept;
/** Multiplies each source value by the given multiplier, then subtracts it to the destination value. */
static void JUCE_CALLTYPE subtractWithMultiply (float* dest, const float* src, float multiplier, int numValues) noexcept;
/** Multiplies each source value by the given multiplier, then subtracts it to the destination value. */
static void JUCE_CALLTYPE subtractWithMultiply (double* dest, const double* src, double multiplier, int numValues) noexcept;
/** Multiplies each source1 value by the corresponding source2 value, then subtracts it to the destination value. */
static void JUCE_CALLTYPE subtractWithMultiply (float* dest, const float* src1, const float* src2, int num) noexcept;
/** Multiplies each source1 value by the corresponding source2 value, then subtracts it to the destination value. */
static void JUCE_CALLTYPE subtractWithMultiply (double* dest, const double* src1, const double* src2, int num) noexcept;
/** Multiplies the destination values by the source values. */
static void JUCE_CALLTYPE multiply (float* dest, const float* src, int numValues) noexcept;
/** Multiplies the destination values by the source values. */
static void JUCE_CALLTYPE multiply (double* dest, const double* src, int numValues) noexcept;
/** Multiplies each source1 value by the correspinding source2 value, then stores it in the destination array. */
static void JUCE_CALLTYPE multiply (float* dest, const float* src1, const float* src2, int numValues) noexcept;
/** Multiplies each source1 value by the correspinding source2 value, then stores it in the destination array. */
static void JUCE_CALLTYPE multiply (double* dest, const double* src1, const double* src2, int numValues) noexcept;
/** Multiplies each of the destination values by a fixed multiplier. */
static void JUCE_CALLTYPE multiply (float* dest, float multiplier, int numValues) noexcept;
/** Multiplies each of the destination values by a fixed multiplier. */
static void JUCE_CALLTYPE multiply (double* dest, double multiplier, int numValues) noexcept;
/** Multiplies each of the source values by a fixed multiplier and stores the result in the destination array. */
static void JUCE_CALLTYPE multiply (float* dest, const float* src, float multiplier, int num) noexcept;
/** Multiplies each of the source values by a fixed multiplier and stores the result in the destination array. */
static void JUCE_CALLTYPE multiply (double* dest, const double* src, double multiplier, int num) noexcept;
/** Copies a source vector to a destination, negating each value. */
static void JUCE_CALLTYPE negate (float* dest, const float* src, int numValues) noexcept;
/** Copies a source vector to a destination, negating each value. */
static void JUCE_CALLTYPE negate (double* dest, const double* src, int numValues) noexcept;
/** Copies a source vector to a destination, taking the absolute of each value. */
static void JUCE_CALLTYPE abs (float* dest, const float* src, int numValues) noexcept;
/** Copies a source vector to a destination, taking the absolute of each value. */
static void JUCE_CALLTYPE abs (double* dest, const double* src, int numValues) noexcept;
/** Converts a stream of integers to floats, multiplying each one by the given multiplier. */
static void JUCE_CALLTYPE convertFixedToFloat (float* dest, const int* src, float multiplier, int numValues) noexcept;
/** Each element of dest will be the minimum of the corresponding element of the source array and the given comp value. */
static void JUCE_CALLTYPE min (float* dest, const float* src, float comp, int num) noexcept;
/** Each element of dest will be the minimum of the corresponding element of the source array and the given comp value. */
static void JUCE_CALLTYPE min (double* dest, const double* src, double comp, int num) noexcept;
/** Each element of dest will be the minimum of the corresponding source1 and source2 values. */
static void JUCE_CALLTYPE min (float* dest, const float* src1, const float* src2, int num) noexcept;
/** Each element of dest will be the minimum of the corresponding source1 and source2 values. */
static void JUCE_CALLTYPE min (double* dest, const double* src1, const double* src2, int num) noexcept;
/** Each element of dest will be the maximum of the corresponding element of the source array and the given comp value. */
static void JUCE_CALLTYPE max (float* dest, const float* src, float comp, int num) noexcept;
/** Each element of dest will be the maximum of the corresponding element of the source array and the given comp value. */
static void JUCE_CALLTYPE max (double* dest, const double* src, double comp, int num) noexcept;
/** Each element of dest will be the maximum of the corresponding source1 and source2 values. */
static void JUCE_CALLTYPE max (float* dest, const float* src1, const float* src2, int num) noexcept;
/** Each element of dest will be the maximum of the corresponding source1 and source2 values. */
static void JUCE_CALLTYPE max (double* dest, const double* src1, const double* src2, int num) noexcept;
/** Each element of dest is calculated by hard clipping the corresponding src element so that it is in the range specified by the arguments low and high. */
static void JUCE_CALLTYPE clip (float* dest, const float* src, float low, float high, int num) noexcept;
/** Each element of dest is calculated by hard clipping the corresponding src element so that it is in the range specified by the arguments low and high. */
static void JUCE_CALLTYPE clip (double* dest, const double* src, double low, double high, int num) noexcept;
/** Finds the minimum and maximum values in the given array. */
static Range<float> JUCE_CALLTYPE findMinAndMax (const float* src, int numValues) noexcept;
/** Finds the minimum and maximum values in the given array. */
static Range<double> JUCE_CALLTYPE findMinAndMax (const double* src, int numValues) noexcept;
/** Finds the minimum value in the given array. */
static float JUCE_CALLTYPE findMinimum (const float* src, int numValues) noexcept;
/** Finds the minimum value in the given array. */
static double JUCE_CALLTYPE findMinimum (const double* src, int numValues) noexcept;
/** Finds the maximum value in the given array. */
static float JUCE_CALLTYPE findMaximum (const float* src, int numValues) noexcept;
/** Finds the maximum value in the given array. */
static double JUCE_CALLTYPE findMaximum (const double* src, int numValues) noexcept;
/** This method enables or disables the SSE/NEON flush-to-zero mode. */
static void JUCE_CALLTYPE enableFlushToZeroMode (bool shouldEnable) noexcept;
/** On Intel CPUs, this method enables the SSE flush-to-zero and denormalised-are-zero modes.
This effectively sets the DAZ and FZ bits of the MXCSR register. On arm CPUs this will
enable flush to zero mode.
It's a convenient thing to call before audio processing code where you really want to
avoid denormalisation performance hits.
*/
static void JUCE_CALLTYPE disableDenormalisedNumberSupport (bool shouldDisable = true) noexcept;
/** This method returns true if denormals are currently disabled. */
static bool JUCE_CALLTYPE areDenormalsDisabled() noexcept;
private:
friend ScopedNoDenormals;
static intptr_t JUCE_CALLTYPE getFpStatusRegister() noexcept;
static void JUCE_CALLTYPE setFpStatusRegister (intptr_t) noexcept;
};
//==============================================================================
/**
Helper class providing an RAII-based mechanism for temporarily disabling
denormals on your CPU.
@tags{Audio}
*/
class ScopedNoDenormals
{
public:
ScopedNoDenormals() noexcept;
~ScopedNoDenormals() noexcept;
private:
#if JUCE_USE_SSE_INTRINSICS || (JUCE_USE_ARM_NEON || defined (__arm64__) || defined (__aarch64__))
intptr_t fpsr;
#endif
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
#ifndef JUCE_SNAP_TO_ZERO
#if JUCE_INTEL
#define JUCE_SNAP_TO_ZERO(n) if (! (n < -1.0e-8f || n > 1.0e-8f)) n = 0;
#else
#define JUCE_SNAP_TO_ZERO(n) ignoreUnused (n)
#endif
#endif
class ScopedNoDenormals;
//==============================================================================
/**
A collection of simple vector operations on arrays of floating point numbers,
accelerated with SIMD instructions where possible, usually accessed from
the FloatVectorOperations class.
@code
float data[64];
// The following two function calls are equivalent:
FloatVectorOperationsBase<float, int>::clear (data, 64);
FloatVectorOperations::clear (data, 64);
@endcode
@see FloatVectorOperations
@tags{Audio}
*/
template <typename FloatType, typename CountType>
struct FloatVectorOperationsBase
{
/** Clears a vector of floating point numbers. */
static void JUCE_CALLTYPE clear (FloatType* dest, CountType numValues) noexcept;
/** Copies a repeated value into a vector of floating point numbers. */
static void JUCE_CALLTYPE fill (FloatType* dest, FloatType valueToFill, CountType numValues) noexcept;
/** Copies a vector of floating point numbers. */
static void JUCE_CALLTYPE copy (FloatType* dest, const FloatType* src, CountType numValues) noexcept;
/** Copies a vector of floating point numbers, multiplying each value by a given multiplier */
static void JUCE_CALLTYPE copyWithMultiply (FloatType* dest, const FloatType* src, FloatType multiplier, CountType numValues) noexcept;
/** Adds a fixed value to the destination values. */
static void JUCE_CALLTYPE add (FloatType* dest, FloatType amountToAdd, CountType numValues) noexcept;
/** Adds a fixed value to each source value and stores it in the destination array. */
static void JUCE_CALLTYPE add (FloatType* dest, const FloatType* src, FloatType amount, CountType numValues) noexcept;
/** Adds the source values to the destination values. */
static void JUCE_CALLTYPE add (FloatType* dest, const FloatType* src, CountType numValues) noexcept;
/** Adds each source1 value to the corresponding source2 value and stores the result in the destination array. */
static void JUCE_CALLTYPE add (FloatType* dest, const FloatType* src1, const FloatType* src2, CountType num) noexcept;
/** Subtracts the source values from the destination values. */
static void JUCE_CALLTYPE subtract (FloatType* dest, const FloatType* src, CountType numValues) noexcept;
/** Subtracts each source2 value from the corresponding source1 value and stores the result in the destination array. */
static void JUCE_CALLTYPE subtract (FloatType* dest, const FloatType* src1, const FloatType* src2, CountType num) noexcept;
/** Multiplies each source value by the given multiplier, then adds it to the destination value. */
static void JUCE_CALLTYPE addWithMultiply (FloatType* dest, const FloatType* src, FloatType multiplier, CountType numValues) noexcept;
/** Multiplies each source1 value by the corresponding source2 value, then adds it to the destination value. */
static void JUCE_CALLTYPE addWithMultiply (FloatType* dest, const FloatType* src1, const FloatType* src2, CountType num) noexcept;
/** Multiplies each source value by the given multiplier, then subtracts it to the destination value. */
static void JUCE_CALLTYPE subtractWithMultiply (FloatType* dest, const FloatType* src, FloatType multiplier, CountType numValues) noexcept;
/** Multiplies each source1 value by the corresponding source2 value, then subtracts it to the destination value. */
static void JUCE_CALLTYPE subtractWithMultiply (FloatType* dest, const FloatType* src1, const FloatType* src2, CountType num) noexcept;
/** Multiplies the destination values by the source values. */
static void JUCE_CALLTYPE multiply (FloatType* dest, const FloatType* src, CountType numValues) noexcept;
/** Multiplies each source1 value by the correspinding source2 value, then stores it in the destination array. */
static void JUCE_CALLTYPE multiply (FloatType* dest, const FloatType* src1, const FloatType* src2, CountType numValues) noexcept;
/** Multiplies each of the destination values by a fixed multiplier. */
static void JUCE_CALLTYPE multiply (FloatType* dest, FloatType multiplier, CountType numValues) noexcept;
/** Multiplies each of the source values by a fixed multiplier and stores the result in the destination array. */
static void JUCE_CALLTYPE multiply (FloatType* dest, const FloatType* src, FloatType multiplier, CountType num) noexcept;
/** Copies a source vector to a destination, negating each value. */
static void JUCE_CALLTYPE negate (FloatType* dest, const FloatType* src, CountType numValues) noexcept;
/** Copies a source vector to a destination, taking the absolute of each value. */
static void JUCE_CALLTYPE abs (FloatType* dest, const FloatType* src, CountType numValues) noexcept;
/** Each element of dest will be the minimum of the corresponding element of the source array and the given comp value. */
static void JUCE_CALLTYPE min (FloatType* dest, const FloatType* src, FloatType comp, CountType num) noexcept;
/** Each element of dest will be the minimum of the corresponding source1 and source2 values. */
static void JUCE_CALLTYPE min (FloatType* dest, const FloatType* src1, const FloatType* src2, CountType num) noexcept;
/** Each element of dest will be the maximum of the corresponding element of the source array and the given comp value. */
static void JUCE_CALLTYPE max (FloatType* dest, const FloatType* src, FloatType comp, CountType num) noexcept;
/** Each element of dest will be the maximum of the corresponding source1 and source2 values. */
static void JUCE_CALLTYPE max (FloatType* dest, const FloatType* src1, const FloatType* src2, CountType num) noexcept;
/** Each element of dest is calculated by hard clipping the corresponding src element so that it is in the range specified by the arguments low and high. */
static void JUCE_CALLTYPE clip (FloatType* dest, const FloatType* src, FloatType low, FloatType high, CountType num) noexcept;
/** Finds the minimum and maximum values in the given array. */
static Range<FloatType> JUCE_CALLTYPE findMinAndMax (const FloatType* src, CountType numValues) noexcept;
/** Finds the minimum value in the given array. */
static FloatType JUCE_CALLTYPE findMinimum (const FloatType* src, CountType numValues) noexcept;
/** Finds the maximum value in the given array. */
static FloatType JUCE_CALLTYPE findMaximum (const FloatType* src, CountType numValues) noexcept;
};
#if ! DOXYGEN
namespace detail
{
template <typename...>
struct NameForwarder;
template <typename Head>
struct NameForwarder<Head> : Head {};
template <typename Head, typename... Tail>
struct NameForwarder<Head, Tail...> : Head, NameForwarder<Tail...>
{
using Head::clear;
using NameForwarder<Tail...>::clear;
using Head::fill;
using NameForwarder<Tail...>::fill;
using Head::copy;
using NameForwarder<Tail...>::copy;
using Head::copyWithMultiply;
using NameForwarder<Tail...>::copyWithMultiply;
using Head::add;
using NameForwarder<Tail...>::add;
using Head::subtract;
using NameForwarder<Tail...>::subtract;
using Head::addWithMultiply;
using NameForwarder<Tail...>::addWithMultiply;
using Head::subtractWithMultiply;
using NameForwarder<Tail...>::subtractWithMultiply;
using Head::multiply;
using NameForwarder<Tail...>::multiply;
using Head::negate;
using NameForwarder<Tail...>::negate;
using Head::abs;
using NameForwarder<Tail...>::abs;
using Head::min;
using NameForwarder<Tail...>::min;
using Head::max;
using NameForwarder<Tail...>::max;
using Head::clip;
using NameForwarder<Tail...>::clip;
using Head::findMinAndMax;
using NameForwarder<Tail...>::findMinAndMax;
using Head::findMinimum;
using NameForwarder<Tail...>::findMinimum;
using Head::findMaximum;
using NameForwarder<Tail...>::findMaximum;
};
} // namespace detail
#endif
//==============================================================================
/**
A collection of simple vector operations on arrays of floating point numbers,
accelerated with SIMD instructions where possible and providing all methods
from FloatVectorOperationsBase.
@see FloatVectorOperationsBase
@tags{Audio}
*/
class JUCE_API FloatVectorOperations : public detail::NameForwarder<FloatVectorOperationsBase<float, int>,
FloatVectorOperationsBase<float, size_t>,
FloatVectorOperationsBase<double, int>,
FloatVectorOperationsBase<double, size_t>>
{
public:
static void JUCE_CALLTYPE convertFixedToFloat (float* dest, const int* src, float multiplier, int num) noexcept;
static void JUCE_CALLTYPE convertFixedToFloat (float* dest, const int* src, float multiplier, size_t num) noexcept;
/** This method enables or disables the SSE/NEON flush-to-zero mode. */
static void JUCE_CALLTYPE enableFlushToZeroMode (bool shouldEnable) noexcept;
/** On Intel CPUs, this method enables the SSE flush-to-zero and denormalised-are-zero modes.
This effectively sets the DAZ and FZ bits of the MXCSR register. On arm CPUs this will
enable flush to zero mode.
It's a convenient thing to call before audio processing code where you really want to
avoid denormalisation performance hits.
*/
static void JUCE_CALLTYPE disableDenormalisedNumberSupport (bool shouldDisable = true) noexcept;
/** This method returns true if denormals are currently disabled. */
static bool JUCE_CALLTYPE areDenormalsDisabled() noexcept;
private:
friend ScopedNoDenormals;
static intptr_t JUCE_CALLTYPE getFpStatusRegister() noexcept;
static void JUCE_CALLTYPE setFpStatusRegister (intptr_t) noexcept;
};
//==============================================================================
/**
Helper class providing an RAII-based mechanism for temporarily disabling
denormals on your CPU.
@tags{Audio}
*/
class ScopedNoDenormals
{
public:
ScopedNoDenormals() noexcept;
~ScopedNoDenormals() noexcept;
private:
#if JUCE_USE_SSE_INTRINSICS || (JUCE_USE_ARM_NEON || defined (__arm64__) || defined (__aarch64__))
intptr_t fpsr;
#endif
};
} // namespace juce

View File

@@ -1,99 +1,99 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifdef JUCE_AUDIO_BASICS_H_INCLUDED
/* When you add this cpp file to your project, you mustn't include it in a file where you've
already included any other headers - just put it inside a file on its own, possibly with your config
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
header files that the compiler may be using.
*/
#error "Incorrect use of JUCE cpp file"
#endif
#include "juce_audio_basics.h"
#if JUCE_MINGW && ! defined (alloca)
#define alloca __builtin_alloca
#endif
#if JUCE_USE_SSE_INTRINSICS
#include <emmintrin.h>
#endif
#ifndef JUCE_USE_VDSP_FRAMEWORK
#define JUCE_USE_VDSP_FRAMEWORK 1
#endif
#if (JUCE_MAC || JUCE_IOS) && JUCE_USE_VDSP_FRAMEWORK
#include <Accelerate/Accelerate.h>
#else
#undef JUCE_USE_VDSP_FRAMEWORK
#endif
#if JUCE_USE_ARM_NEON
#include <arm_neon.h>
#endif
#include "buffers/juce_AudioDataConverters.cpp"
#include "buffers/juce_FloatVectorOperations.cpp"
#include "buffers/juce_AudioChannelSet.cpp"
#include "buffers/juce_AudioProcessLoadMeasurer.cpp"
#include "utilities/juce_IIRFilter.cpp"
#include "utilities/juce_LagrangeInterpolator.cpp"
#include "utilities/juce_WindowedSincInterpolator.cpp"
#include "utilities/juce_Interpolators.cpp"
#include "utilities/juce_SmoothedValue.cpp"
#include "midi/juce_MidiBuffer.cpp"
#include "midi/juce_MidiFile.cpp"
#include "midi/juce_MidiKeyboardState.cpp"
#include "midi/juce_MidiMessage.cpp"
#include "midi/juce_MidiMessageSequence.cpp"
#include "midi/juce_MidiRPN.cpp"
#include "mpe/juce_MPEValue.cpp"
#include "mpe/juce_MPENote.cpp"
#include "mpe/juce_MPEZoneLayout.cpp"
#include "mpe/juce_MPEInstrument.cpp"
#include "mpe/juce_MPEMessages.cpp"
#include "mpe/juce_MPESynthesiserBase.cpp"
#include "mpe/juce_MPESynthesiserVoice.cpp"
#include "mpe/juce_MPESynthesiser.cpp"
#include "mpe/juce_MPEUtils.cpp"
#include "sources/juce_BufferingAudioSource.cpp"
#include "sources/juce_ChannelRemappingAudioSource.cpp"
#include "sources/juce_IIRFilterAudioSource.cpp"
#include "sources/juce_MemoryAudioSource.cpp"
#include "sources/juce_MixerAudioSource.cpp"
#include "sources/juce_ResamplingAudioSource.cpp"
#include "sources/juce_ReverbAudioSource.cpp"
#include "sources/juce_ToneGeneratorAudioSource.cpp"
#include "synthesisers/juce_Synthesiser.cpp"
#include "midi/ump/juce_UMP.h"
#include "midi/ump/juce_UMPUtils.cpp"
#include "midi/ump/juce_UMPView.cpp"
#include "midi/ump/juce_UMPSysEx7.cpp"
#include "midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp"
#if JUCE_UNIT_TESTS
#include "utilities/juce_ADSR_test.cpp"
#include "midi/ump/juce_UMPTests.cpp"
#endif
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifdef JUCE_AUDIO_BASICS_H_INCLUDED
/* When you add this cpp file to your project, you mustn't include it in a file where you've
already included any other headers - just put it inside a file on its own, possibly with your config
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
header files that the compiler may be using.
*/
#error "Incorrect use of JUCE cpp file"
#endif
#include "juce_audio_basics.h"
#if JUCE_MINGW && ! defined (alloca)
#define alloca __builtin_alloca
#endif
#if JUCE_USE_SSE_INTRINSICS
#include <emmintrin.h>
#endif
#ifndef JUCE_USE_VDSP_FRAMEWORK
#define JUCE_USE_VDSP_FRAMEWORK 1
#endif
#if (JUCE_MAC || JUCE_IOS) && JUCE_USE_VDSP_FRAMEWORK
#include <Accelerate/Accelerate.h>
#else
#undef JUCE_USE_VDSP_FRAMEWORK
#endif
#if JUCE_USE_ARM_NEON
#include <arm_neon.h>
#endif
#include "buffers/juce_AudioDataConverters.cpp"
#include "buffers/juce_FloatVectorOperations.cpp"
#include "buffers/juce_AudioChannelSet.cpp"
#include "buffers/juce_AudioProcessLoadMeasurer.cpp"
#include "utilities/juce_IIRFilter.cpp"
#include "utilities/juce_LagrangeInterpolator.cpp"
#include "utilities/juce_WindowedSincInterpolator.cpp"
#include "utilities/juce_Interpolators.cpp"
#include "utilities/juce_SmoothedValue.cpp"
#include "midi/juce_MidiBuffer.cpp"
#include "midi/juce_MidiFile.cpp"
#include "midi/juce_MidiKeyboardState.cpp"
#include "midi/juce_MidiMessage.cpp"
#include "midi/juce_MidiMessageSequence.cpp"
#include "midi/juce_MidiRPN.cpp"
#include "mpe/juce_MPEValue.cpp"
#include "mpe/juce_MPENote.cpp"
#include "mpe/juce_MPEZoneLayout.cpp"
#include "mpe/juce_MPEInstrument.cpp"
#include "mpe/juce_MPEMessages.cpp"
#include "mpe/juce_MPESynthesiserBase.cpp"
#include "mpe/juce_MPESynthesiserVoice.cpp"
#include "mpe/juce_MPESynthesiser.cpp"
#include "mpe/juce_MPEUtils.cpp"
#include "sources/juce_BufferingAudioSource.cpp"
#include "sources/juce_ChannelRemappingAudioSource.cpp"
#include "sources/juce_IIRFilterAudioSource.cpp"
#include "sources/juce_MemoryAudioSource.cpp"
#include "sources/juce_MixerAudioSource.cpp"
#include "sources/juce_ResamplingAudioSource.cpp"
#include "sources/juce_ReverbAudioSource.cpp"
#include "sources/juce_ToneGeneratorAudioSource.cpp"
#include "synthesisers/juce_Synthesiser.cpp"
#include "midi/ump/juce_UMP.h"
#include "midi/ump/juce_UMPUtils.cpp"
#include "midi/ump/juce_UMPView.cpp"
#include "midi/ump/juce_UMPSysEx7.cpp"
#include "midi/ump/juce_UMPMidi1ToMidi2DefaultTranslator.cpp"
#if JUCE_UNIT_TESTS
#include "utilities/juce_ADSR_test.cpp"
#include "midi/ump/juce_UMP_test.cpp"
#endif

View File

@@ -1,123 +1,123 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE 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 module, and is read by
the Projucer to automatically generate project code that uses it.
For details about the syntax and how to create or use a module, see the
JUCE Module Format.md file.
BEGIN_JUCE_MODULE_DECLARATION
ID: juce_audio_basics
vendor: juce
version: 6.1.2
name: JUCE audio and MIDI data classes
description: Classes for audio buffer manipulation, midi message handling, synthesis, etc.
website: http://www.juce.com/juce
license: ISC
minimumCppStandard: 14
dependencies: juce_core
OSXFrameworks: Accelerate
iOSFrameworks: Accelerate
END_JUCE_MODULE_DECLARATION
*******************************************************************************/
#pragma once
#define JUCE_AUDIO_BASICS_H_INCLUDED
#include <juce_core/juce_core.h>
//==============================================================================
#undef Complex // apparently some C libraries actually define these symbols (!)
#undef Factor
//==============================================================================
#if JUCE_MINGW && ! defined (__SSE2__)
#define JUCE_USE_SSE_INTRINSICS 0
#endif
#ifndef JUCE_USE_SSE_INTRINSICS
#define JUCE_USE_SSE_INTRINSICS 1
#endif
#if ! JUCE_INTEL
#undef JUCE_USE_SSE_INTRINSICS
#endif
#if __ARM_NEON__ && ! (JUCE_USE_VDSP_FRAMEWORK || defined (JUCE_USE_ARM_NEON))
#define JUCE_USE_ARM_NEON 1
#endif
#if TARGET_IPHONE_SIMULATOR
#ifdef JUCE_USE_ARM_NEON
#undef JUCE_USE_ARM_NEON
#endif
#define JUCE_USE_ARM_NEON 0
#endif
//==============================================================================
#include "buffers/juce_AudioDataConverters.h"
#include "buffers/juce_FloatVectorOperations.h"
#include "buffers/juce_AudioSampleBuffer.h"
#include "buffers/juce_AudioChannelSet.h"
#include "buffers/juce_AudioProcessLoadMeasurer.h"
#include "utilities/juce_Decibels.h"
#include "utilities/juce_IIRFilter.h"
#include "utilities/juce_GenericInterpolator.h"
#include "utilities/juce_Interpolators.h"
#include "utilities/juce_SmoothedValue.h"
#include "utilities/juce_Reverb.h"
#include "utilities/juce_ADSR.h"
#include "midi/juce_MidiMessage.h"
#include "midi/juce_MidiBuffer.h"
#include "midi/juce_MidiMessageSequence.h"
#include "midi/juce_MidiFile.h"
#include "midi/juce_MidiKeyboardState.h"
#include "midi/juce_MidiRPN.h"
#include "mpe/juce_MPEValue.h"
#include "mpe/juce_MPENote.h"
#include "mpe/juce_MPEZoneLayout.h"
#include "mpe/juce_MPEInstrument.h"
#include "mpe/juce_MPEMessages.h"
#include "mpe/juce_MPESynthesiserBase.h"
#include "mpe/juce_MPESynthesiserVoice.h"
#include "mpe/juce_MPESynthesiser.h"
#include "mpe/juce_MPEUtils.h"
#include "sources/juce_AudioSource.h"
#include "sources/juce_PositionableAudioSource.h"
#include "sources/juce_BufferingAudioSource.h"
#include "sources/juce_ChannelRemappingAudioSource.h"
#include "sources/juce_IIRFilterAudioSource.h"
#include "sources/juce_MemoryAudioSource.h"
#include "sources/juce_MixerAudioSource.h"
#include "sources/juce_ResamplingAudioSource.h"
#include "sources/juce_ReverbAudioSource.h"
#include "sources/juce_ToneGeneratorAudioSource.h"
#include "synthesisers/juce_Synthesiser.h"
#include "audio_play_head/juce_AudioPlayHead.h"
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE 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 module, and is read by
the Projucer to automatically generate project code that uses it.
For details about the syntax and how to create or use a module, see the
JUCE Module Format.md file.
BEGIN_JUCE_MODULE_DECLARATION
ID: juce_audio_basics
vendor: juce
version: 7.0.2
name: JUCE audio and MIDI data classes
description: Classes for audio buffer manipulation, midi message handling, synthesis, etc.
website: http://www.juce.com/juce
license: ISC
minimumCppStandard: 14
dependencies: juce_core
OSXFrameworks: Accelerate
iOSFrameworks: Accelerate
END_JUCE_MODULE_DECLARATION
*******************************************************************************/
#pragma once
#define JUCE_AUDIO_BASICS_H_INCLUDED
#include <juce_core/juce_core.h>
//==============================================================================
#undef Complex // apparently some C libraries actually define these symbols (!)
#undef Factor
//==============================================================================
#if JUCE_MINGW && ! defined (__SSE2__)
#define JUCE_USE_SSE_INTRINSICS 0
#endif
#ifndef JUCE_USE_SSE_INTRINSICS
#define JUCE_USE_SSE_INTRINSICS 1
#endif
#if ! JUCE_INTEL
#undef JUCE_USE_SSE_INTRINSICS
#endif
#if __ARM_NEON__ && ! (JUCE_USE_VDSP_FRAMEWORK || defined (JUCE_USE_ARM_NEON))
#define JUCE_USE_ARM_NEON 1
#endif
#if TARGET_IPHONE_SIMULATOR
#ifdef JUCE_USE_ARM_NEON
#undef JUCE_USE_ARM_NEON
#endif
#define JUCE_USE_ARM_NEON 0
#endif
//==============================================================================
#include "buffers/juce_AudioDataConverters.h"
#include "buffers/juce_FloatVectorOperations.h"
#include "buffers/juce_AudioSampleBuffer.h"
#include "buffers/juce_AudioChannelSet.h"
#include "buffers/juce_AudioProcessLoadMeasurer.h"
#include "utilities/juce_Decibels.h"
#include "utilities/juce_IIRFilter.h"
#include "utilities/juce_GenericInterpolator.h"
#include "utilities/juce_Interpolators.h"
#include "utilities/juce_SmoothedValue.h"
#include "utilities/juce_Reverb.h"
#include "utilities/juce_ADSR.h"
#include "midi/juce_MidiMessage.h"
#include "midi/juce_MidiBuffer.h"
#include "midi/juce_MidiMessageSequence.h"
#include "midi/juce_MidiFile.h"
#include "midi/juce_MidiKeyboardState.h"
#include "midi/juce_MidiRPN.h"
#include "mpe/juce_MPEValue.h"
#include "mpe/juce_MPENote.h"
#include "mpe/juce_MPEZoneLayout.h"
#include "mpe/juce_MPEInstrument.h"
#include "mpe/juce_MPEMessages.h"
#include "mpe/juce_MPESynthesiserBase.h"
#include "mpe/juce_MPESynthesiserVoice.h"
#include "mpe/juce_MPESynthesiser.h"
#include "mpe/juce_MPEUtils.h"
#include "sources/juce_AudioSource.h"
#include "sources/juce_PositionableAudioSource.h"
#include "sources/juce_BufferingAudioSource.h"
#include "sources/juce_ChannelRemappingAudioSource.h"
#include "sources/juce_IIRFilterAudioSource.h"
#include "sources/juce_MemoryAudioSource.h"
#include "sources/juce_MixerAudioSource.h"
#include "sources/juce_ResamplingAudioSource.h"
#include "sources/juce_ReverbAudioSource.h"
#include "sources/juce_ToneGeneratorAudioSource.h"
#include "synthesisers/juce_Synthesiser.h"
#include "audio_play_head/juce_AudioPlayHead.h"

View File

@@ -2,7 +2,7 @@
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.

View File

@@ -1,320 +1,320 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace MidiBufferHelpers
{
inline int getEventTime (const void* d) noexcept
{
return readUnaligned<int32> (d);
}
inline uint16 getEventDataSize (const void* d) noexcept
{
return readUnaligned<uint16> (static_cast<const char*> (d) + sizeof (int32));
}
inline uint16 getEventTotalSize (const void* d) noexcept
{
return (uint16) (getEventDataSize (d) + sizeof (int32) + sizeof (uint16));
}
static int findActualEventLength (const uint8* data, int maxBytes) noexcept
{
auto byte = (unsigned int) *data;
if (byte == 0xf0 || byte == 0xf7)
{
int i = 1;
while (i < maxBytes)
if (data[i++] == 0xf7)
break;
return i;
}
if (byte == 0xff)
{
if (maxBytes == 1)
return 1;
const auto var = MidiMessage::readVariableLengthValue (data + 1, maxBytes - 1);
return jmin (maxBytes, var.value + 2 + var.bytesUsed);
}
if (byte >= 0x80)
return jmin (maxBytes, MidiMessage::getMessageLengthFromFirstByte ((uint8) byte));
return 0;
}
static uint8* findEventAfter (uint8* d, uint8* endData, int samplePosition) noexcept
{
while (d < endData && getEventTime (d) <= samplePosition)
d += getEventTotalSize (d);
return d;
}
}
//==============================================================================
MidiBufferIterator& MidiBufferIterator::operator++() noexcept
{
data += sizeof (int32) + sizeof (uint16) + size_t (MidiBufferHelpers::getEventDataSize (data));
return *this;
}
MidiBufferIterator MidiBufferIterator::operator++ (int) noexcept
{
auto copy = *this;
++(*this);
return copy;
}
MidiBufferIterator::reference MidiBufferIterator::operator*() const noexcept
{
return { data + sizeof (int32) + sizeof (uint16),
MidiBufferHelpers::getEventDataSize (data),
MidiBufferHelpers::getEventTime (data) };
}
//==============================================================================
MidiBuffer::MidiBuffer (const MidiMessage& message) noexcept
{
addEvent (message, 0);
}
void MidiBuffer::swapWith (MidiBuffer& other) noexcept { data.swapWith (other.data); }
void MidiBuffer::clear() noexcept { data.clearQuick(); }
void MidiBuffer::ensureSize (size_t minimumNumBytes) { data.ensureStorageAllocated ((int) minimumNumBytes); }
bool MidiBuffer::isEmpty() const noexcept { return data.size() == 0; }
void MidiBuffer::clear (int startSample, int numSamples)
{
auto start = MidiBufferHelpers::findEventAfter (data.begin(), data.end(), startSample - 1);
auto end = MidiBufferHelpers::findEventAfter (start, data.end(), startSample + numSamples - 1);
data.removeRange ((int) (start - data.begin()), (int) (end - start));
}
bool MidiBuffer::addEvent (const MidiMessage& m, int sampleNumber)
{
return addEvent (m.getRawData(), m.getRawDataSize(), sampleNumber);
}
bool MidiBuffer::addEvent (const void* newData, int maxBytes, int sampleNumber)
{
auto numBytes = MidiBufferHelpers::findActualEventLength (static_cast<const uint8*> (newData), maxBytes);
if (numBytes <= 0)
return true;
if (std::numeric_limits<uint16>::max() < numBytes)
{
// This method only supports messages smaller than (1 << 16) bytes
return false;
}
auto newItemSize = (size_t) numBytes + sizeof (int32) + sizeof (uint16);
auto offset = (int) (MidiBufferHelpers::findEventAfter (data.begin(), data.end(), sampleNumber) - data.begin());
data.insertMultiple (offset, 0, (int) newItemSize);
auto* d = data.begin() + offset;
writeUnaligned<int32> (d, sampleNumber);
d += sizeof (int32);
writeUnaligned<uint16> (d, static_cast<uint16> (numBytes));
d += sizeof (uint16);
memcpy (d, newData, (size_t) numBytes);
return true;
}
void MidiBuffer::addEvents (const MidiBuffer& otherBuffer,
int startSample, int numSamples, int sampleDeltaToAdd)
{
for (auto i = otherBuffer.findNextSamplePosition (startSample); i != otherBuffer.cend(); ++i)
{
const auto metadata = *i;
if (metadata.samplePosition >= startSample + numSamples && numSamples >= 0)
break;
addEvent (metadata.data, metadata.numBytes, metadata.samplePosition + sampleDeltaToAdd);
}
}
int MidiBuffer::getNumEvents() const noexcept
{
int n = 0;
auto end = data.end();
for (auto d = data.begin(); d < end; ++n)
d += MidiBufferHelpers::getEventTotalSize (d);
return n;
}
int MidiBuffer::getFirstEventTime() const noexcept
{
return data.size() > 0 ? MidiBufferHelpers::getEventTime (data.begin()) : 0;
}
int MidiBuffer::getLastEventTime() const noexcept
{
if (data.size() == 0)
return 0;
auto endData = data.end();
for (auto d = data.begin();;)
{
auto nextOne = d + MidiBufferHelpers::getEventTotalSize (d);
if (nextOne >= endData)
return MidiBufferHelpers::getEventTime (d);
d = nextOne;
}
}
MidiBufferIterator MidiBuffer::findNextSamplePosition (int samplePosition) const noexcept
{
return std::find_if (cbegin(), cend(), [&] (const MidiMessageMetadata& metadata) noexcept
{
return metadata.samplePosition >= samplePosition;
});
}
//==============================================================================
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996)
MidiBuffer::Iterator::Iterator (const MidiBuffer& b) noexcept
: buffer (b), iterator (b.data.begin())
{
}
void MidiBuffer::Iterator::setNextSamplePosition (int samplePosition) noexcept
{
iterator = buffer.findNextSamplePosition (samplePosition);
}
bool MidiBuffer::Iterator::getNextEvent (const uint8*& midiData, int& numBytes, int& samplePosition) noexcept
{
if (iterator == buffer.cend())
return false;
const auto metadata = *iterator++;
midiData = metadata.data;
numBytes = metadata.numBytes;
samplePosition = metadata.samplePosition;
return true;
}
bool MidiBuffer::Iterator::getNextEvent (MidiMessage& result, int& samplePosition) noexcept
{
if (iterator == buffer.cend())
return false;
const auto metadata = *iterator++;
result = metadata.getMessage();
samplePosition = metadata.samplePosition;
return true;
}
JUCE_END_IGNORE_WARNINGS_MSVC
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
struct MidiBufferTest : public UnitTest
{
MidiBufferTest()
: UnitTest ("MidiBuffer", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("Clear messages");
{
const auto message = MidiMessage::noteOn (1, 64, 0.5f);
const auto testBuffer = [&]
{
MidiBuffer buffer;
buffer.addEvent (message, 0);
buffer.addEvent (message, 10);
buffer.addEvent (message, 20);
buffer.addEvent (message, 30);
return buffer;
}();
{
auto buffer = testBuffer;
buffer.clear (10, 0);
expectEquals (buffer.getNumEvents(), 4);
}
{
auto buffer = testBuffer;
buffer.clear (10, 1);
expectEquals (buffer.getNumEvents(), 3);
}
{
auto buffer = testBuffer;
buffer.clear (10, 10);
expectEquals (buffer.getNumEvents(), 3);
}
{
auto buffer = testBuffer;
buffer.clear (10, 20);
expectEquals (buffer.getNumEvents(), 2);
}
{
auto buffer = testBuffer;
buffer.clear (10, 30);
expectEquals (buffer.getNumEvents(), 1);
}
{
auto buffer = testBuffer;
buffer.clear (10, 300);
expectEquals (buffer.getNumEvents(), 1);
}
}
}
};
static MidiBufferTest midiBufferTest;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace MidiBufferHelpers
{
inline int getEventTime (const void* d) noexcept
{
return readUnaligned<int32> (d);
}
inline uint16 getEventDataSize (const void* d) noexcept
{
return readUnaligned<uint16> (static_cast<const char*> (d) + sizeof (int32));
}
inline uint16 getEventTotalSize (const void* d) noexcept
{
return (uint16) (getEventDataSize (d) + sizeof (int32) + sizeof (uint16));
}
static int findActualEventLength (const uint8* data, int maxBytes) noexcept
{
auto byte = (unsigned int) *data;
if (byte == 0xf0 || byte == 0xf7)
{
int i = 1;
while (i < maxBytes)
if (data[i++] == 0xf7)
break;
return i;
}
if (byte == 0xff)
{
if (maxBytes == 1)
return 1;
const auto var = MidiMessage::readVariableLengthValue (data + 1, maxBytes - 1);
return jmin (maxBytes, var.value + 2 + var.bytesUsed);
}
if (byte >= 0x80)
return jmin (maxBytes, MidiMessage::getMessageLengthFromFirstByte ((uint8) byte));
return 0;
}
static uint8* findEventAfter (uint8* d, uint8* endData, int samplePosition) noexcept
{
while (d < endData && getEventTime (d) <= samplePosition)
d += getEventTotalSize (d);
return d;
}
}
//==============================================================================
MidiBufferIterator& MidiBufferIterator::operator++() noexcept
{
data += sizeof (int32) + sizeof (uint16) + size_t (MidiBufferHelpers::getEventDataSize (data));
return *this;
}
MidiBufferIterator MidiBufferIterator::operator++ (int) noexcept
{
auto copy = *this;
++(*this);
return copy;
}
MidiBufferIterator::reference MidiBufferIterator::operator*() const noexcept
{
return { data + sizeof (int32) + sizeof (uint16),
MidiBufferHelpers::getEventDataSize (data),
MidiBufferHelpers::getEventTime (data) };
}
//==============================================================================
MidiBuffer::MidiBuffer (const MidiMessage& message) noexcept
{
addEvent (message, 0);
}
void MidiBuffer::swapWith (MidiBuffer& other) noexcept { data.swapWith (other.data); }
void MidiBuffer::clear() noexcept { data.clearQuick(); }
void MidiBuffer::ensureSize (size_t minimumNumBytes) { data.ensureStorageAllocated ((int) minimumNumBytes); }
bool MidiBuffer::isEmpty() const noexcept { return data.size() == 0; }
void MidiBuffer::clear (int startSample, int numSamples)
{
auto start = MidiBufferHelpers::findEventAfter (data.begin(), data.end(), startSample - 1);
auto end = MidiBufferHelpers::findEventAfter (start, data.end(), startSample + numSamples - 1);
data.removeRange ((int) (start - data.begin()), (int) (end - start));
}
bool MidiBuffer::addEvent (const MidiMessage& m, int sampleNumber)
{
return addEvent (m.getRawData(), m.getRawDataSize(), sampleNumber);
}
bool MidiBuffer::addEvent (const void* newData, int maxBytes, int sampleNumber)
{
auto numBytes = MidiBufferHelpers::findActualEventLength (static_cast<const uint8*> (newData), maxBytes);
if (numBytes <= 0)
return true;
if (std::numeric_limits<uint16>::max() < numBytes)
{
// This method only supports messages smaller than (1 << 16) bytes
return false;
}
auto newItemSize = (size_t) numBytes + sizeof (int32) + sizeof (uint16);
auto offset = (int) (MidiBufferHelpers::findEventAfter (data.begin(), data.end(), sampleNumber) - data.begin());
data.insertMultiple (offset, 0, (int) newItemSize);
auto* d = data.begin() + offset;
writeUnaligned<int32> (d, sampleNumber);
d += sizeof (int32);
writeUnaligned<uint16> (d, static_cast<uint16> (numBytes));
d += sizeof (uint16);
memcpy (d, newData, (size_t) numBytes);
return true;
}
void MidiBuffer::addEvents (const MidiBuffer& otherBuffer,
int startSample, int numSamples, int sampleDeltaToAdd)
{
for (auto i = otherBuffer.findNextSamplePosition (startSample); i != otherBuffer.cend(); ++i)
{
const auto metadata = *i;
if (metadata.samplePosition >= startSample + numSamples && numSamples >= 0)
break;
addEvent (metadata.data, metadata.numBytes, metadata.samplePosition + sampleDeltaToAdd);
}
}
int MidiBuffer::getNumEvents() const noexcept
{
int n = 0;
auto end = data.end();
for (auto d = data.begin(); d < end; ++n)
d += MidiBufferHelpers::getEventTotalSize (d);
return n;
}
int MidiBuffer::getFirstEventTime() const noexcept
{
return data.size() > 0 ? MidiBufferHelpers::getEventTime (data.begin()) : 0;
}
int MidiBuffer::getLastEventTime() const noexcept
{
if (data.size() == 0)
return 0;
auto endData = data.end();
for (auto d = data.begin();;)
{
auto nextOne = d + MidiBufferHelpers::getEventTotalSize (d);
if (nextOne >= endData)
return MidiBufferHelpers::getEventTime (d);
d = nextOne;
}
}
MidiBufferIterator MidiBuffer::findNextSamplePosition (int samplePosition) const noexcept
{
return std::find_if (cbegin(), cend(), [&] (const MidiMessageMetadata& metadata) noexcept
{
return metadata.samplePosition >= samplePosition;
});
}
//==============================================================================
JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations")
JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996)
MidiBuffer::Iterator::Iterator (const MidiBuffer& b) noexcept
: buffer (b), iterator (b.data.begin())
{
}
void MidiBuffer::Iterator::setNextSamplePosition (int samplePosition) noexcept
{
iterator = buffer.findNextSamplePosition (samplePosition);
}
bool MidiBuffer::Iterator::getNextEvent (const uint8*& midiData, int& numBytes, int& samplePosition) noexcept
{
if (iterator == buffer.cend())
return false;
const auto metadata = *iterator++;
midiData = metadata.data;
numBytes = metadata.numBytes;
samplePosition = metadata.samplePosition;
return true;
}
bool MidiBuffer::Iterator::getNextEvent (MidiMessage& result, int& samplePosition) noexcept
{
if (iterator == buffer.cend())
return false;
const auto metadata = *iterator++;
result = metadata.getMessage();
samplePosition = metadata.samplePosition;
return true;
}
JUCE_END_IGNORE_WARNINGS_MSVC
JUCE_END_IGNORE_WARNINGS_GCC_LIKE
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
struct MidiBufferTest : public UnitTest
{
MidiBufferTest()
: UnitTest ("MidiBuffer", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("Clear messages");
{
const auto message = MidiMessage::noteOn (1, 64, 0.5f);
const auto testBuffer = [&]
{
MidiBuffer buffer;
buffer.addEvent (message, 0);
buffer.addEvent (message, 10);
buffer.addEvent (message, 20);
buffer.addEvent (message, 30);
return buffer;
}();
{
auto buffer = testBuffer;
buffer.clear (10, 0);
expectEquals (buffer.getNumEvents(), 4);
}
{
auto buffer = testBuffer;
buffer.clear (10, 1);
expectEquals (buffer.getNumEvents(), 3);
}
{
auto buffer = testBuffer;
buffer.clear (10, 10);
expectEquals (buffer.getNumEvents(), 3);
}
{
auto buffer = testBuffer;
buffer.clear (10, 20);
expectEquals (buffer.getNumEvents(), 2);
}
{
auto buffer = testBuffer;
buffer.clear (10, 30);
expectEquals (buffer.getNumEvents(), 1);
}
{
auto buffer = testBuffer;
buffer.clear (10, 300);
expectEquals (buffer.getNumEvents(), 1);
}
}
}
};
static MidiBufferTest midiBufferTest;
#endif
} // namespace juce

View File

@@ -1,345 +1,345 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A view of MIDI message data stored in a contiguous buffer.
Instances of this class do *not* own the midi data bytes that they point to.
Instead, they expect the midi data to live in a separate buffer that outlives
the MidiMessageMetadata instance.
@tags{Audio}
*/
struct MidiMessageMetadata final
{
MidiMessageMetadata() noexcept = default;
MidiMessageMetadata (const uint8* dataIn, int numBytesIn, int positionIn) noexcept
: data (dataIn), numBytes (numBytesIn), samplePosition (positionIn)
{
}
/** Constructs a new MidiMessage instance from the data that this object is viewing.
Note that MidiMessage owns its data storage, whereas MidiMessageMetadata does not.
*/
MidiMessage getMessage() const { return MidiMessage (data, numBytes, samplePosition); }
/** Pointer to the first byte of a MIDI message. */
const uint8* data = nullptr;
/** The number of bytes in the MIDI message. */
int numBytes = 0;
/** The MIDI message's timestamp. */
int samplePosition = 0;
};
//==============================================================================
/**
An iterator to move over contiguous raw MIDI data, which Allows iterating
over a MidiBuffer using C++11 range-for syntax.
In the following example, we log all three-byte messages in a midi buffer.
@code
void processBlock (AudioBuffer<float>&, MidiBuffer& midiBuffer) override
{
for (const MidiMessageMetadata metadata : midiBuffer)
if (metadata.numBytes == 3)
Logger::writeToLog (metadata.getMessage().getDescription());
}
@endcode
@tags{Audio}
*/
class JUCE_API MidiBufferIterator
{
using Ptr = const uint8*;
public:
MidiBufferIterator() = default;
/** Constructs an iterator pointing at the message starting at the byte `dataIn`.
`dataIn` must point to the start of a valid MIDI message. If it does not,
calling other member functions on the iterator will result in undefined
behaviour.
*/
explicit MidiBufferIterator (const uint8* dataIn) noexcept
: data (dataIn)
{
}
using difference_type = std::iterator_traits<Ptr>::difference_type;
using value_type = MidiMessageMetadata;
using reference = MidiMessageMetadata;
using pointer = void;
using iterator_category = std::input_iterator_tag;
/** Make this iterator point to the next message in the buffer. */
MidiBufferIterator& operator++() noexcept;
/** Create a copy of this object, make this iterator point to the next message in
the buffer, then return the copy.
*/
MidiBufferIterator operator++ (int) noexcept;
/** Return true if this iterator points to the same message as another
iterator instance, otherwise return false.
*/
bool operator== (const MidiBufferIterator& other) const noexcept { return data == other.data; }
/** Return false if this iterator points to the same message as another
iterator instance, otherwise returns true.
*/
bool operator!= (const MidiBufferIterator& other) const noexcept { return ! operator== (other); }
/** Return an instance of MidiMessageMetadata which describes the message to which
the iterator is currently pointing.
*/
reference operator*() const noexcept;
private:
Ptr data = nullptr;
};
//==============================================================================
/**
Holds a sequence of time-stamped midi events.
Analogous to the AudioBuffer, this holds a set of midi events with
integer time-stamps. The buffer is kept sorted in order of the time-stamps.
If you're working with a sequence of midi events that may need to be manipulated
or read/written to a midi file, then MidiMessageSequence is probably a more
appropriate container. MidiBuffer is designed for lower-level streams of raw
midi data.
@see MidiMessage
@tags{Audio}
*/
class JUCE_API MidiBuffer
{
public:
//==============================================================================
/** Creates an empty MidiBuffer. */
MidiBuffer() noexcept = default;
/** Creates a MidiBuffer containing a single midi message. */
explicit MidiBuffer (const MidiMessage& message) noexcept;
//==============================================================================
/** Removes all events from the buffer. */
void clear() noexcept;
/** Removes all events between two times from the buffer.
All events for which (start <= event position < start + numSamples) will
be removed.
*/
void clear (int start, int numSamples);
/** Returns true if the buffer is empty.
To actually retrieve the events, use a MidiBufferIterator object
*/
bool isEmpty() const noexcept;
/** Counts the number of events in the buffer.
This is actually quite a slow operation, as it has to iterate through all
the events, so you might prefer to call isEmpty() if that's all you need
to know.
*/
int getNumEvents() const noexcept;
/** Adds an event to the buffer.
The sample number will be used to determine the position of the event in
the buffer, which is always kept sorted. The MidiMessage's timestamp is
ignored.
If an event is added whose sample position is the same as one or more events
already in the buffer, the new event will be placed after the existing ones.
To retrieve events, use a MidiBufferIterator object.
Returns true on success, or false on failure.
*/
bool addEvent (const MidiMessage& midiMessage, int sampleNumber);
/** Adds an event to the buffer from raw midi data.
The sample number will be used to determine the position of the event in
the buffer, which is always kept sorted.
If an event is added whose sample position is the same as one or more events
already in the buffer, the new event will be placed after the existing ones.
The event data will be inspected to calculate the number of bytes in length that
the midi event really takes up, so maxBytesOfMidiData may be longer than the data
that actually gets stored. E.g. if you pass in a note-on and a length of 4 bytes,
it'll actually only store 3 bytes. If the midi data is invalid, it might not
add an event at all.
To retrieve events, use a MidiBufferIterator object.
Returns true on success, or false on failure.
*/
bool addEvent (const void* rawMidiData,
int maxBytesOfMidiData,
int sampleNumber);
/** Adds some events from another buffer to this one.
@param otherBuffer the buffer containing the events you want to add
@param startSample the lowest sample number in the source buffer for which
events should be added. Any source events whose timestamp is
less than this will be ignored
@param numSamples the valid range of samples from the source buffer for which
events should be added - i.e. events in the source buffer whose
timestamp is greater than or equal to (startSample + numSamples)
will be ignored. If this value is less than 0, all events after
startSample will be taken.
@param sampleDeltaToAdd a value which will be added to the source timestamps of the events
that are added to this buffer
*/
void addEvents (const MidiBuffer& otherBuffer,
int startSample,
int numSamples,
int sampleDeltaToAdd);
/** Returns the sample number of the first event in the buffer.
If the buffer's empty, this will just return 0.
*/
int getFirstEventTime() const noexcept;
/** Returns the sample number of the last event in the buffer.
If the buffer's empty, this will just return 0.
*/
int getLastEventTime() const noexcept;
//==============================================================================
/** Exchanges the contents of this buffer with another one.
This is a quick operation, because no memory allocating or copying is done, it
just swaps the internal state of the two buffers.
*/
void swapWith (MidiBuffer&) noexcept;
/** Preallocates some memory for the buffer to use.
This helps to avoid needing to reallocate space when the buffer has messages
added to it.
*/
void ensureSize (size_t minimumNumBytes);
/** Get a read-only iterator pointing to the beginning of this buffer. */
MidiBufferIterator begin() const noexcept { return cbegin(); }
/** Get a read-only iterator pointing one past the end of this buffer. */
MidiBufferIterator end() const noexcept { return cend(); }
/** Get a read-only iterator pointing to the beginning of this buffer. */
MidiBufferIterator cbegin() const noexcept { return MidiBufferIterator (data.begin()); }
/** Get a read-only iterator pointing one past the end of this buffer. */
MidiBufferIterator cend() const noexcept { return MidiBufferIterator (data.end()); }
/** Get an iterator pointing to the first event with a timestamp greater-than or
equal-to `samplePosition`.
*/
MidiBufferIterator findNextSamplePosition (int samplePosition) const noexcept;
//==============================================================================
#ifndef DOXYGEN
/** This class is now deprecated in favour of MidiBufferIterator.
Used to iterate through the events in a MidiBuffer.
Note that altering the buffer while an iterator is using it will produce
undefined behaviour.
@see MidiBuffer
*/
class [[deprecated]] JUCE_API Iterator
{
public:
//==============================================================================
/** Creates an Iterator for this MidiBuffer. */
Iterator (const MidiBuffer& b) noexcept;
//==============================================================================
/** Repositions the iterator so that the next event retrieved will be the first
one whose sample position is at greater than or equal to the given position.
*/
void setNextSamplePosition (int samplePosition) noexcept;
/** Retrieves a copy of the next event from the buffer.
@param result on return, this will be the message. The MidiMessage's timestamp
is set to the same value as samplePosition.
@param samplePosition on return, this will be the position of the event, as a
sample index in the buffer
@returns true if an event was found, or false if the iterator has reached
the end of the buffer
*/
bool getNextEvent (MidiMessage& result,
int& samplePosition) noexcept;
/** Retrieves the next event from the buffer.
@param midiData on return, this pointer will be set to a block of data containing
the midi message. Note that to make it fast, this is a pointer
directly into the MidiBuffer's internal data, so is only valid
temporarily until the MidiBuffer is altered.
@param numBytesOfMidiData on return, this is the number of bytes of data used by the
midi message
@param samplePosition on return, this will be the position of the event, as a
sample index in the buffer
@returns true if an event was found, or false if the iterator has reached
the end of the buffer
*/
bool getNextEvent (const uint8* &midiData,
int& numBytesOfMidiData,
int& samplePosition) noexcept;
private:
//==============================================================================
const MidiBuffer& buffer;
MidiBufferIterator iterator;
};
#endif
/** The raw data holding this buffer.
Obviously access to this data is provided at your own risk. Its internal format could
change in future, so don't write code that relies on it!
*/
Array<uint8> data;
private:
JUCE_LEAK_DETECTOR (MidiBuffer)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A view of MIDI message data stored in a contiguous buffer.
Instances of this class do *not* own the midi data bytes that they point to.
Instead, they expect the midi data to live in a separate buffer that outlives
the MidiMessageMetadata instance.
@tags{Audio}
*/
struct MidiMessageMetadata final
{
MidiMessageMetadata() noexcept = default;
MidiMessageMetadata (const uint8* dataIn, int numBytesIn, int positionIn) noexcept
: data (dataIn), numBytes (numBytesIn), samplePosition (positionIn)
{
}
/** Constructs a new MidiMessage instance from the data that this object is viewing.
Note that MidiMessage owns its data storage, whereas MidiMessageMetadata does not.
*/
MidiMessage getMessage() const { return MidiMessage (data, numBytes, samplePosition); }
/** Pointer to the first byte of a MIDI message. */
const uint8* data = nullptr;
/** The number of bytes in the MIDI message. */
int numBytes = 0;
/** The MIDI message's timestamp. */
int samplePosition = 0;
};
//==============================================================================
/**
An iterator to move over contiguous raw MIDI data, which Allows iterating
over a MidiBuffer using C++11 range-for syntax.
In the following example, we log all three-byte messages in a midi buffer.
@code
void processBlock (AudioBuffer<float>&, MidiBuffer& midiBuffer) override
{
for (const MidiMessageMetadata metadata : midiBuffer)
if (metadata.numBytes == 3)
Logger::writeToLog (metadata.getMessage().getDescription());
}
@endcode
@tags{Audio}
*/
class JUCE_API MidiBufferIterator
{
using Ptr = const uint8*;
public:
MidiBufferIterator() = default;
/** Constructs an iterator pointing at the message starting at the byte `dataIn`.
`dataIn` must point to the start of a valid MIDI message. If it does not,
calling other member functions on the iterator will result in undefined
behaviour.
*/
explicit MidiBufferIterator (const uint8* dataIn) noexcept
: data (dataIn)
{
}
using difference_type = std::iterator_traits<Ptr>::difference_type;
using value_type = MidiMessageMetadata;
using reference = MidiMessageMetadata;
using pointer = void;
using iterator_category = std::input_iterator_tag;
/** Make this iterator point to the next message in the buffer. */
MidiBufferIterator& operator++() noexcept;
/** Create a copy of this object, make this iterator point to the next message in
the buffer, then return the copy.
*/
MidiBufferIterator operator++ (int) noexcept;
/** Return true if this iterator points to the same message as another
iterator instance, otherwise return false.
*/
bool operator== (const MidiBufferIterator& other) const noexcept { return data == other.data; }
/** Return false if this iterator points to the same message as another
iterator instance, otherwise returns true.
*/
bool operator!= (const MidiBufferIterator& other) const noexcept { return ! operator== (other); }
/** Return an instance of MidiMessageMetadata which describes the message to which
the iterator is currently pointing.
*/
reference operator*() const noexcept;
private:
Ptr data = nullptr;
};
//==============================================================================
/**
Holds a sequence of time-stamped midi events.
Analogous to the AudioBuffer, this holds a set of midi events with
integer time-stamps. The buffer is kept sorted in order of the time-stamps.
If you're working with a sequence of midi events that may need to be manipulated
or read/written to a midi file, then MidiMessageSequence is probably a more
appropriate container. MidiBuffer is designed for lower-level streams of raw
midi data.
@see MidiMessage
@tags{Audio}
*/
class JUCE_API MidiBuffer
{
public:
//==============================================================================
/** Creates an empty MidiBuffer. */
MidiBuffer() noexcept = default;
/** Creates a MidiBuffer containing a single midi message. */
explicit MidiBuffer (const MidiMessage& message) noexcept;
//==============================================================================
/** Removes all events from the buffer. */
void clear() noexcept;
/** Removes all events between two times from the buffer.
All events for which (start <= event position < start + numSamples) will
be removed.
*/
void clear (int start, int numSamples);
/** Returns true if the buffer is empty.
To actually retrieve the events, use a MidiBufferIterator object
*/
bool isEmpty() const noexcept;
/** Counts the number of events in the buffer.
This is actually quite a slow operation, as it has to iterate through all
the events, so you might prefer to call isEmpty() if that's all you need
to know.
*/
int getNumEvents() const noexcept;
/** Adds an event to the buffer.
The sample number will be used to determine the position of the event in
the buffer, which is always kept sorted. The MidiMessage's timestamp is
ignored.
If an event is added whose sample position is the same as one or more events
already in the buffer, the new event will be placed after the existing ones.
To retrieve events, use a MidiBufferIterator object.
Returns true on success, or false on failure.
*/
bool addEvent (const MidiMessage& midiMessage, int sampleNumber);
/** Adds an event to the buffer from raw midi data.
The sample number will be used to determine the position of the event in
the buffer, which is always kept sorted.
If an event is added whose sample position is the same as one or more events
already in the buffer, the new event will be placed after the existing ones.
The event data will be inspected to calculate the number of bytes in length that
the midi event really takes up, so maxBytesOfMidiData may be longer than the data
that actually gets stored. E.g. if you pass in a note-on and a length of 4 bytes,
it'll actually only store 3 bytes. If the midi data is invalid, it might not
add an event at all.
To retrieve events, use a MidiBufferIterator object.
Returns true on success, or false on failure.
*/
bool addEvent (const void* rawMidiData,
int maxBytesOfMidiData,
int sampleNumber);
/** Adds some events from another buffer to this one.
@param otherBuffer the buffer containing the events you want to add
@param startSample the lowest sample number in the source buffer for which
events should be added. Any source events whose timestamp is
less than this will be ignored
@param numSamples the valid range of samples from the source buffer for which
events should be added - i.e. events in the source buffer whose
timestamp is greater than or equal to (startSample + numSamples)
will be ignored. If this value is less than 0, all events after
startSample will be taken.
@param sampleDeltaToAdd a value which will be added to the source timestamps of the events
that are added to this buffer
*/
void addEvents (const MidiBuffer& otherBuffer,
int startSample,
int numSamples,
int sampleDeltaToAdd);
/** Returns the sample number of the first event in the buffer.
If the buffer's empty, this will just return 0.
*/
int getFirstEventTime() const noexcept;
/** Returns the sample number of the last event in the buffer.
If the buffer's empty, this will just return 0.
*/
int getLastEventTime() const noexcept;
//==============================================================================
/** Exchanges the contents of this buffer with another one.
This is a quick operation, because no memory allocating or copying is done, it
just swaps the internal state of the two buffers.
*/
void swapWith (MidiBuffer&) noexcept;
/** Preallocates some memory for the buffer to use.
This helps to avoid needing to reallocate space when the buffer has messages
added to it.
*/
void ensureSize (size_t minimumNumBytes);
/** Get a read-only iterator pointing to the beginning of this buffer. */
MidiBufferIterator begin() const noexcept { return cbegin(); }
/** Get a read-only iterator pointing one past the end of this buffer. */
MidiBufferIterator end() const noexcept { return cend(); }
/** Get a read-only iterator pointing to the beginning of this buffer. */
MidiBufferIterator cbegin() const noexcept { return MidiBufferIterator (data.begin()); }
/** Get a read-only iterator pointing one past the end of this buffer. */
MidiBufferIterator cend() const noexcept { return MidiBufferIterator (data.end()); }
/** Get an iterator pointing to the first event with a timestamp greater-than or
equal-to `samplePosition`.
*/
MidiBufferIterator findNextSamplePosition (int samplePosition) const noexcept;
//==============================================================================
#ifndef DOXYGEN
/** This class is now deprecated in favour of MidiBufferIterator.
Used to iterate through the events in a MidiBuffer.
Note that altering the buffer while an iterator is using it will produce
undefined behaviour.
@see MidiBuffer
*/
class [[deprecated]] JUCE_API Iterator
{
public:
//==============================================================================
/** Creates an Iterator for this MidiBuffer. */
Iterator (const MidiBuffer& b) noexcept;
//==============================================================================
/** Repositions the iterator so that the next event retrieved will be the first
one whose sample position is at greater than or equal to the given position.
*/
void setNextSamplePosition (int samplePosition) noexcept;
/** Retrieves a copy of the next event from the buffer.
@param result on return, this will be the message. The MidiMessage's timestamp
is set to the same value as samplePosition.
@param samplePosition on return, this will be the position of the event, as a
sample index in the buffer
@returns true if an event was found, or false if the iterator has reached
the end of the buffer
*/
bool getNextEvent (MidiMessage& result,
int& samplePosition) noexcept;
/** Retrieves the next event from the buffer.
@param midiData on return, this pointer will be set to a block of data containing
the midi message. Note that to make it fast, this is a pointer
directly into the MidiBuffer's internal data, so is only valid
temporarily until the MidiBuffer is altered.
@param numBytesOfMidiData on return, this is the number of bytes of data used by the
midi message
@param samplePosition on return, this will be the position of the event, as a
sample index in the buffer
@returns true if an event was found, or false if the iterator has reached
the end of the buffer
*/
bool getNextEvent (const uint8* &midiData,
int& numBytesOfMidiData,
int& samplePosition) noexcept;
private:
//==============================================================================
const MidiBuffer& buffer;
MidiBufferIterator iterator;
};
#endif
/** The raw data holding this buffer.
Obviously access to this data is provided at your own risk. Its internal format could
change in future, so don't write code that relies on it!
*/
Array<uint8> data;
private:
JUCE_LEAK_DETECTOR (MidiBuffer)
};
} // namespace juce

View File

@@ -1,188 +1,188 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Helper class that takes chunks of incoming midi bytes, packages them into
messages, and dispatches them to a midi callback.
@tags{Audio}
*/
class MidiDataConcatenator
{
public:
MidiDataConcatenator (int initialBufferSize)
: pendingSysexData ((size_t) initialBufferSize)
{
}
void reset()
{
currentMessageLen = 0;
pendingSysexSize = 0;
pendingSysexTime = 0;
}
template <typename UserDataType, typename CallbackType>
void pushMidiData (const void* inputData, int numBytes, double time,
UserDataType* input, CallbackType& callback)
{
auto d = static_cast<const uint8*> (inputData);
while (numBytes > 0)
{
auto nextByte = *d;
if (pendingSysexSize != 0 || nextByte == 0xf0)
{
processSysex (d, numBytes, time, input, callback);
currentMessageLen = 0;
continue;
}
++d;
--numBytes;
if (isRealtimeMessage (nextByte))
{
callback.handleIncomingMidiMessage (input, MidiMessage (nextByte, time));
// These can be embedded in the middle of a normal message, so we won't
// reset the currentMessageLen here.
continue;
}
if (isInitialByte (nextByte))
{
currentMessage[0] = nextByte;
currentMessageLen = 1;
}
else if (currentMessageLen > 0 && currentMessageLen < 3)
{
currentMessage[currentMessageLen++] = nextByte;
}
else
{
// message is too long or invalid MIDI - abandon it and start again with the next byte
currentMessageLen = 0;
continue;
}
auto expectedLength = MidiMessage::getMessageLengthFromFirstByte (currentMessage[0]);
if (expectedLength == currentMessageLen)
{
callback.handleIncomingMidiMessage (input, MidiMessage (currentMessage, expectedLength, time));
currentMessageLen = 1; // reset, but leave the first byte to use as the running status byte
}
}
}
private:
template <typename UserDataType, typename CallbackType>
void processSysex (const uint8*& d, int& numBytes, double time,
UserDataType* input, CallbackType& callback)
{
if (*d == 0xf0)
{
pendingSysexSize = 0;
pendingSysexTime = time;
}
pendingSysexData.ensureSize ((size_t) (pendingSysexSize + numBytes), false);
auto totalMessage = static_cast<uint8*> (pendingSysexData.getData());
auto dest = totalMessage + pendingSysexSize;
do
{
if (pendingSysexSize > 0 && isStatusByte (*d))
{
if (*d == 0xf7)
{
*dest++ = *d++;
++pendingSysexSize;
--numBytes;
break;
}
if (*d >= 0xfa || *d == 0xf8)
{
callback.handleIncomingMidiMessage (input, MidiMessage (*d, time));
++d;
--numBytes;
}
else
{
pendingSysexSize = 0;
int used = 0;
const MidiMessage m (d, numBytes, used, 0, time);
if (used > 0)
{
callback.handleIncomingMidiMessage (input, m);
numBytes -= used;
d += used;
}
break;
}
}
else
{
*dest++ = *d++;
++pendingSysexSize;
--numBytes;
}
}
while (numBytes > 0);
if (pendingSysexSize > 0)
{
if (totalMessage [pendingSysexSize - 1] == 0xf7)
{
callback.handleIncomingMidiMessage (input, MidiMessage (totalMessage, pendingSysexSize, pendingSysexTime));
pendingSysexSize = 0;
}
else
{
callback.handlePartialSysexMessage (input, totalMessage, pendingSysexSize, pendingSysexTime);
}
}
}
static bool isRealtimeMessage (uint8 byte) { return byte >= 0xf8 && byte <= 0xfe; }
static bool isStatusByte (uint8 byte) { return byte >= 0x80; }
static bool isInitialByte (uint8 byte) { return isStatusByte (byte) && byte != 0xf7; }
uint8 currentMessage[3];
int currentMessageLen = 0;
MemoryBlock pendingSysexData;
double pendingSysexTime = 0;
int pendingSysexSize = 0;
JUCE_DECLARE_NON_COPYABLE (MidiDataConcatenator)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Helper class that takes chunks of incoming midi bytes, packages them into
messages, and dispatches them to a midi callback.
@tags{Audio}
*/
class MidiDataConcatenator
{
public:
MidiDataConcatenator (int initialBufferSize)
: pendingSysexData ((size_t) initialBufferSize)
{
}
void reset()
{
currentMessageLen = 0;
pendingSysexSize = 0;
pendingSysexTime = 0;
}
template <typename UserDataType, typename CallbackType>
void pushMidiData (const void* inputData, int numBytes, double time,
UserDataType* input, CallbackType& callback)
{
auto d = static_cast<const uint8*> (inputData);
while (numBytes > 0)
{
auto nextByte = *d;
if (pendingSysexSize != 0 || nextByte == 0xf0)
{
processSysex (d, numBytes, time, input, callback);
currentMessageLen = 0;
continue;
}
++d;
--numBytes;
if (isRealtimeMessage (nextByte))
{
callback.handleIncomingMidiMessage (input, MidiMessage (nextByte, time));
// These can be embedded in the middle of a normal message, so we won't
// reset the currentMessageLen here.
continue;
}
if (isInitialByte (nextByte))
{
currentMessage[0] = nextByte;
currentMessageLen = 1;
}
else if (currentMessageLen > 0 && currentMessageLen < 3)
{
currentMessage[currentMessageLen++] = nextByte;
}
else
{
// message is too long or invalid MIDI - abandon it and start again with the next byte
currentMessageLen = 0;
continue;
}
auto expectedLength = MidiMessage::getMessageLengthFromFirstByte (currentMessage[0]);
if (expectedLength == currentMessageLen)
{
callback.handleIncomingMidiMessage (input, MidiMessage (currentMessage, expectedLength, time));
currentMessageLen = 1; // reset, but leave the first byte to use as the running status byte
}
}
}
private:
template <typename UserDataType, typename CallbackType>
void processSysex (const uint8*& d, int& numBytes, double time,
UserDataType* input, CallbackType& callback)
{
if (*d == 0xf0)
{
pendingSysexSize = 0;
pendingSysexTime = time;
}
pendingSysexData.ensureSize ((size_t) (pendingSysexSize + numBytes), false);
auto totalMessage = static_cast<uint8*> (pendingSysexData.getData());
auto dest = totalMessage + pendingSysexSize;
do
{
if (pendingSysexSize > 0 && isStatusByte (*d))
{
if (*d == 0xf7)
{
*dest++ = *d++;
++pendingSysexSize;
--numBytes;
break;
}
if (*d >= 0xfa || *d == 0xf8)
{
callback.handleIncomingMidiMessage (input, MidiMessage (*d, time));
++d;
--numBytes;
}
else
{
pendingSysexSize = 0;
int used = 0;
const MidiMessage m (d, numBytes, used, 0, time);
if (used > 0)
{
callback.handleIncomingMidiMessage (input, m);
numBytes -= used;
d += used;
}
break;
}
}
else
{
*dest++ = *d++;
++pendingSysexSize;
--numBytes;
}
}
while (numBytes > 0);
if (pendingSysexSize > 0)
{
if (totalMessage [pendingSysexSize - 1] == 0xf7)
{
callback.handleIncomingMidiMessage (input, MidiMessage (totalMessage, pendingSysexSize, pendingSysexTime));
pendingSysexSize = 0;
}
else
{
callback.handlePartialSysexMessage (input, totalMessage, pendingSysexSize, pendingSysexTime);
}
}
}
static bool isRealtimeMessage (uint8 byte) { return byte >= 0xf8 && byte <= 0xfe; }
static bool isStatusByte (uint8 byte) { return byte >= 0x80; }
static bool isInitialByte (uint8 byte) { return isStatusByte (byte) && byte != 0xf7; }
uint8 currentMessage[3];
int currentMessageLen = 0;
MemoryBlock pendingSysexData;
double pendingSysexTime = 0;
int pendingSysexSize = 0;
JUCE_DECLARE_NON_COPYABLE (MidiDataConcatenator)
};
} // namespace juce

File diff suppressed because it is too large Load Diff

View File

@@ -1,201 +1,198 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Reads/writes standard midi format files.
To read a midi file, create a MidiFile object and call its readFrom() method. You
can then get the individual midi tracks from it using the getTrack() method.
To write a file, create a MidiFile object, add some MidiMessageSequence objects
to it using the addTrack() method, and then call its writeTo() method to stream
it out.
@see MidiMessageSequence
@tags{Audio}
*/
class JUCE_API MidiFile
{
public:
//==============================================================================
/** Creates an empty MidiFile object. */
MidiFile();
/** Destructor. */
~MidiFile();
/** Creates a copy of another MidiFile. */
MidiFile (const MidiFile&);
/** Copies from another MidiFile object */
MidiFile& operator= (const MidiFile&);
/** Creates a copy of another MidiFile. */
MidiFile (MidiFile&&);
/** Copies from another MidiFile object */
MidiFile& operator= (MidiFile&&);
//==============================================================================
/** Returns the number of tracks in the file.
@see getTrack, addTrack
*/
int getNumTracks() const noexcept;
/** Returns a pointer to one of the tracks in the file.
@returns a pointer to the track, or nullptr if the index is out-of-range
@see getNumTracks, addTrack
*/
const MidiMessageSequence* getTrack (int index) const noexcept;
/** Adds a midi track to the file.
This will make its own internal copy of the sequence that is passed-in.
@see getNumTracks, getTrack
*/
void addTrack (const MidiMessageSequence& trackSequence);
/** Removes all midi tracks from the file.
@see getNumTracks
*/
void clear();
/** Returns the raw time format code that will be written to a stream.
After reading a midi file, this method will return the time-format that
was read from the file's header. It can be changed using the setTicksPerQuarterNote()
or setSmpteTimeFormat() methods.
If the value returned is positive, it indicates the number of midi ticks
per quarter-note - see setTicksPerQuarterNote().
It it's negative, the upper byte indicates the frames-per-second (but negative), and
the lower byte is the number of ticks per frame - see setSmpteTimeFormat().
*/
short getTimeFormat() const noexcept;
/** Sets the time format to use when this file is written to a stream.
If this is called, the file will be written as bars/beats using the
specified resolution, rather than SMPTE absolute times, as would be
used if setSmpteTimeFormat() had been called instead.
@param ticksPerQuarterNote e.g. 96, 960
@see setSmpteTimeFormat
*/
void setTicksPerQuarterNote (int ticksPerQuarterNote) noexcept;
/** Sets the time format to use when this file is written to a stream.
If this is called, the file will be written using absolute times, rather
than bars/beats as would be the case if setTicksPerBeat() had been called
instead.
@param framesPerSecond must be 24, 25, 29 or 30
@param subframeResolution the sub-second resolution, e.g. 4 (midi time code),
8, 10, 80 (SMPTE bit resolution), or 100. For millisecond
timing, setSmpteTimeFormat (25, 40)
@see setTicksPerBeat
*/
void setSmpteTimeFormat (int framesPerSecond,
int subframeResolution) noexcept;
//==============================================================================
/** Makes a list of all the tempo-change meta-events from all tracks in the midi file.
Useful for finding the positions of all the tempo changes in a file.
@param tempoChangeEvents a list to which all the events will be added
*/
void findAllTempoEvents (MidiMessageSequence& tempoChangeEvents) const;
/** Makes a list of all the time-signature meta-events from all tracks in the midi file.
Useful for finding the positions of all the tempo changes in a file.
@param timeSigEvents a list to which all the events will be added
*/
void findAllTimeSigEvents (MidiMessageSequence& timeSigEvents) const;
/** Makes a list of all the time-signature meta-events from all tracks in the midi file.
@param keySigEvents a list to which all the events will be added
*/
void findAllKeySigEvents (MidiMessageSequence& keySigEvents) const;
/** Returns the latest timestamp in any of the tracks.
(Useful for finding the length of the file).
*/
double getLastTimestamp() const;
//==============================================================================
/** Reads a midi file format stream.
After calling this, you can get the tracks that were read from the file by using the
getNumTracks() and getTrack() methods.
The timestamps of the midi events in the tracks will represent their positions in
terms of midi ticks. To convert them to seconds, use the convertTimestampTicksToSeconds()
method.
@param sourceStream the source stream
@param createMatchingNoteOffs if true, any missing note-offs for previous note-ons will
be automatically added at the end of the file by calling
MidiMessageSequence::updateMatchedPairs on each track.
@param midiFileType if not nullptr, the integer at this address will be set
to 0, 1, or 2 depending on the type of the midi file
@returns true if the stream was read successfully
*/
bool readFrom (InputStream& sourceStream,
bool createMatchingNoteOffs = true,
int* midiFileType = nullptr);
/** Writes the midi tracks as a standard midi file.
The midiFileType value is written as the file's format type, which can be 0, 1
or 2 - see the midi file spec for more info about that.
@param destStream the destination stream
@param midiFileType the type of midi file
@returns true if the operation succeeded.
*/
bool writeTo (OutputStream& destStream, int midiFileType = 1) const;
/** Converts the timestamp of all the midi events from midi ticks to seconds.
This will use the midi time format and tempo/time signature info in the
tracks to convert all the timestamps to absolute values in seconds.
*/
void convertTimestampTicksToSeconds();
private:
//==============================================================================
OwnedArray<MidiMessageSequence> tracks;
short timeFormat;
void readNextTrack (const uint8*, int, bool);
bool writeTrack (OutputStream&, const MidiMessageSequence&) const;
JUCE_LEAK_DETECTOR (MidiFile)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Reads/writes standard midi format files.
To read a midi file, create a MidiFile object and call its readFrom() method. You
can then get the individual midi tracks from it using the getTrack() method.
To write a file, create a MidiFile object, add some MidiMessageSequence objects
to it using the addTrack() method, and then call its writeTo() method to stream
it out.
@see MidiMessageSequence
@tags{Audio}
*/
class JUCE_API MidiFile
{
public:
//==============================================================================
/** Creates an empty MidiFile object. */
MidiFile();
/** Creates a copy of another MidiFile. */
MidiFile (const MidiFile&);
/** Copies from another MidiFile object */
MidiFile& operator= (const MidiFile&);
/** Creates a copy of another MidiFile. */
MidiFile (MidiFile&&);
/** Copies from another MidiFile object */
MidiFile& operator= (MidiFile&&);
//==============================================================================
/** Returns the number of tracks in the file.
@see getTrack, addTrack
*/
int getNumTracks() const noexcept;
/** Returns a pointer to one of the tracks in the file.
@returns a pointer to the track, or nullptr if the index is out-of-range
@see getNumTracks, addTrack
*/
const MidiMessageSequence* getTrack (int index) const noexcept;
/** Adds a midi track to the file.
This will make its own internal copy of the sequence that is passed-in.
@see getNumTracks, getTrack
*/
void addTrack (const MidiMessageSequence& trackSequence);
/** Removes all midi tracks from the file.
@see getNumTracks
*/
void clear();
/** Returns the raw time format code that will be written to a stream.
After reading a midi file, this method will return the time-format that
was read from the file's header. It can be changed using the setTicksPerQuarterNote()
or setSmpteTimeFormat() methods.
If the value returned is positive, it indicates the number of midi ticks
per quarter-note - see setTicksPerQuarterNote().
It it's negative, the upper byte indicates the frames-per-second (but negative), and
the lower byte is the number of ticks per frame - see setSmpteTimeFormat().
*/
short getTimeFormat() const noexcept;
/** Sets the time format to use when this file is written to a stream.
If this is called, the file will be written as bars/beats using the
specified resolution, rather than SMPTE absolute times, as would be
used if setSmpteTimeFormat() had been called instead.
@param ticksPerQuarterNote e.g. 96, 960
@see setSmpteTimeFormat
*/
void setTicksPerQuarterNote (int ticksPerQuarterNote) noexcept;
/** Sets the time format to use when this file is written to a stream.
If this is called, the file will be written using absolute times, rather
than bars/beats as would be the case if setTicksPerBeat() had been called
instead.
@param framesPerSecond must be 24, 25, 29 or 30
@param subframeResolution the sub-second resolution, e.g. 4 (midi time code),
8, 10, 80 (SMPTE bit resolution), or 100. For millisecond
timing, setSmpteTimeFormat (25, 40)
@see setTicksPerBeat
*/
void setSmpteTimeFormat (int framesPerSecond,
int subframeResolution) noexcept;
//==============================================================================
/** Makes a list of all the tempo-change meta-events from all tracks in the midi file.
Useful for finding the positions of all the tempo changes in a file.
@param tempoChangeEvents a list to which all the events will be added
*/
void findAllTempoEvents (MidiMessageSequence& tempoChangeEvents) const;
/** Makes a list of all the time-signature meta-events from all tracks in the midi file.
Useful for finding the positions of all the tempo changes in a file.
@param timeSigEvents a list to which all the events will be added
*/
void findAllTimeSigEvents (MidiMessageSequence& timeSigEvents) const;
/** Makes a list of all the key-signature meta-events from all tracks in the midi file.
@param keySigEvents a list to which all the events will be added
*/
void findAllKeySigEvents (MidiMessageSequence& keySigEvents) const;
/** Returns the latest timestamp in any of the tracks.
(Useful for finding the length of the file).
*/
double getLastTimestamp() const;
//==============================================================================
/** Reads a midi file format stream.
After calling this, you can get the tracks that were read from the file by using the
getNumTracks() and getTrack() methods.
The timestamps of the midi events in the tracks will represent their positions in
terms of midi ticks. To convert them to seconds, use the convertTimestampTicksToSeconds()
method.
@param sourceStream the source stream
@param createMatchingNoteOffs if true, any missing note-offs for previous note-ons will
be automatically added at the end of the file by calling
MidiMessageSequence::updateMatchedPairs on each track.
@param midiFileType if not nullptr, the integer at this address will be set
to 0, 1, or 2 depending on the type of the midi file
@returns true if the stream was read successfully
*/
bool readFrom (InputStream& sourceStream,
bool createMatchingNoteOffs = true,
int* midiFileType = nullptr);
/** Writes the midi tracks as a standard midi file.
The midiFileType value is written as the file's format type, which can be 0, 1
or 2 - see the midi file spec for more info about that.
@param destStream the destination stream
@param midiFileType the type of midi file
@returns true if the operation succeeded.
*/
bool writeTo (OutputStream& destStream, int midiFileType = 1) const;
/** Converts the timestamp of all the midi events from midi ticks to seconds.
This will use the midi time format and tempo/time signature info in the
tracks to convert all the timestamps to absolute values in seconds.
*/
void convertTimestampTicksToSeconds();
private:
//==============================================================================
OwnedArray<MidiMessageSequence> tracks;
short timeFormat;
void readNextTrack (const uint8*, int, bool);
bool writeTrack (OutputStream&, const MidiMessageSequence&) const;
JUCE_LEAK_DETECTOR (MidiFile)
};
} // namespace juce

View File

@@ -1,177 +1,173 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MidiKeyboardState::MidiKeyboardState()
{
zerostruct (noteStates);
}
MidiKeyboardState::~MidiKeyboardState()
{
}
//==============================================================================
void MidiKeyboardState::reset()
{
const ScopedLock sl (lock);
zerostruct (noteStates);
eventsToAdd.clear();
}
bool MidiKeyboardState::isNoteOn (const int midiChannel, const int n) const noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
return isPositiveAndBelow (n, 128)
&& (noteStates[n] & (1 << (midiChannel - 1))) != 0;
}
bool MidiKeyboardState::isNoteOnForChannels (const int midiChannelMask, const int n) const noexcept
{
return isPositiveAndBelow (n, 128)
&& (noteStates[n] & midiChannelMask) != 0;
}
void MidiKeyboardState::noteOn (const int midiChannel, const int midiNoteNumber, const float velocity)
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (isPositiveAndBelow (midiNoteNumber, 128));
const ScopedLock sl (lock);
if (isPositiveAndBelow (midiNoteNumber, 128))
{
const int timeNow = (int) Time::getMillisecondCounter();
eventsToAdd.addEvent (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity), timeNow);
eventsToAdd.clear (0, timeNow - 500);
noteOnInternal (midiChannel, midiNoteNumber, velocity);
}
}
void MidiKeyboardState::noteOnInternal (const int midiChannel, const int midiNoteNumber, const float velocity)
{
if (isPositiveAndBelow (midiNoteNumber, 128))
{
noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] | (1 << (midiChannel - 1)));
listeners.call ([&] (Listener& l) { l.handleNoteOn (this, midiChannel, midiNoteNumber, velocity); });
}
}
void MidiKeyboardState::noteOff (const int midiChannel, const int midiNoteNumber, const float velocity)
{
const ScopedLock sl (lock);
if (isNoteOn (midiChannel, midiNoteNumber))
{
const int timeNow = (int) Time::getMillisecondCounter();
eventsToAdd.addEvent (MidiMessage::noteOff (midiChannel, midiNoteNumber), timeNow);
eventsToAdd.clear (0, timeNow - 500);
noteOffInternal (midiChannel, midiNoteNumber, velocity);
}
}
void MidiKeyboardState::noteOffInternal (const int midiChannel, const int midiNoteNumber, const float velocity)
{
if (isNoteOn (midiChannel, midiNoteNumber))
{
noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] & ~(1 << (midiChannel - 1)));
listeners.call ([&] (Listener& l) { l.handleNoteOff (this, midiChannel, midiNoteNumber, velocity); });
}
}
void MidiKeyboardState::allNotesOff (const int midiChannel)
{
const ScopedLock sl (lock);
if (midiChannel <= 0)
{
for (int i = 1; i <= 16; ++i)
allNotesOff (i);
}
else
{
for (int i = 0; i < 128; ++i)
noteOff (midiChannel, i, 0.0f);
}
}
void MidiKeyboardState::processNextMidiEvent (const MidiMessage& message)
{
if (message.isNoteOn())
{
noteOnInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity());
}
else if (message.isNoteOff())
{
noteOffInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity());
}
else if (message.isAllNotesOff())
{
for (int i = 0; i < 128; ++i)
noteOffInternal (message.getChannel(), i, 0.0f);
}
}
void MidiKeyboardState::processNextMidiBuffer (MidiBuffer& buffer,
const int startSample,
const int numSamples,
const bool injectIndirectEvents)
{
const ScopedLock sl (lock);
for (const auto metadata : buffer)
processNextMidiEvent (metadata.getMessage());
if (injectIndirectEvents)
{
const int firstEventToAdd = eventsToAdd.getFirstEventTime();
const double scaleFactor = numSamples / (double) (eventsToAdd.getLastEventTime() + 1 - firstEventToAdd);
for (const auto metadata : eventsToAdd)
{
const auto pos = jlimit (0, numSamples - 1, roundToInt ((metadata.samplePosition - firstEventToAdd) * scaleFactor));
buffer.addEvent (metadata.getMessage(), startSample + pos);
}
}
eventsToAdd.clear();
}
//==============================================================================
void MidiKeyboardState::addListener (Listener* listener)
{
const ScopedLock sl (lock);
listeners.add (listener);
}
void MidiKeyboardState::removeListener (Listener* listener)
{
const ScopedLock sl (lock);
listeners.remove (listener);
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MidiKeyboardState::MidiKeyboardState()
{
zerostruct (noteStates);
}
//==============================================================================
void MidiKeyboardState::reset()
{
const ScopedLock sl (lock);
zerostruct (noteStates);
eventsToAdd.clear();
}
bool MidiKeyboardState::isNoteOn (const int midiChannel, const int n) const noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
return isPositiveAndBelow (n, 128)
&& (noteStates[n] & (1 << (midiChannel - 1))) != 0;
}
bool MidiKeyboardState::isNoteOnForChannels (const int midiChannelMask, const int n) const noexcept
{
return isPositiveAndBelow (n, 128)
&& (noteStates[n] & midiChannelMask) != 0;
}
void MidiKeyboardState::noteOn (const int midiChannel, const int midiNoteNumber, const float velocity)
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (isPositiveAndBelow (midiNoteNumber, 128));
const ScopedLock sl (lock);
if (isPositiveAndBelow (midiNoteNumber, 128))
{
const int timeNow = (int) Time::getMillisecondCounter();
eventsToAdd.addEvent (MidiMessage::noteOn (midiChannel, midiNoteNumber, velocity), timeNow);
eventsToAdd.clear (0, timeNow - 500);
noteOnInternal (midiChannel, midiNoteNumber, velocity);
}
}
void MidiKeyboardState::noteOnInternal (const int midiChannel, const int midiNoteNumber, const float velocity)
{
if (isPositiveAndBelow (midiNoteNumber, 128))
{
noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] | (1 << (midiChannel - 1)));
listeners.call ([&] (Listener& l) { l.handleNoteOn (this, midiChannel, midiNoteNumber, velocity); });
}
}
void MidiKeyboardState::noteOff (const int midiChannel, const int midiNoteNumber, const float velocity)
{
const ScopedLock sl (lock);
if (isNoteOn (midiChannel, midiNoteNumber))
{
const int timeNow = (int) Time::getMillisecondCounter();
eventsToAdd.addEvent (MidiMessage::noteOff (midiChannel, midiNoteNumber), timeNow);
eventsToAdd.clear (0, timeNow - 500);
noteOffInternal (midiChannel, midiNoteNumber, velocity);
}
}
void MidiKeyboardState::noteOffInternal (const int midiChannel, const int midiNoteNumber, const float velocity)
{
if (isNoteOn (midiChannel, midiNoteNumber))
{
noteStates[midiNoteNumber] = static_cast<uint16> (noteStates[midiNoteNumber] & ~(1 << (midiChannel - 1)));
listeners.call ([&] (Listener& l) { l.handleNoteOff (this, midiChannel, midiNoteNumber, velocity); });
}
}
void MidiKeyboardState::allNotesOff (const int midiChannel)
{
const ScopedLock sl (lock);
if (midiChannel <= 0)
{
for (int i = 1; i <= 16; ++i)
allNotesOff (i);
}
else
{
for (int i = 0; i < 128; ++i)
noteOff (midiChannel, i, 0.0f);
}
}
void MidiKeyboardState::processNextMidiEvent (const MidiMessage& message)
{
if (message.isNoteOn())
{
noteOnInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity());
}
else if (message.isNoteOff())
{
noteOffInternal (message.getChannel(), message.getNoteNumber(), message.getFloatVelocity());
}
else if (message.isAllNotesOff())
{
for (int i = 0; i < 128; ++i)
noteOffInternal (message.getChannel(), i, 0.0f);
}
}
void MidiKeyboardState::processNextMidiBuffer (MidiBuffer& buffer,
const int startSample,
const int numSamples,
const bool injectIndirectEvents)
{
const ScopedLock sl (lock);
for (const auto metadata : buffer)
processNextMidiEvent (metadata.getMessage());
if (injectIndirectEvents)
{
const int firstEventToAdd = eventsToAdd.getFirstEventTime();
const double scaleFactor = numSamples / (double) (eventsToAdd.getLastEventTime() + 1 - firstEventToAdd);
for (const auto metadata : eventsToAdd)
{
const auto pos = jlimit (0, numSamples - 1, roundToInt ((metadata.samplePosition - firstEventToAdd) * scaleFactor));
buffer.addEvent (metadata.getMessage(), startSample + pos);
}
}
eventsToAdd.clear();
}
//==============================================================================
void MidiKeyboardState::addListener (Listener* listener)
{
const ScopedLock sl (lock);
listeners.add (listener);
}
void MidiKeyboardState::removeListener (Listener* listener)
{
const ScopedLock sl (lock);
listeners.remove (listener);
}
} // namespace juce

View File

@@ -1,196 +1,195 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Represents a piano keyboard, keeping track of which keys are currently pressed.
This object can parse a stream of midi events, using them to update its idea
of which keys are pressed for each individual midi channel.
When keys go up or down, it can broadcast these events to listener objects.
It also allows key up/down events to be triggered with its noteOn() and noteOff()
methods, and midi messages for these events will be merged into the
midi stream that gets processed by processNextMidiBuffer().
@tags{Audio}
*/
class JUCE_API MidiKeyboardState
{
public:
//==============================================================================
MidiKeyboardState();
~MidiKeyboardState();
//==============================================================================
/** Resets the state of the object.
All internal data for all the channels is reset, but no events are sent as a
result.
If you want to release any keys that are currently down, and to send out note-up
midi messages for this, use the allNotesOff() method instead.
*/
void reset();
/** Returns true if the given midi key is currently held down for the given midi channel.
The channel number must be between 1 and 16. If you want to see if any notes are
on for a range of channels, use the isNoteOnForChannels() method.
*/
bool isNoteOn (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns true if the given midi key is currently held down on any of a set of midi channels.
The channel mask has a bit set for each midi channel you want to test for - bit
0 = midi channel 1, bit 1 = midi channel 2, etc.
If a note is on for at least one of the specified channels, this returns true.
*/
bool isNoteOnForChannels (int midiChannelMask, int midiNoteNumber) const noexcept;
/** Turns a specified note on.
This will cause a suitable midi note-on event to be injected into the midi buffer during the
next call to processNextMidiBuffer().
It will also trigger a synchronous callback to the listeners to tell them that the key has
gone down.
*/
void noteOn (int midiChannel, int midiNoteNumber, float velocity);
/** Turns a specified note off.
This will cause a suitable midi note-off event to be injected into the midi buffer during the
next call to processNextMidiBuffer().
It will also trigger a synchronous callback to the listeners to tell them that the key has
gone up.
But if the note isn't actually down for the given channel, this method will in fact do nothing.
*/
void noteOff (int midiChannel, int midiNoteNumber, float velocity);
/** This will turn off any currently-down notes for the given midi channel.
If you pass 0 for the midi channel, it will in fact turn off all notes on all channels.
Calling this method will make calls to noteOff(), so can trigger synchronous callbacks
and events being added to the midi stream.
*/
void allNotesOff (int midiChannel);
//==============================================================================
/** Looks at a key-up/down event and uses it to update the state of this object.
To process a buffer full of midi messages, use the processNextMidiBuffer() method
instead.
*/
void processNextMidiEvent (const MidiMessage& message);
/** Scans a midi stream for up/down events and adds its own events to it.
This will look for any up/down events and use them to update the internal state,
synchronously making suitable callbacks to the listeners.
If injectIndirectEvents is true, then midi events to produce the recent noteOn()
and noteOff() calls will be added into the buffer.
Only the section of the buffer whose timestamps are between startSample and
(startSample + numSamples) will be affected, and any events added will be placed
between these times.
If you're going to use this method, you'll need to keep calling it regularly for
it to work satisfactorily.
To process a single midi event at a time, use the processNextMidiEvent() method
instead.
*/
void processNextMidiBuffer (MidiBuffer& buffer,
int startSample,
int numSamples,
bool injectIndirectEvents);
//==============================================================================
/** Receives events from a MidiKeyboardState object. */
class JUCE_API Listener
{
public:
//==============================================================================
virtual ~Listener() = default;
//==============================================================================
/** Called when one of the MidiKeyboardState's keys is pressed.
This will be called synchronously when the state is either processing a
buffer in its MidiKeyboardState::processNextMidiBuffer() method, or
when a note is being played with its MidiKeyboardState::noteOn() method.
Note that this callback could happen from an audio callback thread, so be
careful not to block, and avoid any UI activity in the callback.
*/
virtual void handleNoteOn (MidiKeyboardState* source,
int midiChannel, int midiNoteNumber, float velocity) = 0;
/** Called when one of the MidiKeyboardState's keys is released.
This will be called synchronously when the state is either processing a
buffer in its MidiKeyboardState::processNextMidiBuffer() method, or
when a note is being played with its MidiKeyboardState::noteOff() method.
Note that this callback could happen from an audio callback thread, so be
careful not to block, and avoid any UI activity in the callback.
*/
virtual void handleNoteOff (MidiKeyboardState* source,
int midiChannel, int midiNoteNumber, float velocity) = 0;
};
/** Registers a listener for callbacks when keys go up or down.
@see removeListener
*/
void addListener (Listener* listener);
/** Deregisters a listener.
@see addListener
*/
void removeListener (Listener* listener);
private:
//==============================================================================
CriticalSection lock;
std::atomic<uint16> noteStates[128];
MidiBuffer eventsToAdd;
ListenerList<Listener> listeners;
void noteOnInternal (int midiChannel, int midiNoteNumber, float velocity);
void noteOffInternal (int midiChannel, int midiNoteNumber, float velocity);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardState)
};
using MidiKeyboardStateListener = MidiKeyboardState::Listener;
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Represents a piano keyboard, keeping track of which keys are currently pressed.
This object can parse a stream of midi events, using them to update its idea
of which keys are pressed for each individual midi channel.
When keys go up or down, it can broadcast these events to listener objects.
It also allows key up/down events to be triggered with its noteOn() and noteOff()
methods, and midi messages for these events will be merged into the
midi stream that gets processed by processNextMidiBuffer().
@tags{Audio}
*/
class JUCE_API MidiKeyboardState
{
public:
//==============================================================================
MidiKeyboardState();
//==============================================================================
/** Resets the state of the object.
All internal data for all the channels is reset, but no events are sent as a
result.
If you want to release any keys that are currently down, and to send out note-up
midi messages for this, use the allNotesOff() method instead.
*/
void reset();
/** Returns true if the given midi key is currently held down for the given midi channel.
The channel number must be between 1 and 16. If you want to see if any notes are
on for a range of channels, use the isNoteOnForChannels() method.
*/
bool isNoteOn (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns true if the given midi key is currently held down on any of a set of midi channels.
The channel mask has a bit set for each midi channel you want to test for - bit
0 = midi channel 1, bit 1 = midi channel 2, etc.
If a note is on for at least one of the specified channels, this returns true.
*/
bool isNoteOnForChannels (int midiChannelMask, int midiNoteNumber) const noexcept;
/** Turns a specified note on.
This will cause a suitable midi note-on event to be injected into the midi buffer during the
next call to processNextMidiBuffer().
It will also trigger a synchronous callback to the listeners to tell them that the key has
gone down.
*/
void noteOn (int midiChannel, int midiNoteNumber, float velocity);
/** Turns a specified note off.
This will cause a suitable midi note-off event to be injected into the midi buffer during the
next call to processNextMidiBuffer().
It will also trigger a synchronous callback to the listeners to tell them that the key has
gone up.
But if the note isn't actually down for the given channel, this method will in fact do nothing.
*/
void noteOff (int midiChannel, int midiNoteNumber, float velocity);
/** This will turn off any currently-down notes for the given midi channel.
If you pass 0 for the midi channel, it will in fact turn off all notes on all channels.
Calling this method will make calls to noteOff(), so can trigger synchronous callbacks
and events being added to the midi stream.
*/
void allNotesOff (int midiChannel);
//==============================================================================
/** Looks at a key-up/down event and uses it to update the state of this object.
To process a buffer full of midi messages, use the processNextMidiBuffer() method
instead.
*/
void processNextMidiEvent (const MidiMessage& message);
/** Scans a midi stream for up/down events and adds its own events to it.
This will look for any up/down events and use them to update the internal state,
synchronously making suitable callbacks to the listeners.
If injectIndirectEvents is true, then midi events to produce the recent noteOn()
and noteOff() calls will be added into the buffer.
Only the section of the buffer whose timestamps are between startSample and
(startSample + numSamples) will be affected, and any events added will be placed
between these times.
If you're going to use this method, you'll need to keep calling it regularly for
it to work satisfactorily.
To process a single midi event at a time, use the processNextMidiEvent() method
instead.
*/
void processNextMidiBuffer (MidiBuffer& buffer,
int startSample,
int numSamples,
bool injectIndirectEvents);
//==============================================================================
/** Receives events from a MidiKeyboardState object. */
class JUCE_API Listener
{
public:
//==============================================================================
virtual ~Listener() = default;
//==============================================================================
/** Called when one of the MidiKeyboardState's keys is pressed.
This will be called synchronously when the state is either processing a
buffer in its MidiKeyboardState::processNextMidiBuffer() method, or
when a note is being played with its MidiKeyboardState::noteOn() method.
Note that this callback could happen from an audio callback thread, so be
careful not to block, and avoid any UI activity in the callback.
*/
virtual void handleNoteOn (MidiKeyboardState* source,
int midiChannel, int midiNoteNumber, float velocity) = 0;
/** Called when one of the MidiKeyboardState's keys is released.
This will be called synchronously when the state is either processing a
buffer in its MidiKeyboardState::processNextMidiBuffer() method, or
when a note is being played with its MidiKeyboardState::noteOff() method.
Note that this callback could happen from an audio callback thread, so be
careful not to block, and avoid any UI activity in the callback.
*/
virtual void handleNoteOff (MidiKeyboardState* source,
int midiChannel, int midiNoteNumber, float velocity) = 0;
};
/** Registers a listener for callbacks when keys go up or down.
@see removeListener
*/
void addListener (Listener* listener);
/** Deregisters a listener.
@see addListener
*/
void removeListener (Listener* listener);
private:
//==============================================================================
CriticalSection lock;
std::atomic<uint16> noteStates[128];
MidiBuffer eventsToAdd;
ListenerList<Listener> listeners;
void noteOnInternal (int midiChannel, int midiNoteNumber, float velocity);
void noteOffInternal (int midiChannel, int midiNoteNumber, float velocity);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MidiKeyboardState)
};
using MidiKeyboardStateListener = MidiKeyboardState::Listener;
} // namespace juce

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,321 +1,315 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A sequence of timestamped midi messages.
This allows the sequence to be manipulated, and also to be read from and
written to a standard midi file.
@see MidiMessage, MidiFile
@tags{Audio}
*/
class JUCE_API MidiMessageSequence
{
public:
//==============================================================================
/** Creates an empty midi sequence object. */
MidiMessageSequence();
/** Creates a copy of another sequence. */
MidiMessageSequence (const MidiMessageSequence&);
/** Replaces this sequence with another one. */
MidiMessageSequence& operator= (const MidiMessageSequence&);
/** Move constructor */
MidiMessageSequence (MidiMessageSequence&&) noexcept;
/** Move assignment operator */
MidiMessageSequence& operator= (MidiMessageSequence&&) noexcept;
/** Destructor. */
~MidiMessageSequence();
//==============================================================================
/** Structure used to hold midi events in the sequence.
These structures act as 'handles' on the events as they are moved about in
the list, and make it quick to find the matching note-offs for note-on events.
@see MidiMessageSequence::getEventPointer
*/
class MidiEventHolder
{
public:
//==============================================================================
/** Destructor. */
~MidiEventHolder();
/** The message itself, whose timestamp is used to specify the event's time. */
MidiMessage message;
/** The matching note-off event (if this is a note-on event).
If this isn't a note-on, this pointer will be nullptr.
Use the MidiMessageSequence::updateMatchedPairs() method to keep these
note-offs up-to-date after events have been moved around in the sequence
or deleted.
*/
MidiEventHolder* noteOffObject = nullptr;
private:
//==============================================================================
friend class MidiMessageSequence;
MidiEventHolder (const MidiMessage&);
MidiEventHolder (MidiMessage&&);
JUCE_LEAK_DETECTOR (MidiEventHolder)
};
//==============================================================================
/** Clears the sequence. */
void clear();
/** Returns the number of events in the sequence. */
int getNumEvents() const noexcept;
/** Returns a pointer to one of the events. */
MidiEventHolder* getEventPointer (int index) const noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder** begin() noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder* const* begin() const noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder** end() noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder* const* end() const noexcept;
/** Returns the time of the note-up that matches the note-on at this index.
If the event at this index isn't a note-on, it'll just return 0.
@see MidiMessageSequence::MidiEventHolder::noteOffObject
*/
double getTimeOfMatchingKeyUp (int index) const noexcept;
/** Returns the index of the note-up that matches the note-on at this index.
If the event at this index isn't a note-on, it'll just return -1.
@see MidiMessageSequence::MidiEventHolder::noteOffObject
*/
int getIndexOfMatchingKeyUp (int index) const noexcept;
/** Returns the index of an event. */
int getIndexOf (const MidiEventHolder* event) const noexcept;
/** Returns the index of the first event on or after the given timestamp.
If the time is beyond the end of the sequence, this will return the
number of events.
*/
int getNextIndexAtTime (double timeStamp) const noexcept;
//==============================================================================
/** Returns the timestamp of the first event in the sequence.
@see getEndTime
*/
double getStartTime() const noexcept;
/** Returns the timestamp of the last event in the sequence.
@see getStartTime
*/
double getEndTime() const noexcept;
/** Returns the timestamp of the event at a given index.
If the index is out-of-range, this will return 0.0
*/
double getEventTime (int index) const noexcept;
//==============================================================================
/** Inserts a midi message into the sequence.
The index at which the new message gets inserted will depend on its timestamp,
because the sequence is kept sorted.
Remember to call updateMatchedPairs() after adding note-on events.
@param newMessage the new message to add (an internal copy will be made)
@param timeAdjustment an optional value to add to the timestamp of the message
that will be inserted
@see updateMatchedPairs
*/
MidiEventHolder* addEvent (const MidiMessage& newMessage, double timeAdjustment = 0);
/** Inserts a midi message into the sequence.
The index at which the new message gets inserted will depend on its timestamp,
because the sequence is kept sorted.
Remember to call updateMatchedPairs() after adding note-on events.
@param newMessage the new message to add (an internal copy will be made)
@param timeAdjustment an optional value to add to the timestamp of the message
that will be inserted
@see updateMatchedPairs
*/
MidiEventHolder* addEvent (MidiMessage&& newMessage, double timeAdjustment = 0);
/** Deletes one of the events in the sequence.
Remember to call updateMatchedPairs() after removing events.
@param index the index of the event to delete
@param deleteMatchingNoteUp whether to also remove the matching note-off
if the event you're removing is a note-on
*/
void deleteEvent (int index, bool deleteMatchingNoteUp);
/** Merges another sequence into this one.
Remember to call updateMatchedPairs() after using this method.
@param other the sequence to add from
@param timeAdjustmentDelta an amount to add to the timestamps of the midi events
as they are read from the other sequence
@param firstAllowableDestTime events will not be added if their time is earlier
than this time. (This is after their time has been adjusted
by the timeAdjustmentDelta)
@param endOfAllowableDestTimes events will not be added if their time is equal to
or greater than this time. (This is after their time has
been adjusted by the timeAdjustmentDelta)
*/
void addSequence (const MidiMessageSequence& other,
double timeAdjustmentDelta,
double firstAllowableDestTime,
double endOfAllowableDestTimes);
/** Merges another sequence into this one.
Remember to call updateMatchedPairs() after using this method.
@param other the sequence to add from
@param timeAdjustmentDelta an amount to add to the timestamps of the midi events
as they are read from the other sequence
*/
void addSequence (const MidiMessageSequence& other,
double timeAdjustmentDelta);
//==============================================================================
/** Makes sure all the note-on and note-off pairs are up-to-date.
Call this after re-ordering messages or deleting/adding messages, and it
will scan the list and make sure all the note-offs in the MidiEventHolder
structures are pointing at the correct ones.
*/
void updateMatchedPairs() noexcept;
/** Forces a sort of the sequence.
You may need to call this if you've manually modified the timestamps of some
events such that the overall order now needs updating.
*/
void sort() noexcept;
//==============================================================================
/** Copies all the messages for a particular midi channel to another sequence.
@param channelNumberToExtract the midi channel to look for, in the range 1 to 16
@param destSequence the sequence that the chosen events should be copied to
@param alsoIncludeMetaEvents if true, any meta-events (which don't apply to a specific
channel) will also be copied across.
@see extractSysExMessages
*/
void extractMidiChannelMessages (int channelNumberToExtract,
MidiMessageSequence& destSequence,
bool alsoIncludeMetaEvents) const;
/** Copies all midi sys-ex messages to another sequence.
@param destSequence this is the sequence to which any sys-exes in this sequence
will be added
@see extractMidiChannelMessages
*/
void extractSysExMessages (MidiMessageSequence& destSequence) const;
/** Removes any messages in this sequence that have a specific midi channel.
@param channelNumberToRemove the midi channel to look for, in the range 1 to 16
*/
void deleteMidiChannelMessages (int channelNumberToRemove);
/** Removes any sys-ex messages from this sequence. */
void deleteSysExMessages();
/** Adds an offset to the timestamps of all events in the sequence.
@param deltaTime the amount to add to each timestamp.
*/
void addTimeToMessages (double deltaTime) noexcept;
//==============================================================================
/** Scans through the sequence to determine the state of any midi controllers at
a given time.
This will create a sequence of midi controller changes that can be
used to set all midi controllers to the state they would be in at the
specified time within this sequence.
As well as controllers, it will also recreate the midi program number
and pitch bend position.
This function has special handling for the "bank select" and "data entry"
controllers (0x00, 0x20, 0x06, 0x26, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65).
If the sequence contains multiple bank select and program change messages,
only the bank select messages immediately preceding the final program change
message will be kept.
All "data increment" and "data decrement" messages will be retained. Some hardware will
ignore the requested increment/decrement values, so retaining all messages is the only
way to ensure compatibility with all hardware.
"Parameter number" changes will be slightly condensed. Only the parameter number
events immediately preceding each data entry event will be kept. The parameter number
will also be set to its final value at the end of the sequence, if necessary.
@param channelNumber the midi channel to look for, in the range 1 to 16. Controllers
for other channels will be ignored.
@param time the time at which you want to find out the state - there are
no explicit units for this time measurement, it's the same units
as used for the timestamps of the messages
@param resultMessages an array to which midi controller-change messages will be added. This
will be the minimum number of controller changes to recreate the
state at the required time.
*/
void createControllerUpdatesForTime (int channelNumber, double time,
Array<MidiMessage>& resultMessages);
//==============================================================================
/** Swaps this sequence with another one. */
void swapWith (MidiMessageSequence&) noexcept;
private:
//==============================================================================
friend class MidiFile;
OwnedArray<MidiEventHolder> list;
MidiEventHolder* addEvent (MidiEventHolder*, double);
JUCE_LEAK_DETECTOR (MidiMessageSequence)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A sequence of timestamped midi messages.
This allows the sequence to be manipulated, and also to be read from and
written to a standard midi file.
@see MidiMessage, MidiFile
@tags{Audio}
*/
class JUCE_API MidiMessageSequence
{
public:
//==============================================================================
/** Creates an empty midi sequence object. */
MidiMessageSequence();
/** Creates a copy of another sequence. */
MidiMessageSequence (const MidiMessageSequence&);
/** Replaces this sequence with another one. */
MidiMessageSequence& operator= (const MidiMessageSequence&);
/** Move constructor */
MidiMessageSequence (MidiMessageSequence&&) noexcept;
/** Move assignment operator */
MidiMessageSequence& operator= (MidiMessageSequence&&) noexcept;
//==============================================================================
/** Structure used to hold midi events in the sequence.
These structures act as 'handles' on the events as they are moved about in
the list, and make it quick to find the matching note-offs for note-on events.
@see MidiMessageSequence::getEventPointer
*/
class MidiEventHolder
{
public:
//==============================================================================
/** The message itself, whose timestamp is used to specify the event's time. */
MidiMessage message;
/** The matching note-off event (if this is a note-on event).
If this isn't a note-on, this pointer will be nullptr.
Use the MidiMessageSequence::updateMatchedPairs() method to keep these
note-offs up-to-date after events have been moved around in the sequence
or deleted.
*/
MidiEventHolder* noteOffObject = nullptr;
private:
//==============================================================================
friend class MidiMessageSequence;
MidiEventHolder (const MidiMessage&);
MidiEventHolder (MidiMessage&&);
JUCE_LEAK_DETECTOR (MidiEventHolder)
};
//==============================================================================
/** Clears the sequence. */
void clear();
/** Returns the number of events in the sequence. */
int getNumEvents() const noexcept;
/** Returns a pointer to one of the events. */
MidiEventHolder* getEventPointer (int index) const noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder** begin() noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder* const* begin() const noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder** end() noexcept;
/** Iterator for the list of MidiEventHolders */
MidiEventHolder* const* end() const noexcept;
/** Returns the time of the note-up that matches the note-on at this index.
If the event at this index isn't a note-on, it'll just return 0.
@see MidiMessageSequence::MidiEventHolder::noteOffObject
*/
double getTimeOfMatchingKeyUp (int index) const noexcept;
/** Returns the index of the note-up that matches the note-on at this index.
If the event at this index isn't a note-on, it'll just return -1.
@see MidiMessageSequence::MidiEventHolder::noteOffObject
*/
int getIndexOfMatchingKeyUp (int index) const noexcept;
/** Returns the index of an event. */
int getIndexOf (const MidiEventHolder* event) const noexcept;
/** Returns the index of the first event on or after the given timestamp.
If the time is beyond the end of the sequence, this will return the
number of events.
*/
int getNextIndexAtTime (double timeStamp) const noexcept;
//==============================================================================
/** Returns the timestamp of the first event in the sequence.
@see getEndTime
*/
double getStartTime() const noexcept;
/** Returns the timestamp of the last event in the sequence.
@see getStartTime
*/
double getEndTime() const noexcept;
/** Returns the timestamp of the event at a given index.
If the index is out-of-range, this will return 0.0
*/
double getEventTime (int index) const noexcept;
//==============================================================================
/** Inserts a midi message into the sequence.
The index at which the new message gets inserted will depend on its timestamp,
because the sequence is kept sorted.
Remember to call updateMatchedPairs() after adding note-on events.
@param newMessage the new message to add (an internal copy will be made)
@param timeAdjustment an optional value to add to the timestamp of the message
that will be inserted
@see updateMatchedPairs
*/
MidiEventHolder* addEvent (const MidiMessage& newMessage, double timeAdjustment = 0);
/** Inserts a midi message into the sequence.
The index at which the new message gets inserted will depend on its timestamp,
because the sequence is kept sorted.
Remember to call updateMatchedPairs() after adding note-on events.
@param newMessage the new message to add (an internal copy will be made)
@param timeAdjustment an optional value to add to the timestamp of the message
that will be inserted
@see updateMatchedPairs
*/
MidiEventHolder* addEvent (MidiMessage&& newMessage, double timeAdjustment = 0);
/** Deletes one of the events in the sequence.
Remember to call updateMatchedPairs() after removing events.
@param index the index of the event to delete
@param deleteMatchingNoteUp whether to also remove the matching note-off
if the event you're removing is a note-on
*/
void deleteEvent (int index, bool deleteMatchingNoteUp);
/** Merges another sequence into this one.
Remember to call updateMatchedPairs() after using this method.
@param other the sequence to add from
@param timeAdjustmentDelta an amount to add to the timestamps of the midi events
as they are read from the other sequence
@param firstAllowableDestTime events will not be added if their time is earlier
than this time. (This is after their time has been adjusted
by the timeAdjustmentDelta)
@param endOfAllowableDestTimes events will not be added if their time is equal to
or greater than this time. (This is after their time has
been adjusted by the timeAdjustmentDelta)
*/
void addSequence (const MidiMessageSequence& other,
double timeAdjustmentDelta,
double firstAllowableDestTime,
double endOfAllowableDestTimes);
/** Merges another sequence into this one.
Remember to call updateMatchedPairs() after using this method.
@param other the sequence to add from
@param timeAdjustmentDelta an amount to add to the timestamps of the midi events
as they are read from the other sequence
*/
void addSequence (const MidiMessageSequence& other,
double timeAdjustmentDelta);
//==============================================================================
/** Makes sure all the note-on and note-off pairs are up-to-date.
Call this after re-ordering messages or deleting/adding messages, and it
will scan the list and make sure all the note-offs in the MidiEventHolder
structures are pointing at the correct ones.
*/
void updateMatchedPairs() noexcept;
/** Forces a sort of the sequence.
You may need to call this if you've manually modified the timestamps of some
events such that the overall order now needs updating.
*/
void sort() noexcept;
//==============================================================================
/** Copies all the messages for a particular midi channel to another sequence.
@param channelNumberToExtract the midi channel to look for, in the range 1 to 16
@param destSequence the sequence that the chosen events should be copied to
@param alsoIncludeMetaEvents if true, any meta-events (which don't apply to a specific
channel) will also be copied across.
@see extractSysExMessages
*/
void extractMidiChannelMessages (int channelNumberToExtract,
MidiMessageSequence& destSequence,
bool alsoIncludeMetaEvents) const;
/** Copies all midi sys-ex messages to another sequence.
@param destSequence this is the sequence to which any sys-exes in this sequence
will be added
@see extractMidiChannelMessages
*/
void extractSysExMessages (MidiMessageSequence& destSequence) const;
/** Removes any messages in this sequence that have a specific midi channel.
@param channelNumberToRemove the midi channel to look for, in the range 1 to 16
*/
void deleteMidiChannelMessages (int channelNumberToRemove);
/** Removes any sys-ex messages from this sequence. */
void deleteSysExMessages();
/** Adds an offset to the timestamps of all events in the sequence.
@param deltaTime the amount to add to each timestamp.
*/
void addTimeToMessages (double deltaTime) noexcept;
//==============================================================================
/** Scans through the sequence to determine the state of any midi controllers at
a given time.
This will create a sequence of midi controller changes that can be
used to set all midi controllers to the state they would be in at the
specified time within this sequence.
As well as controllers, it will also recreate the midi program number
and pitch bend position.
This function has special handling for the "bank select" and "data entry"
controllers (0x00, 0x20, 0x06, 0x26, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65).
If the sequence contains multiple bank select and program change messages,
only the bank select messages immediately preceding the final program change
message will be kept.
All "data increment" and "data decrement" messages will be retained. Some hardware will
ignore the requested increment/decrement values, so retaining all messages is the only
way to ensure compatibility with all hardware.
"Parameter number" changes will be slightly condensed. Only the parameter number
events immediately preceding each data entry event will be kept. The parameter number
will also be set to its final value at the end of the sequence, if necessary.
@param channelNumber the midi channel to look for, in the range 1 to 16. Controllers
for other channels will be ignored.
@param time the time at which you want to find out the state - there are
no explicit units for this time measurement, it's the same units
as used for the timestamps of the messages
@param resultMessages an array to which midi controller-change messages will be added. This
will be the minimum number of controller changes to recreate the
state at the required time.
*/
void createControllerUpdatesForTime (int channelNumber, double time,
Array<MidiMessage>& resultMessages);
//==============================================================================
/** Swaps this sequence with another one. */
void swapWith (MidiMessageSequence&) noexcept;
private:
//==============================================================================
friend class MidiFile;
OwnedArray<MidiEventHolder> list;
MidiEventHolder* addEvent (MidiEventHolder*, double);
JUCE_LEAK_DETECTOR (MidiMessageSequence)
};
} // namespace juce

View File

@@ -1,380 +1,375 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MidiRPNDetector::MidiRPNDetector() noexcept
{
}
MidiRPNDetector::~MidiRPNDetector() noexcept
{
}
bool MidiRPNDetector::parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (controllerNumber >= 0 && controllerNumber < 128);
jassert (controllerValue >= 0 && controllerValue < 128);
return states[midiChannel - 1].handleController (midiChannel, controllerNumber, controllerValue, result);
}
void MidiRPNDetector::reset() noexcept
{
for (int i = 0; i < 16; ++i)
{
states[i].parameterMSB = 0xff;
states[i].parameterLSB = 0xff;
states[i].resetValue();
states[i].isNRPN = false;
}
}
//==============================================================================
MidiRPNDetector::ChannelState::ChannelState() noexcept
: parameterMSB (0xff), parameterLSB (0xff), valueMSB (0xff), valueLSB (0xff), isNRPN (false)
{
}
bool MidiRPNDetector::ChannelState::handleController (int channel,
int controllerNumber,
int value,
MidiRPNMessage& result) noexcept
{
switch (controllerNumber)
{
case 0x62: parameterLSB = uint8 (value); resetValue(); isNRPN = true; break;
case 0x63: parameterMSB = uint8 (value); resetValue(); isNRPN = true; break;
case 0x64: parameterLSB = uint8 (value); resetValue(); isNRPN = false; break;
case 0x65: parameterMSB = uint8 (value); resetValue(); isNRPN = false; break;
case 0x06: valueMSB = uint8 (value); return sendIfReady (channel, result);
case 0x26: valueLSB = uint8 (value); break;
default: break;
}
return false;
}
void MidiRPNDetector::ChannelState::resetValue() noexcept
{
valueMSB = 0xff;
valueLSB = 0xff;
}
//==============================================================================
bool MidiRPNDetector::ChannelState::sendIfReady (int channel, MidiRPNMessage& result) noexcept
{
if (parameterMSB < 0x80 && parameterLSB < 0x80)
{
if (valueMSB < 0x80)
{
result.channel = channel;
result.parameterNumber = (parameterMSB << 7) + parameterLSB;
result.isNRPN = isNRPN;
if (valueLSB < 0x80)
{
result.value = (valueMSB << 7) + valueLSB;
result.is14BitValue = true;
}
else
{
result.value = valueMSB;
result.is14BitValue = false;
}
return true;
}
}
return false;
}
//==============================================================================
MidiBuffer MidiRPNGenerator::generate (MidiRPNMessage message)
{
return generate (message.channel,
message.parameterNumber,
message.value,
message.isNRPN,
message.is14BitValue);
}
MidiBuffer MidiRPNGenerator::generate (int midiChannel,
int parameterNumber,
int value,
bool isNRPN,
bool use14BitValue)
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (parameterNumber >= 0 && parameterNumber < 16384);
jassert (value >= 0 && value < (use14BitValue ? 16384 : 128));
uint8 parameterLSB = uint8 (parameterNumber & 0x0000007f);
uint8 parameterMSB = uint8 (parameterNumber >> 7);
uint8 valueLSB = use14BitValue ? uint8 (value & 0x0000007f) : 0x00;
uint8 valueMSB = use14BitValue ? uint8 (value >> 7) : uint8 (value);
uint8 channelByte = uint8 (0xb0 + midiChannel - 1);
MidiBuffer buffer;
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x62 : 0x64, parameterLSB), 0);
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x63 : 0x65, parameterMSB), 0);
// sending the value LSB is optional, but must come before sending the value MSB:
if (use14BitValue)
buffer.addEvent (MidiMessage (channelByte, 0x26, valueLSB), 0);
buffer.addEvent (MidiMessage (channelByte, 0x06, valueMSB), 0);
return buffer;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MidiRPNDetectorTests : public UnitTest
{
public:
MidiRPNDetectorTests()
: UnitTest ("MidiRPNDetector class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("7-bit RPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (detector.parseControllerMessage (2, 6, 42, rpn));
expectEquals (rpn.channel, 2);
expectEquals (rpn.parameterNumber, 7);
expectEquals (rpn.value, 42);
expect (! rpn.isNRPN);
expect (! rpn.is14BitValue);
}
beginTest ("14-bit RPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 100, 44, rpn));
expect (! detector.parseControllerMessage (1, 101, 2, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (1, 6, 1, rpn));
expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("RPNs on multiple channels simultaneously");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 100, 44, rpn));
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (1, 101, 2, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (2, 6, 42, rpn));
expectEquals (rpn.channel, 2);
expectEquals (rpn.parameterNumber, 7);
expectEquals (rpn.value, 42);
expect (! rpn.isNRPN);
expect (! rpn.is14BitValue);
expect (detector.parseControllerMessage (1, 6, 1, rpn));
expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("14-bit RPN with value within 7-bit range");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (16, 100, 0 , rpn));
expect (! detector.parseControllerMessage (16, 101, 0, rpn));
expect (! detector.parseControllerMessage (16, 38, 3, rpn));
expect (detector.parseControllerMessage (16, 6, 0, rpn));
expectEquals (rpn.channel, 16);
expectEquals (rpn.parameterNumber, 0);
expectEquals (rpn.value, 3);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("invalid RPN (wrong order)");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 6, 42, rpn));
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
}
beginTest ("14-bit RPN interspersed with unrelated CC messages");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (16, 3, 80, rpn));
expect (! detector.parseControllerMessage (16, 100, 0 , rpn));
expect (! detector.parseControllerMessage (16, 4, 81, rpn));
expect (! detector.parseControllerMessage (16, 101, 0, rpn));
expect (! detector.parseControllerMessage (16, 5, 82, rpn));
expect (! detector.parseControllerMessage (16, 5, 83, rpn));
expect (! detector.parseControllerMessage (16, 38, 3, rpn));
expect (! detector.parseControllerMessage (16, 4, 84, rpn));
expect (! detector.parseControllerMessage (16, 3, 85, rpn));
expect (detector.parseControllerMessage (16, 6, 0, rpn));
expectEquals (rpn.channel, 16);
expectEquals (rpn.parameterNumber, 0);
expectEquals (rpn.value, 3);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("14-bit NRPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 98, 44, rpn));
expect (! detector.parseControllerMessage (1, 99 , 2, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (1, 6, 1, rpn));
expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("reset");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
detector.reset();
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (! detector.parseControllerMessage (2, 6, 42, rpn));
}
}
};
static MidiRPNDetectorTests MidiRPNDetectorUnitTests;
//==============================================================================
class MidiRPNGeneratorTests : public UnitTest
{
public:
MidiRPNGeneratorTests()
: UnitTest ("MidiRPNGenerator class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("generating RPN/NRPN");
{
{
MidiBuffer buffer = MidiRPNGenerator::generate (1, 23, 1337, true, true);
expectContainsRPN (buffer, 1, 23, 1337, true, true);
}
{
MidiBuffer buffer = MidiRPNGenerator::generate (16, 101, 34, false, false);
expectContainsRPN (buffer, 16, 101, 34, false, false);
}
{
MidiRPNMessage message = { 16, 101, 34, false, false };
MidiBuffer buffer = MidiRPNGenerator::generate (message);
expectContainsRPN (buffer, message);
}
}
}
private:
//==============================================================================
void expectContainsRPN (const MidiBuffer& midiBuffer,
int channel,
int parameterNumber,
int value,
bool isNRPN,
bool is14BitValue)
{
MidiRPNMessage expected = { channel, parameterNumber, value, isNRPN, is14BitValue };
expectContainsRPN (midiBuffer, expected);
}
//==============================================================================
void expectContainsRPN (const MidiBuffer& midiBuffer, MidiRPNMessage expected)
{
MidiRPNMessage result = MidiRPNMessage();
MidiRPNDetector detector;
for (const auto metadata : midiBuffer)
{
const auto midiMessage = metadata.getMessage();
if (detector.parseControllerMessage (midiMessage.getChannel(),
midiMessage.getControllerNumber(),
midiMessage.getControllerValue(),
result))
break;
}
expectEquals (result.channel, expected.channel);
expectEquals (result.parameterNumber, expected.parameterNumber);
expectEquals (result.value, expected.value);
expect (result.isNRPN == expected.isNRPN);
expect (result.is14BitValue == expected.is14BitValue);
}
};
static MidiRPNGeneratorTests MidiRPNGeneratorUnitTests;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MidiRPNDetector::MidiRPNDetector() noexcept
{
}
MidiRPNDetector::~MidiRPNDetector() noexcept
{
}
bool MidiRPNDetector::parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (controllerNumber >= 0 && controllerNumber < 128);
jassert (controllerValue >= 0 && controllerValue < 128);
return states[midiChannel - 1].handleController (midiChannel, controllerNumber, controllerValue, result);
}
void MidiRPNDetector::reset() noexcept
{
for (int i = 0; i < 16; ++i)
{
states[i].parameterMSB = 0xff;
states[i].parameterLSB = 0xff;
states[i].resetValue();
states[i].isNRPN = false;
}
}
//==============================================================================
bool MidiRPNDetector::ChannelState::handleController (int channel,
int controllerNumber,
int value,
MidiRPNMessage& result) noexcept
{
switch (controllerNumber)
{
case 0x62: parameterLSB = uint8 (value); resetValue(); isNRPN = true; break;
case 0x63: parameterMSB = uint8 (value); resetValue(); isNRPN = true; break;
case 0x64: parameterLSB = uint8 (value); resetValue(); isNRPN = false; break;
case 0x65: parameterMSB = uint8 (value); resetValue(); isNRPN = false; break;
case 0x06: valueMSB = uint8 (value); return sendIfReady (channel, result);
case 0x26: valueLSB = uint8 (value); break;
default: break;
}
return false;
}
void MidiRPNDetector::ChannelState::resetValue() noexcept
{
valueMSB = 0xff;
valueLSB = 0xff;
}
//==============================================================================
bool MidiRPNDetector::ChannelState::sendIfReady (int channel, MidiRPNMessage& result) noexcept
{
if (parameterMSB < 0x80 && parameterLSB < 0x80)
{
if (valueMSB < 0x80)
{
result.channel = channel;
result.parameterNumber = (parameterMSB << 7) + parameterLSB;
result.isNRPN = isNRPN;
if (valueLSB < 0x80)
{
result.value = (valueMSB << 7) + valueLSB;
result.is14BitValue = true;
}
else
{
result.value = valueMSB;
result.is14BitValue = false;
}
return true;
}
}
return false;
}
//==============================================================================
MidiBuffer MidiRPNGenerator::generate (MidiRPNMessage message)
{
return generate (message.channel,
message.parameterNumber,
message.value,
message.isNRPN,
message.is14BitValue);
}
MidiBuffer MidiRPNGenerator::generate (int midiChannel,
int parameterNumber,
int value,
bool isNRPN,
bool use14BitValue)
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (parameterNumber >= 0 && parameterNumber < 16384);
jassert (value >= 0 && value < (use14BitValue ? 16384 : 128));
uint8 parameterLSB = uint8 (parameterNumber & 0x0000007f);
uint8 parameterMSB = uint8 (parameterNumber >> 7);
uint8 valueLSB = use14BitValue ? uint8 (value & 0x0000007f) : 0x00;
uint8 valueMSB = use14BitValue ? uint8 (value >> 7) : uint8 (value);
uint8 channelByte = uint8 (0xb0 + midiChannel - 1);
MidiBuffer buffer;
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x62 : 0x64, parameterLSB), 0);
buffer.addEvent (MidiMessage (channelByte, isNRPN ? 0x63 : 0x65, parameterMSB), 0);
// sending the value LSB is optional, but must come before sending the value MSB:
if (use14BitValue)
buffer.addEvent (MidiMessage (channelByte, 0x26, valueLSB), 0);
buffer.addEvent (MidiMessage (channelByte, 0x06, valueMSB), 0);
return buffer;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MidiRPNDetectorTests : public UnitTest
{
public:
MidiRPNDetectorTests()
: UnitTest ("MidiRPNDetector class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("7-bit RPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (detector.parseControllerMessage (2, 6, 42, rpn));
expectEquals (rpn.channel, 2);
expectEquals (rpn.parameterNumber, 7);
expectEquals (rpn.value, 42);
expect (! rpn.isNRPN);
expect (! rpn.is14BitValue);
}
beginTest ("14-bit RPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 100, 44, rpn));
expect (! detector.parseControllerMessage (1, 101, 2, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (1, 6, 1, rpn));
expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("RPNs on multiple channels simultaneously");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 100, 44, rpn));
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (1, 101, 2, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (2, 6, 42, rpn));
expectEquals (rpn.channel, 2);
expectEquals (rpn.parameterNumber, 7);
expectEquals (rpn.value, 42);
expect (! rpn.isNRPN);
expect (! rpn.is14BitValue);
expect (detector.parseControllerMessage (1, 6, 1, rpn));
expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("14-bit RPN with value within 7-bit range");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (16, 100, 0 , rpn));
expect (! detector.parseControllerMessage (16, 101, 0, rpn));
expect (! detector.parseControllerMessage (16, 38, 3, rpn));
expect (detector.parseControllerMessage (16, 6, 0, rpn));
expectEquals (rpn.channel, 16);
expectEquals (rpn.parameterNumber, 0);
expectEquals (rpn.value, 3);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("invalid RPN (wrong order)");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 6, 42, rpn));
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
}
beginTest ("14-bit RPN interspersed with unrelated CC messages");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (16, 3, 80, rpn));
expect (! detector.parseControllerMessage (16, 100, 0 , rpn));
expect (! detector.parseControllerMessage (16, 4, 81, rpn));
expect (! detector.parseControllerMessage (16, 101, 0, rpn));
expect (! detector.parseControllerMessage (16, 5, 82, rpn));
expect (! detector.parseControllerMessage (16, 5, 83, rpn));
expect (! detector.parseControllerMessage (16, 38, 3, rpn));
expect (! detector.parseControllerMessage (16, 4, 84, rpn));
expect (! detector.parseControllerMessage (16, 3, 85, rpn));
expect (detector.parseControllerMessage (16, 6, 0, rpn));
expectEquals (rpn.channel, 16);
expectEquals (rpn.parameterNumber, 0);
expectEquals (rpn.value, 3);
expect (! rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("14-bit NRPN");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (1, 98, 44, rpn));
expect (! detector.parseControllerMessage (1, 99 , 2, rpn));
expect (! detector.parseControllerMessage (1, 38, 94, rpn));
expect (detector.parseControllerMessage (1, 6, 1, rpn));
expectEquals (rpn.channel, 1);
expectEquals (rpn.parameterNumber, 300);
expectEquals (rpn.value, 222);
expect (rpn.isNRPN);
expect (rpn.is14BitValue);
}
beginTest ("reset");
{
MidiRPNDetector detector;
MidiRPNMessage rpn;
expect (! detector.parseControllerMessage (2, 101, 0, rpn));
detector.reset();
expect (! detector.parseControllerMessage (2, 100, 7, rpn));
expect (! detector.parseControllerMessage (2, 6, 42, rpn));
}
}
};
static MidiRPNDetectorTests MidiRPNDetectorUnitTests;
//==============================================================================
class MidiRPNGeneratorTests : public UnitTest
{
public:
MidiRPNGeneratorTests()
: UnitTest ("MidiRPNGenerator class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("generating RPN/NRPN");
{
{
MidiBuffer buffer = MidiRPNGenerator::generate (1, 23, 1337, true, true);
expectContainsRPN (buffer, 1, 23, 1337, true, true);
}
{
MidiBuffer buffer = MidiRPNGenerator::generate (16, 101, 34, false, false);
expectContainsRPN (buffer, 16, 101, 34, false, false);
}
{
MidiRPNMessage message = { 16, 101, 34, false, false };
MidiBuffer buffer = MidiRPNGenerator::generate (message);
expectContainsRPN (buffer, message);
}
}
}
private:
//==============================================================================
void expectContainsRPN (const MidiBuffer& midiBuffer,
int channel,
int parameterNumber,
int value,
bool isNRPN,
bool is14BitValue)
{
MidiRPNMessage expected = { channel, parameterNumber, value, isNRPN, is14BitValue };
expectContainsRPN (midiBuffer, expected);
}
//==============================================================================
void expectContainsRPN (const MidiBuffer& midiBuffer, MidiRPNMessage expected)
{
MidiRPNMessage result = MidiRPNMessage();
MidiRPNDetector detector;
for (const auto metadata : midiBuffer)
{
const auto midiMessage = metadata.getMessage();
if (detector.parseControllerMessage (midiMessage.getChannel(),
midiMessage.getControllerNumber(),
midiMessage.getControllerValue(),
result))
break;
}
expectEquals (result.channel, expected.channel);
expectEquals (result.parameterNumber, expected.parameterNumber);
expectEquals (result.value, expected.value);
expect (result.isNRPN == expected.isNRPN);
expect (result.is14BitValue == expected.is14BitValue);
}
};
static MidiRPNGeneratorTests MidiRPNGeneratorUnitTests;
#endif
} // namespace juce

View File

@@ -1,154 +1,153 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/** Represents a MIDI RPN (registered parameter number) or NRPN (non-registered
parameter number) message.
@tags{Audio}
*/
struct MidiRPNMessage
{
/** Midi channel of the message, in the range 1 to 16. */
int channel;
/** The 14-bit parameter index, in the range 0 to 16383 (0x3fff). */
int parameterNumber;
/** The parameter value, in the range 0 to 16383 (0x3fff).
If the message contains no value LSB, the value will be in the range
0 to 127 (0x7f).
*/
int value;
/** True if this message is an NRPN; false if it is an RPN. */
bool isNRPN;
/** True if the value uses 14-bit resolution (LSB + MSB); false if
the value is 7-bit (MSB only).
*/
bool is14BitValue;
};
//==============================================================================
/**
Parses a stream of MIDI data to assemble RPN and NRPN messages from their
constituent MIDI CC messages.
The detector uses the following parsing rules: the parameter number
LSB/MSB can be sent/received in either order and must both come before the
parameter value; for the parameter value, LSB always has to be sent/received
before the value MSB, otherwise it will be treated as 7-bit (MSB only).
@tags{Audio}
*/
class JUCE_API MidiRPNDetector
{
public:
/** Constructor. */
MidiRPNDetector() noexcept;
/** Destructor. */
~MidiRPNDetector() noexcept;
/** Resets the RPN detector's internal state, so that it forgets about
previously received MIDI CC messages.
*/
void reset() noexcept;
//==============================================================================
/** Takes the next in a stream of incoming MIDI CC messages and returns true
if it forms the last of a sequence that makes an RPN or NPRN.
If this returns true, then the RPNMessage object supplied will be
filled-out with the message's details.
(If it returns false then the RPNMessage object will be unchanged).
*/
bool parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept;
private:
//==============================================================================
struct ChannelState
{
ChannelState() noexcept;
bool handleController (int channel, int controllerNumber,
int value, MidiRPNMessage&) noexcept;
void resetValue() noexcept;
bool sendIfReady (int channel, MidiRPNMessage&) noexcept;
uint8 parameterMSB, parameterLSB, valueMSB, valueLSB;
bool isNRPN;
};
//==============================================================================
ChannelState states[16];
JUCE_LEAK_DETECTOR (MidiRPNDetector)
};
//==============================================================================
/**
Generates an appropriate sequence of MIDI CC messages to represent an RPN
or NRPN message.
This sequence (as a MidiBuffer) can then be directly sent to a MidiOutput.
@tags{Audio}
*/
class JUCE_API MidiRPNGenerator
{
public:
//==============================================================================
/** Generates a MIDI sequence representing the given RPN or NRPN message. */
static MidiBuffer generate (MidiRPNMessage message);
//==============================================================================
/** Generates a MIDI sequence representing an RPN or NRPN message with the
given parameters.
@param channel The MIDI channel of the RPN/NRPN message.
@param parameterNumber The parameter number, in the range 0 to 16383.
@param value The parameter value, in the range 0 to 16383, or
in the range 0 to 127 if sendAs14BitValue is false.
@param isNRPN Whether you need a MIDI RPN or NRPN sequence (RPN is default).
@param use14BitValue If true (default), the value will have 14-bit precision
(two MIDI bytes). If false, instead the value will have
7-bit precision (a single MIDI byte).
*/
static MidiBuffer generate (int channel,
int parameterNumber,
int value,
bool isNRPN = false,
bool use14BitValue = true);
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/** Represents a MIDI RPN (registered parameter number) or NRPN (non-registered
parameter number) message.
@tags{Audio}
*/
struct MidiRPNMessage
{
/** Midi channel of the message, in the range 1 to 16. */
int channel;
/** The 14-bit parameter index, in the range 0 to 16383 (0x3fff). */
int parameterNumber;
/** The parameter value, in the range 0 to 16383 (0x3fff).
If the message contains no value LSB, the value will be in the range
0 to 127 (0x7f).
*/
int value;
/** True if this message is an NRPN; false if it is an RPN. */
bool isNRPN;
/** True if the value uses 14-bit resolution (LSB + MSB); false if
the value is 7-bit (MSB only).
*/
bool is14BitValue;
};
//==============================================================================
/**
Parses a stream of MIDI data to assemble RPN and NRPN messages from their
constituent MIDI CC messages.
The detector uses the following parsing rules: the parameter number
LSB/MSB can be sent/received in either order and must both come before the
parameter value; for the parameter value, LSB always has to be sent/received
before the value MSB, otherwise it will be treated as 7-bit (MSB only).
@tags{Audio}
*/
class JUCE_API MidiRPNDetector
{
public:
/** Constructor. */
MidiRPNDetector() noexcept;
/** Destructor. */
~MidiRPNDetector() noexcept;
/** Resets the RPN detector's internal state, so that it forgets about
previously received MIDI CC messages.
*/
void reset() noexcept;
//==============================================================================
/** Takes the next in a stream of incoming MIDI CC messages and returns true
if it forms the last of a sequence that makes an RPN or NPRN.
If this returns true, then the RPNMessage object supplied will be
filled-out with the message's details.
(If it returns false then the RPNMessage object will be unchanged).
*/
bool parseControllerMessage (int midiChannel,
int controllerNumber,
int controllerValue,
MidiRPNMessage& result) noexcept;
private:
//==============================================================================
struct ChannelState
{
bool handleController (int channel, int controllerNumber,
int value, MidiRPNMessage&) noexcept;
void resetValue() noexcept;
bool sendIfReady (int channel, MidiRPNMessage&) noexcept;
uint8 parameterMSB = 0xff, parameterLSB = 0xff, valueMSB = 0xff, valueLSB = 0xff;
bool isNRPN = false;
};
//==============================================================================
ChannelState states[16];
JUCE_LEAK_DETECTOR (MidiRPNDetector)
};
//==============================================================================
/**
Generates an appropriate sequence of MIDI CC messages to represent an RPN
or NRPN message.
This sequence (as a MidiBuffer) can then be directly sent to a MidiOutput.
@tags{Audio}
*/
class JUCE_API MidiRPNGenerator
{
public:
//==============================================================================
/** Generates a MIDI sequence representing the given RPN or NRPN message. */
static MidiBuffer generate (MidiRPNMessage message);
//==============================================================================
/** Generates a MIDI sequence representing an RPN or NRPN message with the
given parameters.
@param channel The MIDI channel of the RPN/NRPN message.
@param parameterNumber The parameter number, in the range 0 to 16383.
@param value The parameter value, in the range 0 to 16383, or
in the range 0 to 127 if sendAs14BitValue is false.
@param isNRPN Whether you need a MIDI RPN or NRPN sequence (RPN is default).
@param use14BitValue If true (default), the value will have 14-bit precision
(two MIDI bytes). If false, instead the value will have
7-bit precision (a single MIDI byte).
*/
static MidiBuffer generate (int channel,
int parameterNumber,
int value,
bool isNRPN = false,
bool use14BitValue = true);
};
} // namespace juce

View File

@@ -1,43 +1,47 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#include "../juce_MidiDataConcatenator.h"
#include "juce_UMPProtocols.h"
#include "juce_UMPUtils.h"
#include "juce_UMPacket.h"
#include "juce_UMPSysEx7.h"
#include "juce_UMPView.h"
#include "juce_UMPIterator.h"
#include "juce_UMPackets.h"
#include "juce_UMPFactory.h"
#include "juce_UMPConversion.h"
#include "juce_UMPMidi1ToBytestreamTranslator.h"
#include "juce_UMPMidi1ToMidi2DefaultTranslator.h"
#include "juce_UMPConverters.h"
#include "juce_UMPDispatcher.h"
#include "juce_UMPReceiver.h"
namespace juce
{
namespace ump = universal_midi_packets;
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#include "../juce_MidiDataConcatenator.h"
#include "juce_UMPProtocols.h"
#include "juce_UMPUtils.h"
#include "juce_UMPacket.h"
#include "juce_UMPSysEx7.h"
#include "juce_UMPView.h"
#include "juce_UMPIterator.h"
#include "juce_UMPackets.h"
#include "juce_UMPFactory.h"
#include "juce_UMPConversion.h"
#include "juce_UMPMidi1ToBytestreamTranslator.h"
#include "juce_UMPMidi1ToMidi2DefaultTranslator.h"
#include "juce_UMPConverters.h"
#include "juce_UMPDispatcher.h"
#include "juce_UMPReceiver.h"
#ifndef DOXYGEN
namespace juce
{
namespace ump = universal_midi_packets;
}
#endif

View File

@@ -1,326 +1,330 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Functions to assist conversion of UMP messages to/from other formats,
especially older 'bytestream' formatted MidiMessages.
@tags{Audio}
*/
struct Conversion
{
/** Converts from a MIDI 1 bytestream to MIDI 1 on Universal MIDI Packets.
`callback` is a function which accepts a single View argument.
*/
template <typename PacketCallbackFunction>
static void toMidi1 (const MidiMessage& m, PacketCallbackFunction&& callback)
{
const auto* data = m.getRawData();
const auto firstByte = data[0];
const auto size = m.getRawDataSize();
if (firstByte != 0xf0)
{
const auto mask = [size]() -> uint32_t
{
switch (size)
{
case 0: return 0xff000000;
case 1: return 0xffff0000;
case 2: return 0xffffff00;
case 3: return 0xffffffff;
}
return 0x00000000;
}();
const auto extraByte = (uint8_t) ((((firstByte & 0xf0) == 0xf0) ? 0x1 : 0x2) << 0x4);
const PacketX1 packet { mask & Utils::bytesToWord (extraByte, data[0], data[1], data[2]) };
callback (View (packet.data()));
return;
}
const auto numSysExBytes = m.getSysExDataSize();
const auto numMessages = SysEx7::getNumPacketsRequiredForDataSize ((uint32_t) numSysExBytes);
auto* dataOffset = m.getSysExData();
if (numMessages <= 1)
{
const auto packet = Factory::makeSysExIn1Packet (0, (uint8_t) numSysExBytes, dataOffset);
callback (View (packet.data()));
return;
}
constexpr auto byteIncrement = 6;
for (auto i = numSysExBytes; i > 0; i -= byteIncrement, dataOffset += byteIncrement)
{
const auto func = [&]
{
if (i == numSysExBytes)
return Factory::makeSysExStart;
if (i <= byteIncrement)
return Factory::makeSysExEnd;
return Factory::makeSysExContinue;
}();
const auto bytesNow = std::min (byteIncrement, i);
const auto packet = func (0, (uint8_t) bytesNow, dataOffset);
callback (View (packet.data()));
}
}
/** Converts a MidiMessage to one or more messages in UMP format, using
the MIDI 1.0 Protocol.
`packets` is an out-param to allow the caller to control
allocation/deallocation. Returning a new Packets object would
require every call to toMidi1 to allocate. With this version, no
allocations will occur, provided that `packets` has adequate reserved
space.
*/
static void toMidi1 (const MidiMessage& m, Packets& packets)
{
toMidi1 (m, [&] (const View& view) { packets.add (view); });
}
/** Widens a 7-bit MIDI 1.0 value to a 8-bit MIDI 2.0 value. */
static uint8_t scaleTo8 (uint8_t word7Bit)
{
const auto shifted = (uint8_t) (word7Bit << 0x1);
const auto repeat = (uint8_t) (word7Bit & 0x3f);
const auto mask = (uint8_t) (word7Bit <= 0x40 ? 0x0 : 0xff);
return (uint8_t) (shifted | ((repeat >> 5) & mask));
}
/** Widens a 7-bit MIDI 1.0 value to a 16-bit MIDI 2.0 value. */
static uint16_t scaleTo16 (uint8_t word7Bit)
{
const auto shifted = (uint16_t) (word7Bit << 0x9);
const auto repeat = (uint16_t) (word7Bit & 0x3f);
const auto mask = (uint16_t) (word7Bit <= 0x40 ? 0x0 : 0xffff);
return (uint16_t) (shifted | (((repeat << 3) | (repeat >> 3)) & mask));
}
/** Widens a 14-bit MIDI 1.0 value to a 16-bit MIDI 2.0 value. */
static uint16_t scaleTo16 (uint16_t word14Bit)
{
const auto shifted = (uint16_t) (word14Bit << 0x2);
const auto repeat = (uint16_t) (word14Bit & 0x1fff);
const auto mask = (uint16_t) (word14Bit <= 0x2000 ? 0x0 : 0xffff);
return (uint16_t) (shifted | ((repeat >> 11) & mask));
}
/** Widens a 7-bit MIDI 1.0 value to a 32-bit MIDI 2.0 value. */
static uint32_t scaleTo32 (uint8_t word7Bit)
{
const auto shifted = (uint32_t) (word7Bit << 0x19);
const auto repeat = (uint32_t) (word7Bit & 0x3f);
const auto mask = (uint32_t) (word7Bit <= 0x40 ? 0x0 : 0xffffffff);
return (uint32_t) (shifted | (((repeat << 19)
| (repeat << 13)
| (repeat << 7)
| (repeat << 1)
| (repeat >> 5)) & mask));
}
/** Widens a 14-bit MIDI 1.0 value to a 32-bit MIDI 2.0 value. */
static uint32_t scaleTo32 (uint16_t word14Bit)
{
const auto shifted = (uint32_t) (word14Bit << 0x12);
const auto repeat = (uint32_t) (word14Bit & 0x1fff);
const auto mask = (uint32_t) (word14Bit <= 0x2000 ? 0x0 : 0xffffffff);
return (uint32_t) (shifted | (((repeat << 5) | (repeat >> 8)) & mask));
}
/** Narrows a 16-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint8_t word8Bit) { return (uint8_t) (word8Bit >> 1); }
/** Narrows a 16-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint16_t word16Bit) { return (uint8_t) (word16Bit >> 9); }
/** Narrows a 32-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint32_t word32Bit) { return (uint8_t) (word32Bit >> 25); }
/** Narrows a 32-bit MIDI 2.0 value to a 14-bit MIDI 1.0 value. */
static uint16_t scaleTo14 (uint16_t word16Bit) { return (uint16_t) (word16Bit >> 2); }
/** Narrows a 32-bit MIDI 2.0 value to a 14-bit MIDI 1.0 value. */
static uint16_t scaleTo14 (uint32_t word32Bit) { return (uint16_t) (word32Bit >> 18); }
/** Converts UMP messages which may include MIDI 2.0 channel voice messages into
equivalent MIDI 1.0 messages (still in UMP format).
`callback` is a function that accepts a single View argument and will be
called with each converted packet.
Note that not all MIDI 2.0 messages have MIDI 1.0 equivalents, so such
messages will be ignored.
*/
template <typename Callback>
static void midi2ToMidi1DefaultTranslation (const View& v, Callback&& callback)
{
const auto firstWord = v[0];
if (Utils::getMessageType (firstWord) != 0x4)
{
callback (v);
return;
}
const auto status = Utils::getStatus (firstWord);
const auto typeAndGroup = (uint8_t) ((0x2 << 0x4) | Utils::getGroup (firstWord));
switch (status)
{
case 0x8: // note off
case 0x9: // note on
case 0xa: // poly pressure
case 0xb: // control change
{
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const auto byte2 = (uint8_t) ((firstWord >> 0x08) & 0xff);
const auto byte3 = scaleTo7 (v[1]);
// If this is a note-on, and the scaled byte is 0,
// the scaled velocity should be 1 instead of 0
const auto needsCorrection = status == 0x9 && byte3 == 0;
const auto correctedByte = (uint8_t) (needsCorrection ? 1 : byte3);
const auto shouldIgnore = status == 0xb && [&]
{
switch (byte2)
{
case 0:
case 6:
case 32:
case 38:
case 98:
case 99:
case 100:
case 101:
return true;
}
return false;
}();
if (shouldIgnore)
return;
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
byte2,
correctedByte) };
callback (View (packet.data()));
return;
}
case 0xd: // channel pressure
{
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const auto byte2 = scaleTo7 (v[1]);
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
byte2,
0) };
callback (View (packet.data()));
return;
}
case 0x2: // rpn
case 0x3: // nrpn
{
const auto ccX = (uint8_t) (status == 0x2 ? 101 : 99);
const auto ccY = (uint8_t) (status == 0x2 ? 100 : 98);
const auto statusAndChannel = (uint8_t) ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto data = scaleTo14 (v[1]);
const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccX, (uint8_t) ((firstWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccY, (uint8_t) ((firstWord >> 0x0) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 6, (uint8_t) ((data >> 0x7) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 38, (uint8_t) ((data >> 0x0) & 0x7f)) },
};
for (const auto& packet : packets)
callback (View (packet.data()));
return;
}
case 0xc: // program change / bank select
{
if (firstWord & 1)
{
const auto statusAndChannel = (uint8_t) ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto secondWord = v[1];
const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 0, (uint8_t) ((secondWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 32, (uint8_t) ((secondWord >> 0x0) & 0x7f)) },
};
for (const auto& packet : packets)
callback (View (packet.data()));
}
const auto statusAndChannel = (uint8_t) ((0xc << 0x4) | Utils::getChannel (firstWord));
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
(uint8_t) ((v[1] >> 0x18) & 0x7f),
0) };
callback (View (packet.data()));
return;
}
case 0xe: // pitch bend
{
const auto data = scaleTo14 (v[1]);
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
(uint8_t) (data & 0x7f),
(uint8_t) ((data >> 7) & 0x7f)) };
callback (View (packet.data()));
return;
}
default: // other message types do not translate
return;
}
}
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Functions to assist conversion of UMP messages to/from other formats,
especially older 'bytestream' formatted MidiMessages.
@tags{Audio}
*/
struct Conversion
{
/** Converts from a MIDI 1 bytestream to MIDI 1 on Universal MIDI Packets.
`callback` is a function which accepts a single View argument.
*/
template <typename PacketCallbackFunction>
static void toMidi1 (const MidiMessage& m, PacketCallbackFunction&& callback)
{
const auto* data = m.getRawData();
const auto firstByte = data[0];
const auto size = m.getRawDataSize();
if (firstByte != 0xf0)
{
const auto mask = [size]() -> uint32_t
{
switch (size)
{
case 0: return 0xff000000;
case 1: return 0xffff0000;
case 2: return 0xffffff00;
case 3: return 0xffffffff;
}
return 0x00000000;
}();
const auto extraByte = (uint8_t) ((((firstByte & 0xf0) == 0xf0) ? 0x1 : 0x2) << 0x4);
const PacketX1 packet { mask & Utils::bytesToWord (extraByte, data[0], data[1], data[2]) };
callback (View (packet.data()));
return;
}
const auto numSysExBytes = m.getSysExDataSize();
const auto numMessages = SysEx7::getNumPacketsRequiredForDataSize ((uint32_t) numSysExBytes);
auto* dataOffset = m.getSysExData();
if (numMessages <= 1)
{
const auto packet = Factory::makeSysExIn1Packet (0, (uint8_t) numSysExBytes, dataOffset);
callback (View (packet.data()));
return;
}
constexpr auto byteIncrement = 6;
for (auto i = numSysExBytes; i > 0; i -= byteIncrement, dataOffset += byteIncrement)
{
const auto func = [&]
{
if (i == numSysExBytes)
return Factory::makeSysExStart;
if (i <= byteIncrement)
return Factory::makeSysExEnd;
return Factory::makeSysExContinue;
}();
const auto bytesNow = std::min (byteIncrement, i);
const auto packet = func (0, (uint8_t) bytesNow, dataOffset);
callback (View (packet.data()));
}
}
/** Converts a MidiMessage to one or more messages in UMP format, using
the MIDI 1.0 Protocol.
`packets` is an out-param to allow the caller to control
allocation/deallocation. Returning a new Packets object would
require every call to toMidi1 to allocate. With this version, no
allocations will occur, provided that `packets` has adequate reserved
space.
*/
static void toMidi1 (const MidiMessage& m, Packets& packets)
{
toMidi1 (m, [&] (const View& view) { packets.add (view); });
}
/** Widens a 7-bit MIDI 1.0 value to a 8-bit MIDI 2.0 value. */
static uint8_t scaleTo8 (uint8_t word7Bit)
{
const auto shifted = (uint8_t) (word7Bit << 0x1);
const auto repeat = (uint8_t) (word7Bit & 0x3f);
const auto mask = (uint8_t) (word7Bit <= 0x40 ? 0x0 : 0xff);
return (uint8_t) (shifted | ((repeat >> 5) & mask));
}
/** Widens a 7-bit MIDI 1.0 value to a 16-bit MIDI 2.0 value. */
static uint16_t scaleTo16 (uint8_t word7Bit)
{
const auto shifted = (uint16_t) (word7Bit << 0x9);
const auto repeat = (uint16_t) (word7Bit & 0x3f);
const auto mask = (uint16_t) (word7Bit <= 0x40 ? 0x0 : 0xffff);
return (uint16_t) (shifted | (((repeat << 3) | (repeat >> 3)) & mask));
}
/** Widens a 14-bit MIDI 1.0 value to a 16-bit MIDI 2.0 value. */
static uint16_t scaleTo16 (uint16_t word14Bit)
{
const auto shifted = (uint16_t) (word14Bit << 0x2);
const auto repeat = (uint16_t) (word14Bit & 0x1fff);
const auto mask = (uint16_t) (word14Bit <= 0x2000 ? 0x0 : 0xffff);
return (uint16_t) (shifted | ((repeat >> 11) & mask));
}
/** Widens a 7-bit MIDI 1.0 value to a 32-bit MIDI 2.0 value. */
static uint32_t scaleTo32 (uint8_t word7Bit)
{
const auto shifted = (uint32_t) (word7Bit << 0x19);
const auto repeat = (uint32_t) (word7Bit & 0x3f);
const auto mask = (uint32_t) (word7Bit <= 0x40 ? 0x0 : 0xffffffff);
return (uint32_t) (shifted | (((repeat << 19)
| (repeat << 13)
| (repeat << 7)
| (repeat << 1)
| (repeat >> 5)) & mask));
}
/** Widens a 14-bit MIDI 1.0 value to a 32-bit MIDI 2.0 value. */
static uint32_t scaleTo32 (uint16_t word14Bit)
{
const auto shifted = (uint32_t) (word14Bit << 0x12);
const auto repeat = (uint32_t) (word14Bit & 0x1fff);
const auto mask = (uint32_t) (word14Bit <= 0x2000 ? 0x0 : 0xffffffff);
return (uint32_t) (shifted | (((repeat << 5) | (repeat >> 8)) & mask));
}
/** Narrows a 16-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint8_t word8Bit) { return (uint8_t) (word8Bit >> 1); }
/** Narrows a 16-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint16_t word16Bit) { return (uint8_t) (word16Bit >> 9); }
/** Narrows a 32-bit MIDI 2.0 value to a 7-bit MIDI 1.0 value. */
static uint8_t scaleTo7 (uint32_t word32Bit) { return (uint8_t) (word32Bit >> 25); }
/** Narrows a 32-bit MIDI 2.0 value to a 14-bit MIDI 1.0 value. */
static uint16_t scaleTo14 (uint16_t word16Bit) { return (uint16_t) (word16Bit >> 2); }
/** Narrows a 32-bit MIDI 2.0 value to a 14-bit MIDI 1.0 value. */
static uint16_t scaleTo14 (uint32_t word32Bit) { return (uint16_t) (word32Bit >> 18); }
/** Converts UMP messages which may include MIDI 2.0 channel voice messages into
equivalent MIDI 1.0 messages (still in UMP format).
`callback` is a function that accepts a single View argument and will be
called with each converted packet.
Note that not all MIDI 2.0 messages have MIDI 1.0 equivalents, so such
messages will be ignored.
*/
template <typename Callback>
static void midi2ToMidi1DefaultTranslation (const View& v, Callback&& callback)
{
const auto firstWord = v[0];
if (Utils::getMessageType (firstWord) != 0x4)
{
callback (v);
return;
}
const auto status = Utils::getStatus (firstWord);
const auto typeAndGroup = (uint8_t) ((0x2 << 0x4) | Utils::getGroup (firstWord));
switch (status)
{
case 0x8: // note off
case 0x9: // note on
case 0xa: // poly pressure
case 0xb: // control change
{
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const auto byte2 = (uint8_t) ((firstWord >> 0x08) & 0xff);
const auto byte3 = scaleTo7 (v[1]);
// If this is a note-on, and the scaled byte is 0,
// the scaled velocity should be 1 instead of 0
const auto needsCorrection = status == 0x9 && byte3 == 0;
const auto correctedByte = (uint8_t) (needsCorrection ? 1 : byte3);
const auto shouldIgnore = status == 0xb && [&]
{
switch (byte2)
{
case 0:
case 6:
case 32:
case 38:
case 98:
case 99:
case 100:
case 101:
return true;
}
return false;
}();
if (shouldIgnore)
return;
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
byte2,
correctedByte) };
callback (View (packet.data()));
return;
}
case 0xd: // channel pressure
{
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const auto byte2 = scaleTo7 (v[1]);
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
byte2,
0) };
callback (View (packet.data()));
return;
}
case 0x2: // rpn
case 0x3: // nrpn
{
const auto ccX = (uint8_t) (status == 0x2 ? 101 : 99);
const auto ccY = (uint8_t) (status == 0x2 ? 100 : 98);
const auto statusAndChannel = (uint8_t) ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto data = scaleTo14 (v[1]);
const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccX, (uint8_t) ((firstWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, ccY, (uint8_t) ((firstWord >> 0x0) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 6, (uint8_t) ((data >> 0x7) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 38, (uint8_t) ((data >> 0x0) & 0x7f)) },
};
for (const auto& packet : packets)
callback (View (packet.data()));
return;
}
case 0xc: // program change / bank select
{
if (firstWord & 1)
{
const auto statusAndChannel = (uint8_t) ((0xb << 0x4) | Utils::getChannel (firstWord));
const auto secondWord = v[1];
const PacketX1 packets[]
{
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 0, (uint8_t) ((secondWord >> 0x8) & 0x7f)) },
PacketX1 { Utils::bytesToWord (typeAndGroup, statusAndChannel, 32, (uint8_t) ((secondWord >> 0x0) & 0x7f)) },
};
for (const auto& packet : packets)
callback (View (packet.data()));
}
const auto statusAndChannel = (uint8_t) ((0xc << 0x4) | Utils::getChannel (firstWord));
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
(uint8_t) ((v[1] >> 0x18) & 0x7f),
0) };
callback (View (packet.data()));
return;
}
case 0xe: // pitch bend
{
const auto data = scaleTo14 (v[1]);
const auto statusAndChannel = (uint8_t) ((firstWord >> 0x10) & 0xff);
const PacketX1 packet { Utils::bytesToWord (typeAndGroup,
statusAndChannel,
(uint8_t) (data & 0x7f),
(uint8_t) ((data >> 7) & 0x7f)) };
callback (View (packet.data()));
return;
}
default: // other message types do not translate
return;
}
}
};
}
}
#endif

View File

@@ -1,165 +1,169 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to MIDI 1.0 messages in UMP format.
@tags{Audio}
*/
struct ToUMP1Converter
{
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
Conversion::toMidi1 (m, std::forward<Fn> (fn));
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
Conversion::midi2ToMidi1DefaultTranslation (v, std::forward<Fn> (fn));
}
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to MIDI 2.0 messages in UMP format.
@tags{Audio}
*/
struct ToUMP2Converter
{
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
Conversion::toMidi1 (m, [&] (const View& v)
{
translator.dispatch (v, fn);
});
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
translator.dispatch (v, std::forward<Fn> (fn));
}
void reset()
{
translator.reset();
}
Midi1ToMidi2DefaultTranslator translator;
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to UMP format.
The packet protocol can be selected using the constructor parameter.
@tags{Audio}
*/
class GenericUMPConverter
{
public:
explicit GenericUMPConverter (PacketProtocol m)
: mode (m) {}
void reset()
{
std::get<1> (converters).reset();
}
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
switch (mode)
{
case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (m, std::forward<Fn> (fn));
case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (m, std::forward<Fn> (fn));
}
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
switch (mode)
{
case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (v, std::forward<Fn> (fn));
case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (v, std::forward<Fn> (fn));
}
}
template <typename Fn>
void convert (Iterator begin, Iterator end, Fn&& fn)
{
std::for_each (begin, end, [&] (const View& v)
{
convert (v, fn);
});
}
PacketProtocol getProtocol() const noexcept { return mode; }
private:
std::tuple<ToUMP1Converter, ToUMP2Converter> converters;
const PacketProtocol mode{};
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to bytestream format.
@tags{Audio}
*/
struct ToBytestreamConverter
{
explicit ToBytestreamConverter (int storageSize)
: translator (storageSize) {}
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
fn (m);
}
template <typename Fn>
void convert (const View& v, double time, Fn&& fn)
{
Conversion::midi2ToMidi1DefaultTranslation (v, [&] (const View& midi1)
{
translator.dispatch (midi1, time, fn);
});
}
void reset() { translator.reset(); }
Midi1ToBytestreamTranslator translator;
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to MIDI 1.0 messages in UMP format.
@tags{Audio}
*/
struct ToUMP1Converter
{
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
Conversion::toMidi1 (m, std::forward<Fn> (fn));
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
Conversion::midi2ToMidi1DefaultTranslation (v, std::forward<Fn> (fn));
}
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to MIDI 2.0 messages in UMP format.
@tags{Audio}
*/
struct ToUMP2Converter
{
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
Conversion::toMidi1 (m, [&] (const View& v)
{
translator.dispatch (v, fn);
});
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
translator.dispatch (v, std::forward<Fn> (fn));
}
void reset()
{
translator.reset();
}
Midi1ToMidi2DefaultTranslator translator;
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to UMP format.
The packet protocol can be selected using the constructor parameter.
@tags{Audio}
*/
class GenericUMPConverter
{
public:
explicit GenericUMPConverter (PacketProtocol m)
: mode (m) {}
void reset()
{
std::get<1> (converters).reset();
}
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
switch (mode)
{
case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (m, std::forward<Fn> (fn));
case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (m, std::forward<Fn> (fn));
}
}
template <typename Fn>
void convert (const View& v, Fn&& fn)
{
switch (mode)
{
case PacketProtocol::MIDI_1_0: return std::get<0> (converters).convert (v, std::forward<Fn> (fn));
case PacketProtocol::MIDI_2_0: return std::get<1> (converters).convert (v, std::forward<Fn> (fn));
}
}
template <typename Fn>
void convert (Iterator begin, Iterator end, Fn&& fn)
{
std::for_each (begin, end, [&] (const View& v)
{
convert (v, fn);
});
}
PacketProtocol getProtocol() const noexcept { return mode; }
private:
std::tuple<ToUMP1Converter, ToUMP2Converter> converters;
const PacketProtocol mode{};
};
/**
Allows conversion from bytestream- or Universal MIDI Packet-formatted
messages to bytestream format.
@tags{Audio}
*/
struct ToBytestreamConverter
{
explicit ToBytestreamConverter (int storageSize)
: translator (storageSize) {}
template <typename Fn>
void convert (const MidiMessage& m, Fn&& fn)
{
fn (m);
}
template <typename Fn>
void convert (const View& v, double time, Fn&& fn)
{
Conversion::midi2ToMidi1DefaultTranslation (v, [&] (const View& midi1)
{
translator.dispatch (midi1, time, fn);
});
}
void reset() { translator.reset(); }
Midi1ToBytestreamTranslator translator;
};
}
}
#endif

View File

@@ -1,198 +1,202 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Parses a raw stream of uint32_t, and calls a user-provided callback every time
a full Universal MIDI Packet is encountered.
@tags{Audio}
*/
class Dispatcher
{
public:
/** Clears the dispatcher. */
void reset() { currentPacketLen = 0; }
/** Calls `callback` with a View of each packet encountered in the range delimited
by `begin` and `end`.
If the range ends part-way through a packet, the next call to `dispatch` will
continue from that point in the packet (unless `reset` is called first).
*/
template <typename PacketCallbackFunction>
void dispatch (const uint32_t* begin,
const uint32_t* end,
double timeStamp,
PacketCallbackFunction&& callback)
{
std::for_each (begin, end, [&] (uint32_t word)
{
nextPacket[currentPacketLen++] = word;
if (currentPacketLen == Utils::getNumWordsForMessageType (nextPacket.front()))
{
callback (View (nextPacket.data()), timeStamp);
currentPacketLen = 0;
}
});
}
private:
std::array<uint32_t, 4> nextPacket;
size_t currentPacketLen = 0;
};
//==============================================================================
/**
Parses a stream of bytes representing a sequence of bytestream-encoded MIDI 1.0 messages,
converting the messages to UMP format and passing the packets to a user-provided callback
as they become ready.
@tags{Audio}
*/
class BytestreamToUMPDispatcher
{
public:
/** Initialises the dispatcher.
Channel messages will be converted to the requested protocol format `pp`.
`storageSize` bytes will be allocated to store incomplete messages.
*/
explicit BytestreamToUMPDispatcher (PacketProtocol pp, int storageSize)
: concatenator (storageSize),
converter (pp)
{}
void reset()
{
concatenator.reset();
converter.reset();
}
/** Calls `callback` with a View of each converted packet as it becomes ready.
@param begin the first byte in a range of bytes representing bytestream-encoded MIDI messages.
@param end one-past the last byte in a range of bytes representing bytestream-encoded MIDI messages.
@param timestamp a timestamp to apply to the created packets.
@param callback a callback which will be passed a View pointing to each new packet as it becomes ready.
*/
template <typename PacketCallbackFunction>
void dispatch (const uint8_t* begin,
const uint8_t* end,
double timestamp,
PacketCallbackFunction&& callback)
{
using CallbackPtr = decltype (std::addressof (callback));
#if JUCE_MINGW
#define JUCE_MINGW_HIDDEN_VISIBILITY __attribute__ ((visibility ("hidden")))
#else
#define JUCE_MINGW_HIDDEN_VISIBILITY
#endif
struct JUCE_MINGW_HIDDEN_VISIBILITY Callback
{
Callback (BytestreamToUMPDispatcher& d, CallbackPtr c)
: dispatch (d), callbackPtr (c) {}
void handleIncomingMidiMessage (void*, const MidiMessage& msg) const
{
Conversion::toMidi1 (msg, [&] (const View& view)
{
dispatch.converter.convert (view, *callbackPtr);
});
}
void handlePartialSysexMessage (void*, const uint8_t*, int, double) const {}
BytestreamToUMPDispatcher& dispatch;
CallbackPtr callbackPtr = nullptr;
};
#undef JUCE_MINGW_HIDDEN_VISIBILITY
Callback inputCallback { *this, &callback };
concatenator.pushMidiData (begin, int (end - begin), timestamp, (void*) nullptr, inputCallback);
}
private:
MidiDataConcatenator concatenator;
GenericUMPConverter converter;
};
//==============================================================================
/**
Parses a stream of 32-bit words representing a sequence of UMP-encoded MIDI messages,
converting the messages to MIDI 1.0 bytestream format and passing them to a user-provided
callback as they become ready.
@tags{Audio}
*/
class ToBytestreamDispatcher
{
public:
/** Initialises the dispatcher.
`storageSize` bytes will be allocated to store incomplete messages.
*/
explicit ToBytestreamDispatcher (int storageSize)
: converter (storageSize) {}
/** Clears the dispatcher. */
void reset()
{
dispatcher.reset();
converter.reset();
}
/** Calls `callback` with converted bytestream-formatted MidiMessage whenever
a new message becomes available.
@param begin the first word in a stream of words representing UMP-encoded MIDI packets.
@param end one-past the last word in a stream of words representing UMP-encoded MIDI packets.
@param timestamp a timestamp to apply to converted messages.
@param callback a callback which will be passed a MidiMessage each time a new message becomes ready.
*/
template <typename BytestreamMessageCallback>
void dispatch (const uint32_t* begin,
const uint32_t* end,
double timestamp,
BytestreamMessageCallback&& callback)
{
dispatcher.dispatch (begin, end, timestamp, [&] (const View& view, double time)
{
converter.convert (view, time, callback);
});
}
private:
Dispatcher dispatcher;
ToBytestreamConverter converter;
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Parses a raw stream of uint32_t, and calls a user-provided callback every time
a full Universal MIDI Packet is encountered.
@tags{Audio}
*/
class Dispatcher
{
public:
/** Clears the dispatcher. */
void reset() { currentPacketLen = 0; }
/** Calls `callback` with a View of each packet encountered in the range delimited
by `begin` and `end`.
If the range ends part-way through a packet, the next call to `dispatch` will
continue from that point in the packet (unless `reset` is called first).
*/
template <typename PacketCallbackFunction>
void dispatch (const uint32_t* begin,
const uint32_t* end,
double timeStamp,
PacketCallbackFunction&& callback)
{
std::for_each (begin, end, [&] (uint32_t word)
{
nextPacket[currentPacketLen++] = word;
if (currentPacketLen == Utils::getNumWordsForMessageType (nextPacket.front()))
{
callback (View (nextPacket.data()), timeStamp);
currentPacketLen = 0;
}
});
}
private:
std::array<uint32_t, 4> nextPacket;
size_t currentPacketLen = 0;
};
//==============================================================================
/**
Parses a stream of bytes representing a sequence of bytestream-encoded MIDI 1.0 messages,
converting the messages to UMP format and passing the packets to a user-provided callback
as they become ready.
@tags{Audio}
*/
class BytestreamToUMPDispatcher
{
public:
/** Initialises the dispatcher.
Channel messages will be converted to the requested protocol format `pp`.
`storageSize` bytes will be allocated to store incomplete messages.
*/
explicit BytestreamToUMPDispatcher (PacketProtocol pp, int storageSize)
: concatenator (storageSize),
converter (pp)
{}
void reset()
{
concatenator.reset();
converter.reset();
}
/** Calls `callback` with a View of each converted packet as it becomes ready.
@param begin the first byte in a range of bytes representing bytestream-encoded MIDI messages.
@param end one-past the last byte in a range of bytes representing bytestream-encoded MIDI messages.
@param timestamp a timestamp to apply to the created packets.
@param callback a callback which will be passed a View pointing to each new packet as it becomes ready.
*/
template <typename PacketCallbackFunction>
void dispatch (const uint8_t* begin,
const uint8_t* end,
double timestamp,
PacketCallbackFunction&& callback)
{
using CallbackPtr = decltype (std::addressof (callback));
#if JUCE_MINGW
#define JUCE_MINGW_HIDDEN_VISIBILITY __attribute__ ((visibility ("hidden")))
#else
#define JUCE_MINGW_HIDDEN_VISIBILITY
#endif
struct JUCE_MINGW_HIDDEN_VISIBILITY Callback
{
Callback (BytestreamToUMPDispatcher& d, CallbackPtr c)
: dispatch (d), callbackPtr (c) {}
void handleIncomingMidiMessage (void*, const MidiMessage& msg) const
{
Conversion::toMidi1 (msg, [&] (const View& view)
{
dispatch.converter.convert (view, *callbackPtr);
});
}
void handlePartialSysexMessage (void*, const uint8_t*, int, double) const {}
BytestreamToUMPDispatcher& dispatch;
CallbackPtr callbackPtr = nullptr;
};
#undef JUCE_MINGW_HIDDEN_VISIBILITY
Callback inputCallback { *this, &callback };
concatenator.pushMidiData (begin, int (end - begin), timestamp, (void*) nullptr, inputCallback);
}
private:
MidiDataConcatenator concatenator;
GenericUMPConverter converter;
};
//==============================================================================
/**
Parses a stream of 32-bit words representing a sequence of UMP-encoded MIDI messages,
converting the messages to MIDI 1.0 bytestream format and passing them to a user-provided
callback as they become ready.
@tags{Audio}
*/
class ToBytestreamDispatcher
{
public:
/** Initialises the dispatcher.
`storageSize` bytes will be allocated to store incomplete messages.
*/
explicit ToBytestreamDispatcher (int storageSize)
: converter (storageSize) {}
/** Clears the dispatcher. */
void reset()
{
dispatcher.reset();
converter.reset();
}
/** Calls `callback` with converted bytestream-formatted MidiMessage whenever
a new message becomes available.
@param begin the first word in a stream of words representing UMP-encoded MIDI packets.
@param end one-past the last word in a stream of words representing UMP-encoded MIDI packets.
@param timestamp a timestamp to apply to converted messages.
@param callback a callback which will be passed a MidiMessage each time a new message becomes ready.
*/
template <typename BytestreamMessageCallback>
void dispatch (const uint32_t* begin,
const uint32_t* end,
double timestamp,
BytestreamMessageCallback&& callback)
{
dispatcher.dispatch (begin, end, timestamp, [&] (const View& view, double time)
{
converter.convert (view, time, callback);
});
}
private:
Dispatcher dispatcher;
ToBytestreamConverter converter;
};
}
}
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -1,126 +1,130 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Enables iteration over a collection of Universal MIDI Packets stored as
a contiguous range of 32-bit words.
This iterator is used by Packets to allow access to the messages
that it contains.
@tags{Audio}
*/
class Iterator
{
public:
/** Creates an invalid (singular) iterator. */
Iterator() noexcept = default;
/** Creates an iterator pointing at `ptr`. */
explicit Iterator (const uint32_t* ptr, size_t bytes) noexcept
: view (ptr)
#if JUCE_DEBUG
, bytesRemaining (bytes)
#endif
{
ignoreUnused (bytes);
}
using difference_type = std::iterator_traits<const uint32_t*>::difference_type;
using value_type = View;
using reference = const View&;
using pointer = const View*;
using iterator_category = std::forward_iterator_tag;
/** Moves this iterator to the next packet in the range. */
Iterator& operator++() noexcept
{
const auto increment = view.size();
#if JUCE_DEBUG
// If you hit this, the memory region contained a truncated or otherwise
// malformed Universal MIDI Packet.
// The Iterator can only be used on regions containing complete packets!
jassert (increment <= bytesRemaining);
bytesRemaining -= increment;
#endif
view = View (view.data() + increment);
return *this;
}
/** Moves this iterator to the next packet in the range,
returning the value of the iterator before it was
incremented.
*/
Iterator operator++ (int) noexcept
{
auto copy = *this;
++(*this);
return copy;
}
/** Returns true if this iterator points to the same address
as another iterator.
*/
bool operator== (const Iterator& other) const noexcept
{
return view == other.view;
}
/** Returns false if this iterator points to the same address
as another iterator.
*/
bool operator!= (const Iterator& other) const noexcept
{
return ! operator== (other);
}
/** Returns a reference to a View of the packet currently
pointed-to by this iterator.
The View can be queried for its size and content.
*/
reference operator*() noexcept { return view; }
/** Returns a pointer to a View of the packet currently
pointed-to by this iterator.
The View can be queried for its size and content.
*/
pointer operator->() noexcept { return &view; }
private:
View view;
#if JUCE_DEBUG
size_t bytesRemaining = 0;
#endif
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Enables iteration over a collection of Universal MIDI Packets stored as
a contiguous range of 32-bit words.
This iterator is used by Packets to allow access to the messages
that it contains.
@tags{Audio}
*/
class Iterator
{
public:
/** Creates an invalid (singular) iterator. */
Iterator() noexcept = default;
/** Creates an iterator pointing at `ptr`. */
explicit Iterator (const uint32_t* ptr, size_t bytes) noexcept
: view (ptr)
#if JUCE_DEBUG
, bytesRemaining (bytes)
#endif
{
ignoreUnused (bytes);
}
using difference_type = std::iterator_traits<const uint32_t*>::difference_type;
using value_type = View;
using reference = const View&;
using pointer = const View*;
using iterator_category = std::forward_iterator_tag;
/** Moves this iterator to the next packet in the range. */
Iterator& operator++() noexcept
{
const auto increment = view.size();
#if JUCE_DEBUG
// If you hit this, the memory region contained a truncated or otherwise
// malformed Universal MIDI Packet.
// The Iterator can only be used on regions containing complete packets!
jassert (increment <= bytesRemaining);
bytesRemaining -= increment;
#endif
view = View (view.data() + increment);
return *this;
}
/** Moves this iterator to the next packet in the range,
returning the value of the iterator before it was
incremented.
*/
Iterator operator++ (int) noexcept
{
auto copy = *this;
++(*this);
return copy;
}
/** Returns true if this iterator points to the same address
as another iterator.
*/
bool operator== (const Iterator& other) const noexcept
{
return view == other.view;
}
/** Returns false if this iterator points to the same address
as another iterator.
*/
bool operator!= (const Iterator& other) const noexcept
{
return ! operator== (other);
}
/** Returns a reference to a View of the packet currently
pointed-to by this iterator.
The View can be queried for its size and content.
*/
reference operator*() noexcept { return view; }
/** Returns a pointer to a View of the packet currently
pointed-to by this iterator.
The View can be queried for its size and content.
*/
pointer operator->() noexcept { return &view; }
private:
View view;
#if JUCE_DEBUG
size_t bytesRemaining = 0;
#endif
};
}
}
#endif

View File

@@ -1,213 +1,217 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Parses a raw stream of uint32_t holding a series of Universal MIDI Packets using
the MIDI 1.0 Protocol, converting to plain (non-UMP) MidiMessages.
@tags{Audio}
*/
class Midi1ToBytestreamTranslator
{
public:
/** Ensures that there is room in the internal buffer for a sysex message of at least
`initialBufferSize` bytes.
*/
explicit Midi1ToBytestreamTranslator (int initialBufferSize)
{
pendingSysExData.reserve (size_t (initialBufferSize));
}
/** Clears the concatenator. */
void reset()
{
pendingSysExData.clear();
pendingSysExTime = 0.0;
}
/** Converts a Universal MIDI Packet using the MIDI 1.0 Protocol to
an equivalent MidiMessage. Accumulates SysEx packets into a single
MidiMessage, as appropriate.
@param packet a packet which is using the MIDI 1.0 Protocol.
@param time the timestamp to be applied to these messages.
@param callback a callback which will be called with each converted MidiMessage.
*/
template <typename MessageCallback>
void dispatch (const View& packet, double time, MessageCallback&& callback)
{
const auto firstWord = *packet.data();
if (! pendingSysExData.empty() && shouldPacketTerminateSysExEarly (firstWord))
pendingSysExData.clear();
switch (packet.size())
{
case 1:
{
// Utility messages don't translate to bytestream format
if (Utils::getMessageType (firstWord) != 0x00)
callback (fromUmp (PacketX1 { firstWord }, time));
break;
}
case 2:
{
if (Utils::getMessageType (firstWord) == 0x3)
processSysEx (PacketX2 { packet[0], packet[1] }, time, callback);
break;
}
case 3: // no 3-word packets in the current spec
case 4: // no 4-word packets translate to bytestream format
default:
break;
}
}
/** Converts from a Universal MIDI Packet to MIDI 1 bytestream format.
This is only capable of converting a single Universal MIDI Packet to
an equivalent bytestream MIDI message. This function cannot understand
multi-packet messages, like SysEx7 messages.
To convert multi-packet messages, use `Midi1ToBytestreamTranslator`
to convert from a UMP MIDI 1.0 stream, or `ToBytestreamDispatcher`
to convert from both MIDI 2.0 and MIDI 1.0.
*/
static MidiMessage fromUmp (const PacketX1& m, double time = 0)
{
const auto word = m.front();
jassert (Utils::getNumWordsForMessageType (word) == 1);
const std::array<uint8_t, 3> bytes { { uint8_t ((word >> 0x10) & 0xff),
uint8_t ((word >> 0x08) & 0xff),
uint8_t ((word >> 0x00) & 0xff) } };
const auto numBytes = MidiMessage::getMessageLengthFromFirstByte (bytes.front());
return MidiMessage (bytes.data(), numBytes, time);
}
private:
template <typename MessageCallback>
void processSysEx (const PacketX2& packet,
double time,
MessageCallback&& callback)
{
switch (getSysEx7Kind (packet[0]))
{
case SysEx7::Kind::complete:
startSysExMessage (time);
pushBytes (packet);
terminateSysExMessage (callback);
break;
case SysEx7::Kind::begin:
startSysExMessage (time);
pushBytes (packet);
break;
case SysEx7::Kind::continuation:
if (pendingSysExData.empty())
break;
pushBytes (packet);
break;
case SysEx7::Kind::end:
if (pendingSysExData.empty())
break;
pushBytes (packet);
terminateSysExMessage (callback);
break;
}
}
void pushBytes (const PacketX2& packet)
{
const auto bytes = SysEx7::getDataBytes (packet);
pendingSysExData.insert (pendingSysExData.end(),
bytes.data.begin(),
bytes.data.begin() + bytes.size);
}
void startSysExMessage (double time)
{
pendingSysExTime = time;
pendingSysExData.push_back (0xf0);
}
template <typename MessageCallback>
void terminateSysExMessage (MessageCallback&& callback)
{
pendingSysExData.push_back (0xf7);
callback (MidiMessage (pendingSysExData.data(),
int (pendingSysExData.size()),
pendingSysExTime));
pendingSysExData.clear();
}
static bool shouldPacketTerminateSysExEarly (uint32_t firstWord)
{
return ! (isSysExContinuation (firstWord)
|| isSystemRealTime (firstWord)
|| isJROrNOP (firstWord));
}
static SysEx7::Kind getSysEx7Kind (uint32_t word)
{
return SysEx7::Kind ((word >> 0x14) & 0xf);
}
static bool isJROrNOP (uint32_t word)
{
return Utils::getMessageType (word) == 0x0;
}
static bool isSysExContinuation (uint32_t word)
{
if (Utils::getMessageType (word) != 0x3)
return false;
const auto kind = getSysEx7Kind (word);
return kind == SysEx7::Kind::continuation || kind == SysEx7::Kind::end;
}
static bool isSystemRealTime (uint32_t word)
{
return Utils::getMessageType (word) == 0x1 && ((word >> 0x10) & 0xff) >= 0xf8;
}
std::vector<uint8_t> pendingSysExData;
double pendingSysExTime = 0.0;
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Parses a raw stream of uint32_t holding a series of Universal MIDI Packets using
the MIDI 1.0 Protocol, converting to plain (non-UMP) MidiMessages.
@tags{Audio}
*/
class Midi1ToBytestreamTranslator
{
public:
/** Ensures that there is room in the internal buffer for a sysex message of at least
`initialBufferSize` bytes.
*/
explicit Midi1ToBytestreamTranslator (int initialBufferSize)
{
pendingSysExData.reserve (size_t (initialBufferSize));
}
/** Clears the concatenator. */
void reset()
{
pendingSysExData.clear();
pendingSysExTime = 0.0;
}
/** Converts a Universal MIDI Packet using the MIDI 1.0 Protocol to
an equivalent MidiMessage. Accumulates SysEx packets into a single
MidiMessage, as appropriate.
@param packet a packet which is using the MIDI 1.0 Protocol.
@param time the timestamp to be applied to these messages.
@param callback a callback which will be called with each converted MidiMessage.
*/
template <typename MessageCallback>
void dispatch (const View& packet, double time, MessageCallback&& callback)
{
const auto firstWord = *packet.data();
if (! pendingSysExData.empty() && shouldPacketTerminateSysExEarly (firstWord))
pendingSysExData.clear();
switch (packet.size())
{
case 1:
{
// Utility messages don't translate to bytestream format
if (Utils::getMessageType (firstWord) != 0x00)
callback (fromUmp (PacketX1 { firstWord }, time));
break;
}
case 2:
{
if (Utils::getMessageType (firstWord) == 0x3)
processSysEx (PacketX2 { packet[0], packet[1] }, time, callback);
break;
}
case 3: // no 3-word packets in the current spec
case 4: // no 4-word packets translate to bytestream format
default:
break;
}
}
/** Converts from a Universal MIDI Packet to MIDI 1 bytestream format.
This is only capable of converting a single Universal MIDI Packet to
an equivalent bytestream MIDI message. This function cannot understand
multi-packet messages, like SysEx7 messages.
To convert multi-packet messages, use `Midi1ToBytestreamTranslator`
to convert from a UMP MIDI 1.0 stream, or `ToBytestreamDispatcher`
to convert from both MIDI 2.0 and MIDI 1.0.
*/
static MidiMessage fromUmp (const PacketX1& m, double time = 0)
{
const auto word = m.front();
jassert (Utils::getNumWordsForMessageType (word) == 1);
const std::array<uint8_t, 3> bytes { { uint8_t ((word >> 0x10) & 0xff),
uint8_t ((word >> 0x08) & 0xff),
uint8_t ((word >> 0x00) & 0xff) } };
const auto numBytes = MidiMessage::getMessageLengthFromFirstByte (bytes.front());
return MidiMessage (bytes.data(), numBytes, time);
}
private:
template <typename MessageCallback>
void processSysEx (const PacketX2& packet,
double time,
MessageCallback&& callback)
{
switch (getSysEx7Kind (packet[0]))
{
case SysEx7::Kind::complete:
startSysExMessage (time);
pushBytes (packet);
terminateSysExMessage (callback);
break;
case SysEx7::Kind::begin:
startSysExMessage (time);
pushBytes (packet);
break;
case SysEx7::Kind::continuation:
if (pendingSysExData.empty())
break;
pushBytes (packet);
break;
case SysEx7::Kind::end:
if (pendingSysExData.empty())
break;
pushBytes (packet);
terminateSysExMessage (callback);
break;
}
}
void pushBytes (const PacketX2& packet)
{
const auto bytes = SysEx7::getDataBytes (packet);
pendingSysExData.insert (pendingSysExData.end(),
bytes.data.begin(),
bytes.data.begin() + bytes.size);
}
void startSysExMessage (double time)
{
pendingSysExTime = time;
pendingSysExData.push_back (0xf0);
}
template <typename MessageCallback>
void terminateSysExMessage (MessageCallback&& callback)
{
pendingSysExData.push_back (0xf7);
callback (MidiMessage (pendingSysExData.data(),
int (pendingSysExData.size()),
pendingSysExTime));
pendingSysExData.clear();
}
static bool shouldPacketTerminateSysExEarly (uint32_t firstWord)
{
return ! (isSysExContinuation (firstWord)
|| isSystemRealTime (firstWord)
|| isJROrNOP (firstWord));
}
static SysEx7::Kind getSysEx7Kind (uint32_t word)
{
return SysEx7::Kind ((word >> 0x14) & 0xf);
}
static bool isJROrNOP (uint32_t word)
{
return Utils::getMessageType (word) == 0x0;
}
static bool isSysExContinuation (uint32_t word)
{
if (Utils::getMessageType (word) != 0x3)
return false;
const auto kind = getSysEx7Kind (word);
return kind == SysEx7::Kind::continuation || kind == SysEx7::Kind::end;
}
static bool isSystemRealTime (uint32_t word)
{
return Utils::getMessageType (word) == 0x1 && ((word >> 0x10) & 0xff) >= 0xf8;
}
std::vector<uint8_t> pendingSysExData;
double pendingSysExTime = 0.0;
};
}
}
#endif

View File

@@ -1,195 +1,195 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
PacketX2 Midi1ToMidi2DefaultTranslator::processNoteOnOrOff (const HelperValues helpers)
{
const auto velocity = helpers.byte2;
const auto needsConversion = (helpers.byte0 >> 0x4) == 0x9 && velocity == 0;
const auto firstByte = needsConversion ? (uint8_t) ((0x8 << 0x4) | (helpers.byte0 & 0xf))
: helpers.byte0;
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, firstByte, helpers.byte1, 0),
(uint32_t) (Conversion::scaleTo16 (velocity) << 0x10)
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processPolyPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, helpers.byte1, 0),
Conversion::scaleTo32 (helpers.byte2)
};
}
bool Midi1ToMidi2DefaultTranslator::processControlChange (const HelperValues helpers,
PacketX2& packet)
{
const auto statusAndChannel = helpers.byte0;
const auto cc = helpers.byte1;
const auto shouldAccumulate = [&]
{
switch (cc)
{
case 6:
case 38:
case 98:
case 99:
case 100:
case 101:
return true;
}
return false;
}();
const auto group = (uint8_t) (helpers.typeAndGroup & 0xf);
const auto channel = (uint8_t) (statusAndChannel & 0xf);
const auto byte = helpers.byte2;
if (shouldAccumulate)
{
auto& accumulator = groupAccumulators[group][channel];
if (accumulator.addByte (cc, byte))
{
const auto& bytes = accumulator.getBytes();
const auto bank = bytes[0];
const auto index = bytes[1];
const auto msb = bytes[2];
const auto lsb = bytes[3];
const auto value = (uint16_t) (((msb & 0x7f) << 7) | (lsb & 0x7f));
const auto newStatus = (uint8_t) (accumulator.getKind() == PnKind::nrpn ? 0x3 : 0x2);
packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, (uint8_t) ((newStatus << 0x4) | channel), bank, index),
Conversion::scaleTo32 (value)
};
return true;
}
return false;
}
if (cc == 0)
{
groupBanks[group][channel].setMsb (byte);
return false;
}
if (cc == 32)
{
groupBanks[group][channel].setLsb (byte);
return false;
}
packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, statusAndChannel, cc, 0),
Conversion::scaleTo32 (helpers.byte2)
};
return true;
}
PacketX2 Midi1ToMidi2DefaultTranslator::processProgramChange (const HelperValues helpers) const
{
const auto group = (uint8_t) (helpers.typeAndGroup & 0xf);
const auto channel = (uint8_t) (helpers.byte0 & 0xf);
const auto bank = groupBanks[group][channel];
const auto valid = bank.isValid();
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, valid ? 1 : 0),
Utils::bytesToWord (helpers.byte1, 0, valid ? bank.getMsb() : 0, valid ? bank.getLsb() : 0)
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processChannelPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, 0),
Conversion::scaleTo32 (helpers.byte1)
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processPitchBend (const HelperValues helpers)
{
const auto lsb = helpers.byte1;
const auto msb = helpers.byte2;
const auto value = (uint16_t) (msb << 7 | lsb);
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, 0),
Conversion::scaleTo32 (value)
};
}
bool Midi1ToMidi2DefaultTranslator::PnAccumulator::addByte (uint8_t cc, uint8_t byte)
{
const auto isStart = cc == 99 || cc == 101;
if (isStart)
{
kind = cc == 99 ? PnKind::nrpn : PnKind::rpn;
index = 0;
}
bytes[index] = byte;
const auto shouldContinue = [&]
{
switch (index)
{
case 0: return isStart;
case 1: return kind == PnKind::nrpn ? cc == 98 : cc == 100;
case 2: return cc == 6;
case 3: return cc == 38;
}
return false;
}();
index = shouldContinue ? index + 1 : 0;
if (index != bytes.size())
return false;
index = 0;
return true;
}
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
PacketX2 Midi1ToMidi2DefaultTranslator::processNoteOnOrOff (const HelperValues helpers)
{
const auto velocity = helpers.byte2;
const auto needsConversion = (helpers.byte0 >> 0x4) == 0x9 && velocity == 0;
const auto firstByte = needsConversion ? (uint8_t) ((0x8 << 0x4) | (helpers.byte0 & 0xf))
: helpers.byte0;
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, firstByte, helpers.byte1, 0),
(uint32_t) (Conversion::scaleTo16 (velocity) << 0x10)
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processPolyPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, helpers.byte1, 0),
Conversion::scaleTo32 (helpers.byte2)
};
}
bool Midi1ToMidi2DefaultTranslator::processControlChange (const HelperValues helpers,
PacketX2& packet)
{
const auto statusAndChannel = helpers.byte0;
const auto cc = helpers.byte1;
const auto shouldAccumulate = [&]
{
switch (cc)
{
case 6:
case 38:
case 98:
case 99:
case 100:
case 101:
return true;
}
return false;
}();
const auto group = (uint8_t) (helpers.typeAndGroup & 0xf);
const auto channel = (uint8_t) (statusAndChannel & 0xf);
const auto byte = helpers.byte2;
if (shouldAccumulate)
{
auto& accumulator = groupAccumulators[group][channel];
if (accumulator.addByte (cc, byte))
{
const auto& bytes = accumulator.getBytes();
const auto bank = bytes[0];
const auto index = bytes[1];
const auto msb = bytes[2];
const auto lsb = bytes[3];
const auto value = (uint16_t) (((msb & 0x7f) << 7) | (lsb & 0x7f));
const auto newStatus = (uint8_t) (accumulator.getKind() == PnKind::nrpn ? 0x3 : 0x2);
packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, (uint8_t) ((newStatus << 0x4) | channel), bank, index),
Conversion::scaleTo32 (value)
};
return true;
}
return false;
}
if (cc == 0)
{
groupBanks[group][channel].setMsb (byte);
return false;
}
if (cc == 32)
{
groupBanks[group][channel].setLsb (byte);
return false;
}
packet = PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, statusAndChannel, cc, 0),
Conversion::scaleTo32 (helpers.byte2)
};
return true;
}
PacketX2 Midi1ToMidi2DefaultTranslator::processProgramChange (const HelperValues helpers) const
{
const auto group = (uint8_t) (helpers.typeAndGroup & 0xf);
const auto channel = (uint8_t) (helpers.byte0 & 0xf);
const auto bank = groupBanks[group][channel];
const auto valid = bank.isValid();
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, valid ? 1 : 0),
Utils::bytesToWord (helpers.byte1, 0, valid ? bank.getMsb() : 0, valid ? bank.getLsb() : 0)
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processChannelPressure (const HelperValues helpers)
{
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, 0),
Conversion::scaleTo32 (helpers.byte1)
};
}
PacketX2 Midi1ToMidi2DefaultTranslator::processPitchBend (const HelperValues helpers)
{
const auto lsb = helpers.byte1;
const auto msb = helpers.byte2;
const auto value = (uint16_t) (msb << 7 | lsb);
return PacketX2
{
Utils::bytesToWord (helpers.typeAndGroup, helpers.byte0, 0, 0),
Conversion::scaleTo32 (value)
};
}
bool Midi1ToMidi2DefaultTranslator::PnAccumulator::addByte (uint8_t cc, uint8_t byte)
{
const auto isStart = cc == 99 || cc == 101;
if (isStart)
{
kind = cc == 99 ? PnKind::nrpn : PnKind::rpn;
index = 0;
}
bytes[index] = byte;
const auto shouldContinue = [&]
{
switch (index)
{
case 0: return isStart;
case 1: return kind == PnKind::nrpn ? cc == 98 : cc == 100;
case 2: return cc == 6;
case 3: return cc == 38;
}
return false;
}();
index = shouldContinue ? index + 1 : 0;
if (index != bytes.size())
return false;
index = 0;
return true;
}
}
}

View File

@@ -1,187 +1,191 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Translates a series of MIDI 1 Universal MIDI Packets to corresponding MIDI 2
packets.
@tags{Audio}
*/
class Midi1ToMidi2DefaultTranslator
{
public:
Midi1ToMidi2DefaultTranslator() = default;
/** Converts MIDI 1 Universal MIDI Packets to corresponding MIDI 2 packets,
calling `callback` with each converted packet.
In some cases (such as RPN/NRPN messages) multiple MIDI 1 packets will
convert to a single MIDI 2 packet. In these cases, the translator will
accumulate the full message internally, and send a single callback with
the completed message, once all the individual MIDI 1 packets have been
processed.
*/
template <typename PacketCallback>
void dispatch (const View& v, PacketCallback&& callback)
{
const auto firstWord = v[0];
const auto messageType = Utils::getMessageType (firstWord);
if (messageType != 0x2)
{
callback (v);
return;
}
const HelperValues helperValues
{
(uint8_t) ((0x4 << 0x4) | Utils::getGroup (firstWord)),
(uint8_t) ((firstWord >> 0x10) & 0xff),
(uint8_t) ((firstWord >> 0x08) & 0x7f),
(uint8_t) ((firstWord >> 0x00) & 0x7f),
};
switch (Utils::getStatus (firstWord))
{
case 0x8:
case 0x9:
{
const auto packet = processNoteOnOrOff (helperValues);
callback (View (packet.data()));
return;
}
case 0xa:
{
const auto packet = processPolyPressure (helperValues);
callback (View (packet.data()));
return;
}
case 0xb:
{
PacketX2 packet;
if (processControlChange (helperValues, packet))
callback (View (packet.data()));
return;
}
case 0xc:
{
const auto packet = processProgramChange (helperValues);
callback (View (packet.data()));
return;
}
case 0xd:
{
const auto packet = processChannelPressure (helperValues);
callback (View (packet.data()));
return;
}
case 0xe:
{
const auto packet = processPitchBend (helperValues);
callback (View (packet.data()));
return;
}
}
}
void reset()
{
groupAccumulators = {};
groupBanks = {};
}
private:
enum class PnKind { nrpn, rpn };
struct HelperValues
{
uint8_t typeAndGroup;
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
};
static PacketX2 processNoteOnOrOff (const HelperValues helpers);
static PacketX2 processPolyPressure (const HelperValues helpers);
bool processControlChange (const HelperValues helpers, PacketX2& packet);
PacketX2 processProgramChange (const HelperValues helpers) const;
static PacketX2 processChannelPressure (const HelperValues helpers);
static PacketX2 processPitchBend (const HelperValues helpers);
class PnAccumulator
{
public:
bool addByte (uint8_t cc, uint8_t byte);
const std::array<uint8_t, 4>& getBytes() const noexcept { return bytes; }
PnKind getKind() const noexcept { return kind; }
private:
std::array<uint8_t, 4> bytes;
uint8_t index = 0;
PnKind kind = PnKind::nrpn;
};
class Bank
{
public:
bool isValid() const noexcept { return ! (msb & 0x80); }
uint8_t getMsb() const noexcept { return msb & 0x7f; }
uint8_t getLsb() const noexcept { return lsb & 0x7f; }
void setMsb (uint8_t i) noexcept { msb = i & 0x7f; }
void setLsb (uint8_t i) noexcept { msb &= 0x7f; lsb = i & 0x7f; }
private:
// We use the top bit to indicate whether this bank is valid.
// After reading the spec, it's not clear how we should determine whether
// there are valid values, so we'll just assume that the bank is valid
// once either the lsb or msb have been written.
uint8_t msb = 0x80;
uint8_t lsb = 0x00;
};
using ChannelAccumulators = std::array<PnAccumulator, 16>;
std::array<ChannelAccumulators, 16> groupAccumulators;
using ChannelBanks = std::array<Bank, 16>;
std::array<ChannelBanks, 16> groupBanks;
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Translates a series of MIDI 1 Universal MIDI Packets to corresponding MIDI 2
packets.
@tags{Audio}
*/
class Midi1ToMidi2DefaultTranslator
{
public:
Midi1ToMidi2DefaultTranslator() = default;
/** Converts MIDI 1 Universal MIDI Packets to corresponding MIDI 2 packets,
calling `callback` with each converted packet.
In some cases (such as RPN/NRPN messages) multiple MIDI 1 packets will
convert to a single MIDI 2 packet. In these cases, the translator will
accumulate the full message internally, and send a single callback with
the completed message, once all the individual MIDI 1 packets have been
processed.
*/
template <typename PacketCallback>
void dispatch (const View& v, PacketCallback&& callback)
{
const auto firstWord = v[0];
const auto messageType = Utils::getMessageType (firstWord);
if (messageType != 0x2)
{
callback (v);
return;
}
const HelperValues helperValues
{
(uint8_t) ((0x4 << 0x4) | Utils::getGroup (firstWord)),
(uint8_t) ((firstWord >> 0x10) & 0xff),
(uint8_t) ((firstWord >> 0x08) & 0x7f),
(uint8_t) ((firstWord >> 0x00) & 0x7f),
};
switch (Utils::getStatus (firstWord))
{
case 0x8:
case 0x9:
{
const auto packet = processNoteOnOrOff (helperValues);
callback (View (packet.data()));
return;
}
case 0xa:
{
const auto packet = processPolyPressure (helperValues);
callback (View (packet.data()));
return;
}
case 0xb:
{
PacketX2 packet;
if (processControlChange (helperValues, packet))
callback (View (packet.data()));
return;
}
case 0xc:
{
const auto packet = processProgramChange (helperValues);
callback (View (packet.data()));
return;
}
case 0xd:
{
const auto packet = processChannelPressure (helperValues);
callback (View (packet.data()));
return;
}
case 0xe:
{
const auto packet = processPitchBend (helperValues);
callback (View (packet.data()));
return;
}
}
}
void reset()
{
groupAccumulators = {};
groupBanks = {};
}
private:
enum class PnKind { nrpn, rpn };
struct HelperValues
{
uint8_t typeAndGroup;
uint8_t byte0;
uint8_t byte1;
uint8_t byte2;
};
static PacketX2 processNoteOnOrOff (const HelperValues helpers);
static PacketX2 processPolyPressure (const HelperValues helpers);
bool processControlChange (const HelperValues helpers, PacketX2& packet);
PacketX2 processProgramChange (const HelperValues helpers) const;
static PacketX2 processChannelPressure (const HelperValues helpers);
static PacketX2 processPitchBend (const HelperValues helpers);
class PnAccumulator
{
public:
bool addByte (uint8_t cc, uint8_t byte);
const std::array<uint8_t, 4>& getBytes() const noexcept { return bytes; }
PnKind getKind() const noexcept { return kind; }
private:
std::array<uint8_t, 4> bytes;
uint8_t index = 0;
PnKind kind = PnKind::nrpn;
};
class Bank
{
public:
bool isValid() const noexcept { return ! (msb & 0x80); }
uint8_t getMsb() const noexcept { return msb & 0x7f; }
uint8_t getLsb() const noexcept { return lsb & 0x7f; }
void setMsb (uint8_t i) noexcept { msb = i & 0x7f; }
void setLsb (uint8_t i) noexcept { msb &= 0x7f; lsb = i & 0x7f; }
private:
// We use the top bit to indicate whether this bank is valid.
// After reading the spec, it's not clear how we should determine whether
// there are valid values, so we'll just assume that the bank is valid
// once either the lsb or msb have been written.
uint8_t msb = 0x80;
uint8_t lsb = 0x00;
};
using ChannelAccumulators = std::array<PnAccumulator, 16>;
std::array<ChannelAccumulators, 16> groupAccumulators;
using ChannelBanks = std::array<Bank, 16>;
std::array<ChannelBanks, 16> groupBanks;
};
}
}
#endif

View File

@@ -1,44 +1,48 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets. */
enum class PacketProtocol
{
MIDI_1_0,
MIDI_2_0,
};
/** All kinds of MIDI protocol understood by JUCE. */
enum class MidiProtocol
{
bytestream,
UMP_MIDI_1_0,
UMP_MIDI_2_0,
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/** The kinds of MIDI protocol that can be formatted into Universal MIDI Packets. */
enum class PacketProtocol
{
MIDI_1_0,
MIDI_2_0,
};
/** All kinds of MIDI protocol understood by JUCE. */
enum class MidiProtocol
{
bytestream,
UMP_MIDI_1_0,
UMP_MIDI_2_0,
};
}
}
#endif

View File

@@ -1,42 +1,46 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
A base class for classes which receive Universal MIDI Packets from an input.
@tags{Audio}
*/
struct Receiver
{
virtual ~Receiver() noexcept = default;
/** This will be called each time a new packet is ready for processing. */
virtual void packetReceived (const View& packet, double time) = 0;
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
A base class for classes which receive Universal MIDI Packets from an input.
@tags{Audio}
*/
struct Receiver
{
virtual ~Receiver() noexcept = default;
/** This will be called each time a new packet is ready for processing. */
virtual void packetReceived (const View& packet, double time) = 0;
};
}
}
#endif

View File

@@ -1,53 +1,53 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
uint32_t SysEx7::getNumPacketsRequiredForDataSize (uint32_t size)
{
constexpr auto denom = 6;
return (size / denom) + ((size % denom) != 0);
}
SysEx7::PacketBytes SysEx7::getDataBytes (const PacketX2& packet)
{
const auto numBytes = Utils::getChannel (packet[0]);
constexpr uint8_t maxBytes = 6;
jassert (numBytes <= maxBytes);
return
{
{ { packet.getU8<2>(),
packet.getU8<3>(),
packet.getU8<4>(),
packet.getU8<5>(),
packet.getU8<6>(),
packet.getU8<7>() } },
jmin (numBytes, maxBytes)
};
}
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
uint32_t SysEx7::getNumPacketsRequiredForDataSize (uint32_t size)
{
constexpr auto denom = 6;
return (size / denom) + ((size % denom) != 0);
}
SysEx7::PacketBytes SysEx7::getDataBytes (const PacketX2& packet)
{
const auto numBytes = Utils::getChannel (packet[0]);
constexpr uint8_t maxBytes = 6;
jassert (numBytes <= maxBytes);
return
{
{ { packet.getU8<2>(),
packet.getU8<3>(),
packet.getU8<4>(),
packet.getU8<5>(),
packet.getU8<6>(),
packet.getU8<7>() } },
jmin (numBytes, maxBytes)
};
}
}
}

View File

@@ -1,73 +1,77 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
This struct acts as a single-file namespace for Univeral MIDI Packet
functionality related to 7-bit SysEx.
@tags{Audio}
*/
struct SysEx7
{
/** Returns the number of 64-bit packets required to hold a series of
SysEx bytes.
The number passed to this function should exclude the leading/trailing
SysEx bytes used in an old midi bytestream, as these are not required
when using Universal MIDI Packets.
*/
static uint32_t getNumPacketsRequiredForDataSize (uint32_t);
/** The different kinds of UMP SysEx-7 message. */
enum class Kind : uint8_t
{
/** The whole message fits in a single 2-word packet. */
complete = 0,
/** The packet begins a SysEx message that will continue in subsequent packets. */
begin = 1,
/** The packet is a continuation of an ongoing SysEx message. */
continuation = 2,
/** The packet terminates an ongoing SysEx message. */
end = 3
};
/** Holds the bytes from a single SysEx-7 packet. */
struct PacketBytes
{
std::array<uint8_t, 6> data;
uint8_t size;
};
/** Extracts the data bytes from a 64-bit data message. */
static PacketBytes getDataBytes (const PacketX2& packet);
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
This struct acts as a single-file namespace for Universal MIDI Packet
functionality related to 7-bit SysEx.
@tags{Audio}
*/
struct SysEx7
{
/** Returns the number of 64-bit packets required to hold a series of
SysEx bytes.
The number passed to this function should exclude the leading/trailing
SysEx bytes used in an old midi bytestream, as these are not required
when using Universal MIDI Packets.
*/
static uint32_t getNumPacketsRequiredForDataSize (uint32_t);
/** The different kinds of UMP SysEx-7 message. */
enum class Kind : uint8_t
{
/** The whole message fits in a single 2-word packet. */
complete = 0,
/** The packet begins a SysEx message that will continue in subsequent packets. */
begin = 1,
/** The packet is a continuation of an ongoing SysEx message. */
continuation = 2,
/** The packet terminates an ongoing SysEx message. */
end = 3
};
/** Holds the bytes from a single SysEx-7 packet. */
struct PacketBytes
{
std::array<uint8_t, 6> data;
uint8_t size;
};
/** Extracts the data bytes from a 64-bit data message. */
static PacketBytes getDataBytes (const PacketX2& packet);
};
}
}
#endif

View File

@@ -1,59 +1,59 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
uint32_t Utils::getNumWordsForMessageType (uint32_t mt)
{
switch (Utils::getMessageType (mt))
{
case 0x0:
case 0x1:
case 0x2:
case 0x6:
case 0x7:
return 1;
case 0x3:
case 0x4:
case 0x8:
case 0x9:
case 0xa:
return 2;
case 0xb:
case 0xc:
return 3;
case 0x5:
case 0xd:
case 0xe:
case 0xf:
return 4;
}
jassertfalse;
return 1;
}
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
uint32_t Utils::getNumWordsForMessageType (uint32_t mt)
{
switch (Utils::getMessageType (mt))
{
case 0x0:
case 0x1:
case 0x2:
case 0x6:
case 0x7:
return 1;
case 0x3:
case 0x4:
case 0x8:
case 0x9:
case 0xa:
return 2;
case 0xb:
case 0xc:
return 3;
case 0x5:
case 0xd:
case 0xe:
case 0xf:
return 4;
}
jassertfalse;
return 1;
}
}
}

View File

@@ -1,113 +1,117 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Helpful types and functions for interacting with Universal MIDI Packets.
@tags{Audio}
*/
struct Utils
{
/** Joins 4 bytes into a single 32-bit word. */
static constexpr uint32_t bytesToWord (uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
return uint32_t (a << 0x18 | b << 0x10 | c << 0x08 | d << 0x00);
}
/** Returns the expected number of 32-bit words in a Universal MIDI Packet, given
the first word of the packet.
The result will be between 1 and 4 inclusive.
A result of 1 means that the word is itself a complete packet.
*/
static uint32_t getNumWordsForMessageType (uint32_t);
/**
Helper functions for setting/getting 4-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U4
{
static constexpr uint32_t shift = (uint32_t) 0x1c - Index * 4;
static constexpr uint32_t set (uint32_t word, uint8_t value)
{
return (word & ~((uint32_t) 0xf << shift)) | (uint32_t) ((value & 0xf) << shift);
}
static constexpr uint8_t get (uint32_t word)
{
return (uint8_t) ((word >> shift) & 0xf);
}
};
/**
Helper functions for setting/getting 8-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U8
{
static constexpr uint32_t shift = (uint32_t) 0x18 - Index * 8;
static constexpr uint32_t set (uint32_t word, uint8_t value)
{
return (word & ~((uint32_t) 0xff << shift)) | (uint32_t) (value << shift);
}
static constexpr uint8_t get (uint32_t word)
{
return (uint8_t) ((word >> shift) & 0xff);
}
};
/**
Helper functions for setting/getting 16-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U16
{
static constexpr uint32_t shift = (uint32_t) 0x10 - Index * 16;
static constexpr uint32_t set (uint32_t word, uint16_t value)
{
return (word & ~((uint32_t) 0xffff << shift)) | (uint32_t) (value << shift);
}
static constexpr uint16_t get (uint32_t word)
{
return (uint16_t) ((word >> shift) & 0xffff);
}
};
static constexpr uint8_t getMessageType (uint32_t w) noexcept { return U4<0>::get (w); }
static constexpr uint8_t getGroup (uint32_t w) noexcept { return U4<1>::get (w); }
static constexpr uint8_t getStatus (uint32_t w) noexcept { return U4<2>::get (w); }
static constexpr uint8_t getChannel (uint32_t w) noexcept { return U4<3>::get (w); }
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Helpful types and functions for interacting with Universal MIDI Packets.
@tags{Audio}
*/
struct Utils
{
/** Joins 4 bytes into a single 32-bit word. */
static constexpr uint32_t bytesToWord (uint8_t a, uint8_t b, uint8_t c, uint8_t d)
{
return uint32_t (a << 0x18 | b << 0x10 | c << 0x08 | d << 0x00);
}
/** Returns the expected number of 32-bit words in a Universal MIDI Packet, given
the first word of the packet.
The result will be between 1 and 4 inclusive.
A result of 1 means that the word is itself a complete packet.
*/
static uint32_t getNumWordsForMessageType (uint32_t);
/**
Helper functions for setting/getting 4-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U4
{
static constexpr uint32_t shift = (uint32_t) 0x1c - Index * 4;
static constexpr uint32_t set (uint32_t word, uint8_t value)
{
return (word & ~((uint32_t) 0xf << shift)) | (uint32_t) ((value & 0xf) << shift);
}
static constexpr uint8_t get (uint32_t word)
{
return (uint8_t) ((word >> shift) & 0xf);
}
};
/**
Helper functions for setting/getting 8-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U8
{
static constexpr uint32_t shift = (uint32_t) 0x18 - Index * 8;
static constexpr uint32_t set (uint32_t word, uint8_t value)
{
return (word & ~((uint32_t) 0xff << shift)) | (uint32_t) (value << shift);
}
static constexpr uint8_t get (uint32_t word)
{
return (uint8_t) ((word >> shift) & 0xff);
}
};
/**
Helper functions for setting/getting 16-bit ranges inside a 32-bit word.
*/
template <size_t Index>
struct U16
{
static constexpr uint32_t shift = (uint32_t) 0x10 - Index * 16;
static constexpr uint32_t set (uint32_t word, uint16_t value)
{
return (word & ~((uint32_t) 0xffff << shift)) | (uint32_t) (value << shift);
}
static constexpr uint16_t get (uint32_t word)
{
return (uint16_t) ((word >> shift) & 0xffff);
}
};
static constexpr uint8_t getMessageType (uint32_t w) noexcept { return U4<0>::get (w); }
static constexpr uint8_t getGroup (uint32_t w) noexcept { return U4<1>::get (w); }
static constexpr uint8_t getStatus (uint32_t w) noexcept { return U4<2>::get (w); }
static constexpr uint8_t getChannel (uint32_t w) noexcept { return U4<3>::get (w); }
};
}
}
#endif

View File

@@ -1,35 +1,35 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
uint32_t View::size() const noexcept
{
jassert (ptr != nullptr);
return Utils::getNumWordsForMessageType (*ptr);
}
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
uint32_t View::size() const noexcept
{
jassert (ptr != nullptr);
return Utils::getNumWordsForMessageType (*ptr);
}
}
}

View File

@@ -1,88 +1,92 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Points to a single Universal MIDI Packet.
The packet must be well-formed for member functions to work correctly.
Specifically, the constructor argument must be the beginning of a region of
uint32_t that contains at least `getNumWordsForMessageType(*ddata)` items,
where `data` is the constructor argument.
NOTE: Instances of this class do not own the memory that they point to!
If you need to store a packet pointed-to by a View for later use, copy
the view contents to a Packets collection, or use the Utils::PacketX types.
@tags{Audio}
*/
class View
{
public:
/** Create an invalid view. */
View() noexcept = default;
/** Create a view of the packet starting at address `d`. */
explicit View (const uint32_t* data) noexcept : ptr (data) {}
/** Get a pointer to the first word in the Universal MIDI Packet currently
pointed-to by this view.
*/
const uint32_t* data() const noexcept { return ptr; }
/** Get the number of 32-words (between 1 and 4 inclusive) in the Universal
MIDI Packet currently pointed-to by this view.
*/
uint32_t size() const noexcept;
/** Get a specific word from this packet.
Passing an `index` that is greater than or equal to the result of `size`
will cause undefined behaviour.
*/
const uint32_t& operator[] (size_t index) const noexcept { return ptr[index]; }
/** Get an iterator pointing to the first word in the packet. */
const uint32_t* begin() const noexcept { return ptr; }
const uint32_t* cbegin() const noexcept { return ptr; }
/** Get an iterator pointing one-past the last word in the packet. */
const uint32_t* end() const noexcept { return ptr + size(); }
const uint32_t* cend() const noexcept { return ptr + size(); }
/** Return true if this view is pointing to the same address as another view. */
bool operator== (const View& other) const noexcept { return ptr == other.ptr; }
/** Return false if this view is pointing to the same address as another view. */
bool operator!= (const View& other) const noexcept { return ! operator== (other); }
private:
const uint32_t* ptr = nullptr;
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Points to a single Universal MIDI Packet.
The packet must be well-formed for member functions to work correctly.
Specifically, the constructor argument must be the beginning of a region of
uint32_t that contains at least `getNumWordsForMessageType(*ddata)` items,
where `data` is the constructor argument.
NOTE: Instances of this class do not own the memory that they point to!
If you need to store a packet pointed-to by a View for later use, copy
the view contents to a Packets collection, or use the Utils::PacketX types.
@tags{Audio}
*/
class View
{
public:
/** Create an invalid view. */
View() noexcept = default;
/** Create a view of the packet starting at address `d`. */
explicit View (const uint32_t* data) noexcept : ptr (data) {}
/** Get a pointer to the first word in the Universal MIDI Packet currently
pointed-to by this view.
*/
const uint32_t* data() const noexcept { return ptr; }
/** Get the number of 32-words (between 1 and 4 inclusive) in the Universal
MIDI Packet currently pointed-to by this view.
*/
uint32_t size() const noexcept;
/** Get a specific word from this packet.
Passing an `index` that is greater than or equal to the result of `size`
will cause undefined behaviour.
*/
const uint32_t& operator[] (size_t index) const noexcept { return ptr[index]; }
/** Get an iterator pointing to the first word in the packet. */
const uint32_t* begin() const noexcept { return ptr; }
const uint32_t* cbegin() const noexcept { return ptr; }
/** Get an iterator pointing one-past the last word in the packet. */
const uint32_t* end() const noexcept { return ptr + size(); }
const uint32_t* cend() const noexcept { return ptr + size(); }
/** Return true if this view is pointing to the same address as another view. */
bool operator== (const View& other) const noexcept { return ptr == other.ptr; }
/** Return false if this view is pointing to the same address as another view. */
bool operator!= (const View& other) const noexcept { return ! operator== (other); }
private:
const uint32_t* ptr = nullptr;
};
}
}
#endif

View File

@@ -1,189 +1,193 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Holds a single Universal MIDI Packet.
@tags{Audio}
*/
template <size_t numWords>
class Packet
{
public:
Packet() = default;
template <size_t w = numWords, typename std::enable_if<w == 1, int>::type = 0>
Packet (uint32_t a)
: contents { { a } }
{
jassert (Utils::getNumWordsForMessageType (a) == 1);
}
template <size_t w = numWords, typename std::enable_if<w == 2, int>::type = 0>
Packet (uint32_t a, uint32_t b)
: contents { { a, b } }
{
jassert (Utils::getNumWordsForMessageType (a) == 2);
}
template <size_t w = numWords, typename std::enable_if<w == 3, int>::type = 0>
Packet (uint32_t a, uint32_t b, uint32_t c)
: contents { { a, b, c } }
{
jassert (Utils::getNumWordsForMessageType (a) == 3);
}
template <size_t w = numWords, typename std::enable_if<w == 4, int>::type = 0>
Packet (uint32_t a, uint32_t b, uint32_t c, uint32_t d)
: contents { { a, b, c, d } }
{
jassert (Utils::getNumWordsForMessageType (a) == 4);
}
template <size_t w, typename std::enable_if<w == numWords, int>::type = 0>
explicit Packet (const std::array<uint32_t, w>& fullPacket)
: contents (fullPacket)
{
jassert (Utils::getNumWordsForMessageType (fullPacket.front()) == numWords);
}
Packet withMessageType (uint8_t type) const noexcept
{
return withU4<0> (type);
}
Packet withGroup (uint8_t group) const noexcept
{
return withU4<1> (group);
}
Packet withStatus (uint8_t status) const noexcept
{
return withU4<2> (status);
}
Packet withChannel (uint8_t channel) const noexcept
{
return withU4<3> (channel);
}
uint8_t getMessageType() const noexcept { return getU4<0>(); }
uint8_t getGroup() const noexcept { return getU4<1>(); }
uint8_t getStatus() const noexcept { return getU4<2>(); }
uint8_t getChannel() const noexcept { return getU4<3>(); }
template <size_t index>
Packet withU4 (uint8_t value) const noexcept
{
constexpr auto word = index / 8;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U4<index % 8>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU8 (uint8_t value) const noexcept
{
constexpr auto word = index / 4;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U8<index % 4>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU16 (uint16_t value) const noexcept
{
constexpr auto word = index / 2;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U16<index % 2>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU32 (uint32_t value) const noexcept
{
auto copy = *this;
std::get<index> (copy.contents) = value;
return copy;
}
template <size_t index>
uint8_t getU4() const noexcept
{
return Utils::U4<index % 8>::get (this->template getU32<index / 8>());
}
template <size_t index>
uint8_t getU8() const noexcept
{
return Utils::U8<index % 4>::get (this->template getU32<index / 4>());
}
template <size_t index>
uint16_t getU16() const noexcept
{
return Utils::U16<index % 2>::get (this->template getU32<index / 2>());
}
template <size_t index>
uint32_t getU32() const noexcept
{
return std::get<index> (contents);
}
//==============================================================================
using Contents = std::array<uint32_t, numWords>;
using const_iterator = typename Contents::const_iterator;
const_iterator begin() const noexcept { return contents.begin(); }
const_iterator cbegin() const noexcept { return contents.begin(); }
const_iterator end() const noexcept { return contents.end(); }
const_iterator cend() const noexcept { return contents.end(); }
const uint32_t* data() const noexcept { return contents.data(); }
const uint32_t& front() const noexcept { return contents.front(); }
const uint32_t& back() const noexcept { return contents.back(); }
const uint32_t& operator[] (size_t index) const noexcept { return contents[index]; }
private:
Contents contents { {} };
};
using PacketX1 = Packet<1>;
using PacketX2 = Packet<2>;
using PacketX3 = Packet<3>;
using PacketX4 = Packet<4>;
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Holds a single Universal MIDI Packet.
@tags{Audio}
*/
template <size_t numWords>
class Packet
{
public:
Packet() = default;
template <size_t w = numWords, typename std::enable_if<w == 1, int>::type = 0>
Packet (uint32_t a)
: contents { { a } }
{
jassert (Utils::getNumWordsForMessageType (a) == 1);
}
template <size_t w = numWords, typename std::enable_if<w == 2, int>::type = 0>
Packet (uint32_t a, uint32_t b)
: contents { { a, b } }
{
jassert (Utils::getNumWordsForMessageType (a) == 2);
}
template <size_t w = numWords, typename std::enable_if<w == 3, int>::type = 0>
Packet (uint32_t a, uint32_t b, uint32_t c)
: contents { { a, b, c } }
{
jassert (Utils::getNumWordsForMessageType (a) == 3);
}
template <size_t w = numWords, typename std::enable_if<w == 4, int>::type = 0>
Packet (uint32_t a, uint32_t b, uint32_t c, uint32_t d)
: contents { { a, b, c, d } }
{
jassert (Utils::getNumWordsForMessageType (a) == 4);
}
template <size_t w, typename std::enable_if<w == numWords, int>::type = 0>
explicit Packet (const std::array<uint32_t, w>& fullPacket)
: contents (fullPacket)
{
jassert (Utils::getNumWordsForMessageType (fullPacket.front()) == numWords);
}
Packet withMessageType (uint8_t type) const noexcept
{
return withU4<0> (type);
}
Packet withGroup (uint8_t group) const noexcept
{
return withU4<1> (group);
}
Packet withStatus (uint8_t status) const noexcept
{
return withU4<2> (status);
}
Packet withChannel (uint8_t channel) const noexcept
{
return withU4<3> (channel);
}
uint8_t getMessageType() const noexcept { return getU4<0>(); }
uint8_t getGroup() const noexcept { return getU4<1>(); }
uint8_t getStatus() const noexcept { return getU4<2>(); }
uint8_t getChannel() const noexcept { return getU4<3>(); }
template <size_t index>
Packet withU4 (uint8_t value) const noexcept
{
constexpr auto word = index / 8;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U4<index % 8>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU8 (uint8_t value) const noexcept
{
constexpr auto word = index / 4;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U8<index % 4>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU16 (uint16_t value) const noexcept
{
constexpr auto word = index / 2;
auto copy = *this;
std::get<word> (copy.contents) = Utils::U16<index % 2>::set (copy.template getU32<word>(), value);
return copy;
}
template <size_t index>
Packet withU32 (uint32_t value) const noexcept
{
auto copy = *this;
std::get<index> (copy.contents) = value;
return copy;
}
template <size_t index>
uint8_t getU4() const noexcept
{
return Utils::U4<index % 8>::get (this->template getU32<index / 8>());
}
template <size_t index>
uint8_t getU8() const noexcept
{
return Utils::U8<index % 4>::get (this->template getU32<index / 4>());
}
template <size_t index>
uint16_t getU16() const noexcept
{
return Utils::U16<index % 2>::get (this->template getU32<index / 2>());
}
template <size_t index>
uint32_t getU32() const noexcept
{
return std::get<index> (contents);
}
//==============================================================================
using Contents = std::array<uint32_t, numWords>;
using const_iterator = typename Contents::const_iterator;
const_iterator begin() const noexcept { return contents.begin(); }
const_iterator cbegin() const noexcept { return contents.begin(); }
const_iterator end() const noexcept { return contents.end(); }
const_iterator cend() const noexcept { return contents.end(); }
const uint32_t* data() const noexcept { return contents.data(); }
const uint32_t& front() const noexcept { return contents.front(); }
const uint32_t& back() const noexcept { return contents.back(); }
const uint32_t& operator[] (size_t index) const noexcept { return contents[index]; }
private:
Contents contents { {} };
};
using PacketX1 = Packet<1>;
using PacketX2 = Packet<2>;
using PacketX3 = Packet<3>;
using PacketX4 = Packet<4>;
}
}
#endif

View File

@@ -1,92 +1,96 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace universal_midi_packets
{
/**
Holds a collection of Universal MIDI Packets.
Unlike MidiBuffer, this collection does not store any additional information
(e.g. timestamps) alongside the raw messages.
If timestamps are required, these can be added to the container in UMP format,
as Jitter Reduction Utility messages.
@tags{Audio}
*/
class Packets
{
public:
/** Adds a single packet to the collection.
The View must be valid for this to work. If the view
points to a malformed message, or if the view points to a region
too short for the contained message, this call will result in
undefined behaviour.
*/
void add (const View& v) { storage.insert (storage.end(), v.cbegin(), v.cend()); }
void add (const PacketX1& p) { addImpl (p); }
void add (const PacketX2& p) { addImpl (p); }
void add (const PacketX3& p) { addImpl (p); }
void add (const PacketX4& p) { addImpl (p); }
/** Pre-allocates space for at least `numWords` 32-bit words in this collection. */
void reserve (size_t numWords) { storage.reserve (numWords); }
/** Removes all previously-added packets from this collection. */
void clear() { storage.clear(); }
/** Gets an iterator pointing to the first packet in this collection. */
Iterator cbegin() const noexcept { return Iterator (data(), size()); }
Iterator begin() const noexcept { return cbegin(); }
/** Gets an iterator pointing one-past the last packet in this collection. */
Iterator cend() const noexcept { return Iterator (data() + size(), 0); }
Iterator end() const noexcept { return cend(); }
/** Gets a pointer to the contents of the collection as a range of raw 32-bit words. */
const uint32_t* data() const noexcept { return storage.data(); }
/** Returns the number of uint32_t words in storage.
Note that this is likely to be larger than the number of packets
currently being stored, as some packets span multiple words.
*/
size_t size() const noexcept { return storage.size(); }
private:
template <size_t numWords>
void addImpl (const Packet<numWords>& p)
{
jassert (Utils::getNumWordsForMessageType (p[0]) == numWords);
add (View (p.data()));
}
std::vector<uint32_t> storage;
};
}
}
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#ifndef DOXYGEN
namespace juce
{
namespace universal_midi_packets
{
/**
Holds a collection of Universal MIDI Packets.
Unlike MidiBuffer, this collection does not store any additional information
(e.g. timestamps) alongside the raw messages.
If timestamps are required, these can be added to the container in UMP format,
as Jitter Reduction Utility messages.
@tags{Audio}
*/
class Packets
{
public:
/** Adds a single packet to the collection.
The View must be valid for this to work. If the view
points to a malformed message, or if the view points to a region
too short for the contained message, this call will result in
undefined behaviour.
*/
void add (const View& v) { storage.insert (storage.end(), v.cbegin(), v.cend()); }
void add (const PacketX1& p) { addImpl (p); }
void add (const PacketX2& p) { addImpl (p); }
void add (const PacketX3& p) { addImpl (p); }
void add (const PacketX4& p) { addImpl (p); }
/** Pre-allocates space for at least `numWords` 32-bit words in this collection. */
void reserve (size_t numWords) { storage.reserve (numWords); }
/** Removes all previously-added packets from this collection. */
void clear() { storage.clear(); }
/** Gets an iterator pointing to the first packet in this collection. */
Iterator cbegin() const noexcept { return Iterator (data(), size()); }
Iterator begin() const noexcept { return cbegin(); }
/** Gets an iterator pointing one-past the last packet in this collection. */
Iterator cend() const noexcept { return Iterator (data() + size(), 0); }
Iterator end() const noexcept { return cend(); }
/** Gets a pointer to the contents of the collection as a range of raw 32-bit words. */
const uint32_t* data() const noexcept { return storage.data(); }
/** Returns the number of uint32_t words in storage.
Note that this is likely to be larger than the number of packets
currently being stored, as some packets span multiple words.
*/
size_t size() const noexcept { return storage.size(); }
private:
template <size_t numWords>
void addImpl (const Packet<numWords>& p)
{
jassert (Utils::getNumWordsForMessageType (p[0]) == numWords);
add (View (p.data()));
}
std::vector<uint32_t> storage;
};
}
}
#endif

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,415 +1,426 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class represents an instrument handling MPE.
It has an MPE zone layout and maintains a state of currently
active (playing) notes and the values of their dimensions of expression.
You can trigger and modulate notes:
- by passing MIDI messages with the method processNextMidiEvent;
- by directly calling the methods noteOn, noteOff etc.
The class implements the channel and note management logic specified in
MPE. If you pass it a message, it will know what notes on what
channels (if any) should be affected by that message.
The class has a Listener class with the three callbacks MPENoteAdded,
MPENoteChanged, and MPENoteFinished. Implement such a
Listener class to react to note changes and trigger some functionality for
your application that depends on the MPE note state.
For example, you can use this class to write an MPE visualiser.
If you want to write a real-time audio synth with MPE functionality,
you should instead use the classes MPESynthesiserBase, which adds
the ability to render audio and to manage voices.
@see MPENote, MPEZoneLayout, MPESynthesiser
@tags{Audio}
*/
class JUCE_API MPEInstrument
{
public:
/** Constructor.
This will construct an MPE instrument with inactive lower and upper zones.
In order to process incoming MIDI, call setZoneLayout, define the layout
via MIDI RPN messages, or set the instrument to legacy mode.
*/
MPEInstrument() noexcept;
/** Destructor. */
virtual ~MPEInstrument();
//==============================================================================
/** Returns the current zone layout of the instrument.
This happens by value, to enforce thread-safety and class invariants.
Note: If the instrument is in legacy mode, the return value of this
method is unspecified.
*/
MPEZoneLayout getZoneLayout() const noexcept;
/** Re-sets the zone layout of the instrument to the one passed in.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This will also disable legacy mode in case it was enabled previously.
*/
void setZoneLayout (MPEZoneLayout newLayout);
/** Returns true if the given MIDI channel (1-16) is a note channel in any
of the MPEInstrument's MPE zones; false otherwise.
When in legacy mode, this will return true if the given channel is
contained in the current legacy mode channel range; false otherwise.
*/
bool isMemberChannel (int midiChannel) const noexcept;
/** Returns true if the given MIDI channel (1-16) is a master channel (channel
1 or 16).
In legacy mode, this will always return false.
*/
bool isMasterChannel (int midiChannel) const noexcept;
/** Returns true if the given MIDI channel (1-16) is used by any of the
MPEInstrument's MPE zones; false otherwise.
When in legacy mode, this will return true if the given channel is
contained in the current legacy mode channel range; false otherwise.
*/
bool isUsingChannel (int midiChannel) const noexcept;
//==============================================================================
/** The MPE note tracking mode. In case there is more than one note playing
simultaneously on the same MIDI channel, this determines which of these
notes will be modulated by an incoming MPE message on that channel
(pressure, pitchbend, or timbre).
The default is lastNotePlayedOnChannel.
*/
enum TrackingMode
{
lastNotePlayedOnChannel, /**< The most recent note on the channel that is still played (key down and/or sustained). */
lowestNoteOnChannel, /**< The lowest note (by initialNote) on the channel with the note key still down. */
highestNoteOnChannel, /**< The highest note (by initialNote) on the channel with the note key still down. */
allNotesOnChannel /**< All notes on the channel (key down and/or sustained). */
};
/** Set the MPE tracking mode for the pressure dimension. */
void setPressureTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the pitchbend dimension. */
void setPitchbendTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the timbre dimension. */
void setTimbreTrackingMode (TrackingMode modeToUse);
//==============================================================================
/** Process a MIDI message and trigger the appropriate method calls
(noteOn, noteOff etc.)
You can override this method if you need some special MIDI message
treatment on top of the standard MPE logic implemented here.
*/
virtual void processNextMidiEvent (const MidiMessage& message);
//==============================================================================
/** Request a note-on on the given channel, with the given initial note
number and velocity.
If the message arrives on a valid note channel, this will create a
new MPENote and call the noteAdded callback.
*/
virtual void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity);
/** Request a note-off.
If there is a matching playing note, this will release the note
(except if it is sustained by a sustain or sostenuto pedal) and call
the noteReleased callback.
*/
virtual void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity);
/** Request a pitchbend on the given channel with the given value (in units
of MIDI pitchwheel position).
Internally, this will determine whether the pitchwheel move is a
per-note pitchbend or a master pitchbend (depending on midiChannel),
take the correct per-note or master pitchbend range of the affected MPE
zone, and apply the resulting pitchbend to the affected note(s) (if any).
*/
virtual void pitchbend (int midiChannel, MPEValue pitchbend);
/** Request a pressure change on the given channel with the given value.
This will modify the pressure dimension of the note currently held down
on this channel (if any). If the channel is a zone master channel,
the pressure change will be broadcast to all notes in this zone.
*/
virtual void pressure (int midiChannel, MPEValue value);
/** Request a third dimension (timbre) change on the given channel with the
given value.
This will modify the timbre dimension of the note currently held down
on this channel (if any). If the channel is a zone master channel,
the timbre change will be broadcast to all notes in this zone.
*/
virtual void timbre (int midiChannel, MPEValue value);
/** Request a poly-aftertouch change for a given note number.
The change will be broadcast to all notes sharing the channel and note
number of the change message.
*/
virtual void polyAftertouch (int midiChannel, int midiNoteNumber, MPEValue value);
/** Request a sustain pedal press or release.
If midiChannel is a zone's master channel, this will act on all notes in
that zone; otherwise, nothing will happen.
*/
virtual void sustainPedal (int midiChannel, bool isDown);
/** Request a sostenuto pedal press or release.
If midiChannel is a zone's master channel, this will act on all notes in
that zone; otherwise, nothing will happen.
*/
virtual void sostenutoPedal (int midiChannel, bool isDown);
/** Discard all currently playing notes.
This will also call the noteReleased listener callback for all of them.
*/
void releaseAllNotes();
//==============================================================================
/** Returns the number of MPE notes currently played by the instrument. */
int getNumPlayingNotes() const noexcept;
/** Returns the note at the given index.
If there is no such note, returns an invalid MPENote. The notes are sorted
such that the most recently added note is the last element.
*/
MPENote getNote (int index) const noexcept;
/** Returns the note currently playing on the given midiChannel with the
specified initial MIDI note number, if there is such a note. Otherwise,
this returns an invalid MPENote (check with note.isValid() before use!)
*/
MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns the most recent note that is playing on the given midiChannel
(this will be the note which has received the most recent note-on without
a corresponding note-off), if there is such a note. Otherwise, this returns an
invalid MPENote (check with note.isValid() before use!)
*/
MPENote getMostRecentNote (int midiChannel) const noexcept;
/** Returns the most recent note that is not the note passed in. If there is no
such note, this returns an invalid MPENote (check with note.isValid() before use!).
This helper method might be useful for some custom voice handling algorithms.
*/
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
//==============================================================================
/** Derive from this class to be informed about any changes in the expressive
MIDI notes played by this instrument.
Note: This listener type receives its callbacks immediately, and not
via the message thread (so you might be for example in the MIDI thread).
Therefore you should never do heavy work such as graphics rendering etc.
inside those callbacks.
*/
class JUCE_API Listener
{
public:
/** Destructor. */
virtual ~Listener() = default;
/** Implement this callback to be informed whenever a new expressive MIDI
note is triggered.
*/
virtual void noteAdded (MPENote newNote) { ignoreUnused (newNote); }
/** Implement this callback to be informed whenever a currently playing
MPE note's pressure value changes.
*/
virtual void notePressureChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whenever a currently playing
MPE note's pitchbend value changes.
Note: This can happen if the note itself is bent, if there is a
master channel pitchbend event, or if both occur simultaneously.
Call MPENote::getFrequencyInHertz to get the effective note frequency.
*/
virtual void notePitchbendChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whenever a currently playing
MPE note's timbre value changes.
*/
virtual void noteTimbreChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whether a currently playing
MPE note's key state (whether the key is down and/or the note is
sustained) has changed.
Note: If the key state changes to MPENote::off, noteReleased is
called instead.
*/
virtual void noteKeyStateChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whenever an MPE note
is released (either by a note-off message, or by a sustain/sostenuto
pedal release for a note that already received a note-off),
and should therefore stop playing.
*/
virtual void noteReleased (MPENote finishedNote) { ignoreUnused (finishedNote); }
};
//==============================================================================
/** Adds a listener. */
void addListener (Listener* listenerToAdd);
/** Removes a listener. */
void removeListener (Listener* listenerToRemove);
//==============================================================================
/** Puts the instrument into legacy mode.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This special zone layout mode is for backwards compatibility with
non-MPE MIDI devices. In this mode, the instrument will ignore the
current MPE zone layout. It will instead take a range of MIDI channels
(default: all channels 1-16) and treat them as note channels, with no
master channel. MIDI channels outside of this range will be ignored.
@param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
Must be between 0 and 96, otherwise behaviour is undefined.
The default pitchbend range in legacy mode is +/- 2 semitones.
@param channelRange The range of MIDI channels to use for notes when in legacy mode.
The default is to use all MIDI channels (1-16).
To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
*/
void enableLegacyMode (int pitchbendRange = 2,
Range<int> channelRange = Range<int> (1, 17));
/** Returns true if the instrument is in legacy mode, false otherwise. */
bool isLegacyModeEnabled() const noexcept;
/** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
Range<int> getLegacyModeChannelRange() const noexcept;
/** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
void setLegacyModeChannelRange (Range<int> channelRange);
/** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
int getLegacyModePitchbendRange() const noexcept;
/** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
void setLegacyModePitchbendRange (int pitchbendRange);
protected:
//==============================================================================
CriticalSection lock;
private:
//==============================================================================
Array<MPENote> notes;
MPEZoneLayout zoneLayout;
ListenerList<Listener> listeners;
uint8 lastPressureLowerBitReceivedOnChannel[16];
uint8 lastTimbreLowerBitReceivedOnChannel[16];
bool isMemberChannelSustained[16];
struct LegacyMode
{
bool isEnabled;
Range<int> channelRange;
int pitchbendRange;
};
struct MPEDimension
{
TrackingMode trackingMode = lastNotePlayedOnChannel;
MPEValue lastValueReceivedOnChannel[16];
MPEValue MPENote::* value;
MPEValue& getValue (MPENote& note) noexcept { return note.*(value); }
};
LegacyMode legacyMode;
MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
void resetLastReceivedValues();
void updateDimension (int midiChannel, MPEDimension&, MPEValue);
void updateDimensionMaster (bool, MPEDimension&, MPEValue);
void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue);
void callListenersDimensionChanged (const MPENote&, const MPEDimension&);
MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const;
void processMidiNoteOnMessage (const MidiMessage&);
void processMidiNoteOffMessage (const MidiMessage&);
void processMidiPitchWheelMessage (const MidiMessage&);
void processMidiChannelPressureMessage (const MidiMessage&);
void processMidiControllerMessage (const MidiMessage&);
void processMidiResetAllControllersMessage (const MidiMessage&);
void processMidiAfterTouchMessage (const MidiMessage&);
void handlePressureMSB (int midiChannel, int value) noexcept;
void handlePressureLSB (int midiChannel, int value) noexcept;
void handleTimbreMSB (int midiChannel, int value) noexcept;
void handleTimbreLSB (int midiChannel, int value) noexcept;
void handleSustainOrSostenuto (int midiChannel, bool isDown, bool isSostenuto);
const MPENote* getNotePtr (int midiChannel, int midiNoteNumber) const noexcept;
MPENote* getNotePtr (int midiChannel, int midiNoteNumber) noexcept;
const MPENote* getNotePtr (int midiChannel, TrackingMode) const noexcept;
MPENote* getNotePtr (int midiChannel, TrackingMode) noexcept;
const MPENote* getLastNotePlayedPtr (int midiChannel) const noexcept;
MPENote* getLastNotePlayedPtr (int midiChannel) noexcept;
const MPENote* getHighestNotePtr (int midiChannel) const noexcept;
MPENote* getHighestNotePtr (int midiChannel) noexcept;
const MPENote* getLowestNotePtr (int midiChannel) const noexcept;
MPENote* getLowestNotePtr (int midiChannel) noexcept;
void updateNoteTotalPitchbend (MPENote&);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEInstrument)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class represents an instrument handling MPE.
It has an MPE zone layout and maintains a state of currently
active (playing) notes and the values of their dimensions of expression.
You can trigger and modulate notes:
- by passing MIDI messages with the method processNextMidiEvent;
- by directly calling the methods noteOn, noteOff etc.
The class implements the channel and note management logic specified in
MPE. If you pass it a message, it will know what notes on what
channels (if any) should be affected by that message.
The class has a Listener class that can be used to react to note and
state changes and trigger some functionality for your application.
For example, you can use this class to write an MPE visualiser.
If you want to write a real-time audio synth with MPE functionality,
you should instead use the classes MPESynthesiserBase, which adds
the ability to render audio and to manage voices.
@see MPENote, MPEZoneLayout, MPESynthesiser
@tags{Audio}
*/
class JUCE_API MPEInstrument
{
public:
/** Constructor.
This will construct an MPE instrument with inactive lower and upper zones.
In order to process incoming MIDI messages call setZoneLayout, use the MPEZoneLayout
constructor, define the layout via MIDI RPN messages, or set the instrument to legacy mode.
*/
MPEInstrument() noexcept;
/** Constructs an MPE instrument with the specified zone layout. */
MPEInstrument (MPEZoneLayout layout);
/** Destructor. */
virtual ~MPEInstrument();
//==============================================================================
/** Returns the current zone layout of the instrument.
This happens by value, to enforce thread-safety and class invariants.
Note: If the instrument is in legacy mode, the return value of this
method is unspecified.
*/
MPEZoneLayout getZoneLayout() const noexcept;
/** Re-sets the zone layout of the instrument to the one passed in.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This will also disable legacy mode in case it was enabled previously.
*/
void setZoneLayout (MPEZoneLayout newLayout);
/** Returns true if the given MIDI channel (1-16) is a note channel in any
of the MPEInstrument's MPE zones; false otherwise.
When in legacy mode, this will return true if the given channel is
contained in the current legacy mode channel range; false otherwise.
*/
bool isMemberChannel (int midiChannel) const noexcept;
/** Returns true if the given MIDI channel (1-16) is a master channel (channel
1 or 16).
In legacy mode, this will always return false.
*/
bool isMasterChannel (int midiChannel) const noexcept;
/** Returns true if the given MIDI channel (1-16) is used by any of the
MPEInstrument's MPE zones; false otherwise.
When in legacy mode, this will return true if the given channel is
contained in the current legacy mode channel range; false otherwise.
*/
bool isUsingChannel (int midiChannel) const noexcept;
//==============================================================================
/** The MPE note tracking mode. In case there is more than one note playing
simultaneously on the same MIDI channel, this determines which of these
notes will be modulated by an incoming MPE message on that channel
(pressure, pitchbend, or timbre).
The default is lastNotePlayedOnChannel.
*/
enum TrackingMode
{
lastNotePlayedOnChannel, /**< The most recent note on the channel that is still played (key down and/or sustained). */
lowestNoteOnChannel, /**< The lowest note (by initialNote) on the channel with the note key still down. */
highestNoteOnChannel, /**< The highest note (by initialNote) on the channel with the note key still down. */
allNotesOnChannel /**< All notes on the channel (key down and/or sustained). */
};
/** Set the MPE tracking mode for the pressure dimension. */
void setPressureTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the pitchbend dimension. */
void setPitchbendTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the timbre dimension. */
void setTimbreTrackingMode (TrackingMode modeToUse);
//==============================================================================
/** Process a MIDI message and trigger the appropriate method calls
(noteOn, noteOff etc.)
You can override this method if you need some special MIDI message
treatment on top of the standard MPE logic implemented here.
*/
virtual void processNextMidiEvent (const MidiMessage& message);
//==============================================================================
/** Request a note-on on the given channel, with the given initial note
number and velocity.
If the message arrives on a valid note channel, this will create a
new MPENote and call the noteAdded callback.
*/
virtual void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity);
/** Request a note-off.
If there is a matching playing note, this will release the note
(except if it is sustained by a sustain or sostenuto pedal) and call
the noteReleased callback.
*/
virtual void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity);
/** Request a pitchbend on the given channel with the given value (in units
of MIDI pitchwheel position).
Internally, this will determine whether the pitchwheel move is a
per-note pitchbend or a master pitchbend (depending on midiChannel),
take the correct per-note or master pitchbend range of the affected MPE
zone, and apply the resulting pitchbend to the affected note(s) (if any).
*/
virtual void pitchbend (int midiChannel, MPEValue pitchbend);
/** Request a pressure change on the given channel with the given value.
This will modify the pressure dimension of the note currently held down
on this channel (if any). If the channel is a zone master channel,
the pressure change will be broadcast to all notes in this zone.
*/
virtual void pressure (int midiChannel, MPEValue value);
/** Request a third dimension (timbre) change on the given channel with the
given value.
This will modify the timbre dimension of the note currently held down
on this channel (if any). If the channel is a zone master channel,
the timbre change will be broadcast to all notes in this zone.
*/
virtual void timbre (int midiChannel, MPEValue value);
/** Request a poly-aftertouch change for a given note number.
The change will be broadcast to all notes sharing the channel and note
number of the change message.
*/
virtual void polyAftertouch (int midiChannel, int midiNoteNumber, MPEValue value);
/** Request a sustain pedal press or release.
If midiChannel is a zone's master channel, this will act on all notes in
that zone; otherwise, nothing will happen.
*/
virtual void sustainPedal (int midiChannel, bool isDown);
/** Request a sostenuto pedal press or release.
If midiChannel is a zone's master channel, this will act on all notes in
that zone; otherwise, nothing will happen.
*/
virtual void sostenutoPedal (int midiChannel, bool isDown);
/** Discard all currently playing notes.
This will also call the noteReleased listener callback for all of them.
*/
void releaseAllNotes();
//==============================================================================
/** Returns the number of MPE notes currently played by the instrument. */
int getNumPlayingNotes() const noexcept;
/** Returns the note at the given index.
If there is no such note, returns an invalid MPENote. The notes are sorted
such that the most recently added note is the last element.
*/
MPENote getNote (int index) const noexcept;
/** Returns the note currently playing on the given midiChannel with the
specified initial MIDI note number, if there is such a note. Otherwise,
this returns an invalid MPENote (check with note.isValid() before use!)
*/
MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns the note with a given ID. */
MPENote getNoteWithID (uint16 noteID) const noexcept;
/** Returns the most recent note that is playing on the given midiChannel
(this will be the note which has received the most recent note-on without
a corresponding note-off), if there is such a note. Otherwise, this returns an
invalid MPENote (check with note.isValid() before use!)
*/
MPENote getMostRecentNote (int midiChannel) const noexcept;
/** Returns the most recent note that is not the note passed in. If there is no
such note, this returns an invalid MPENote (check with note.isValid() before use!).
This helper method might be useful for some custom voice handling algorithms.
*/
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
//==============================================================================
/** Derive from this class to be informed about any changes in the MPE notes played
by this instrument, and any changes to its zone layout.
Note: This listener type receives its callbacks immediately, and not
via the message thread (so you might be for example in the MIDI thread).
Therefore you should never do heavy work such as graphics rendering etc.
inside those callbacks.
*/
class JUCE_API Listener
{
public:
/** Destructor. */
virtual ~Listener() = default;
/** Implement this callback to be informed whenever a new expressive MIDI
note is triggered.
*/
virtual void noteAdded (MPENote newNote) { ignoreUnused (newNote); }
/** Implement this callback to be informed whenever a currently playing
MPE note's pressure value changes.
*/
virtual void notePressureChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whenever a currently playing
MPE note's pitchbend value changes.
Note: This can happen if the note itself is bent, if there is a
master channel pitchbend event, or if both occur simultaneously.
Call MPENote::getFrequencyInHertz to get the effective note frequency.
*/
virtual void notePitchbendChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whenever a currently playing
MPE note's timbre value changes.
*/
virtual void noteTimbreChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whether a currently playing
MPE note's key state (whether the key is down and/or the note is
sustained) has changed.
Note: If the key state changes to MPENote::off, noteReleased is
called instead.
*/
virtual void noteKeyStateChanged (MPENote changedNote) { ignoreUnused (changedNote); }
/** Implement this callback to be informed whenever an MPE note
is released (either by a note-off message, or by a sustain/sostenuto
pedal release for a note that already received a note-off),
and should therefore stop playing.
*/
virtual void noteReleased (MPENote finishedNote) { ignoreUnused (finishedNote); }
/** Implement this callback to be informed whenever the MPE zone layout
or legacy mode settings of this instrument have been changed.
*/
virtual void zoneLayoutChanged() {}
};
//==============================================================================
/** Adds a listener. */
void addListener (Listener* listenerToAdd);
/** Removes a listener. */
void removeListener (Listener* listenerToRemove);
//==============================================================================
/** Puts the instrument into legacy mode. If legacy mode is already enabled this method
does nothing.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This special zone layout mode is for backwards compatibility with
non-MPE MIDI devices. In this mode, the instrument will ignore the
current MPE zone layout. It will instead take a range of MIDI channels
(default: all channels 1-16) and treat them as note channels, with no
master channel. MIDI channels outside of this range will be ignored.
@param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
Must be between 0 and 96, otherwise behaviour is undefined.
The default pitchbend range in legacy mode is +/- 2 semitones.
@param channelRange The range of MIDI channels to use for notes when in legacy mode.
The default is to use all MIDI channels (1-16).
To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
*/
void enableLegacyMode (int pitchbendRange = 2,
Range<int> channelRange = Range<int> (1, 17));
/** Returns true if the instrument is in legacy mode, false otherwise. */
bool isLegacyModeEnabled() const noexcept;
/** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
Range<int> getLegacyModeChannelRange() const noexcept;
/** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
void setLegacyModeChannelRange (Range<int> channelRange);
/** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
int getLegacyModePitchbendRange() const noexcept;
/** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
void setLegacyModePitchbendRange (int pitchbendRange);
protected:
//==============================================================================
CriticalSection lock;
private:
//==============================================================================
Array<MPENote> notes;
MPEZoneLayout zoneLayout;
ListenerList<Listener> listeners;
uint8 lastPressureLowerBitReceivedOnChannel[16];
uint8 lastTimbreLowerBitReceivedOnChannel[16];
bool isMemberChannelSustained[16];
struct LegacyMode
{
bool isEnabled = false;
Range<int> channelRange;
int pitchbendRange = 2;
};
struct MPEDimension
{
TrackingMode trackingMode = lastNotePlayedOnChannel;
MPEValue lastValueReceivedOnChannel[16];
MPEValue MPENote::* value;
MPEValue& getValue (MPENote& note) noexcept { return note.*(value); }
};
LegacyMode legacyMode;
MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
void resetLastReceivedValues();
void updateDimension (int midiChannel, MPEDimension&, MPEValue);
void updateDimensionMaster (bool, MPEDimension&, MPEValue);
void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue);
void callListenersDimensionChanged (const MPENote&, const MPEDimension&);
MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const;
void processMidiNoteOnMessage (const MidiMessage&);
void processMidiNoteOffMessage (const MidiMessage&);
void processMidiPitchWheelMessage (const MidiMessage&);
void processMidiChannelPressureMessage (const MidiMessage&);
void processMidiControllerMessage (const MidiMessage&);
void processMidiResetAllControllersMessage (const MidiMessage&);
void processMidiAfterTouchMessage (const MidiMessage&);
void handlePressureMSB (int midiChannel, int value) noexcept;
void handlePressureLSB (int midiChannel, int value) noexcept;
void handleTimbreMSB (int midiChannel, int value) noexcept;
void handleTimbreLSB (int midiChannel, int value) noexcept;
void handleSustainOrSostenuto (int midiChannel, bool isDown, bool isSostenuto);
const MPENote* getNotePtr (int midiChannel, int midiNoteNumber) const noexcept;
MPENote* getNotePtr (int midiChannel, int midiNoteNumber) noexcept;
const MPENote* getNotePtr (int midiChannel, TrackingMode) const noexcept;
MPENote* getNotePtr (int midiChannel, TrackingMode) noexcept;
const MPENote* getLastNotePlayedPtr (int midiChannel) const noexcept;
MPENote* getLastNotePlayedPtr (int midiChannel) noexcept;
const MPENote* getHighestNotePtr (int midiChannel) const noexcept;
MPENote* getHighestNotePtr (int midiChannel) noexcept;
const MPENote* getLowestNotePtr (int midiChannel) const noexcept;
MPENote* getLowestNotePtr (int midiChannel) noexcept;
void updateNoteTotalPitchbend (MPENote&);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEInstrument)
};
} // namespace juce

View File

@@ -1,376 +0,0 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2017 - ROLI Ltd.
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
#pragma once
//==============================================================================
/*
This class represents an instrument handling MPE.
It has an MPE zone layout and maintans a state of currently
active (playing) notes and the values of their dimensions of expression.
You can trigger and modulate notes:
- by passing MIDI messages with the method processNextMidiEvent;
- by directly calling the methods noteOn, noteOff etc.
The class implements the channel and note management logic specified in
MPE. If you pass it a message, it will know what notes on what
channels (if any) should be affected by that message.
The class has a Listener class with the three callbacks MPENoteAdded,
MPENoteChanged, and MPENoteFinished. Implement such a
Listener class to react to note changes and trigger some functionality for
your application that depends on the MPE note state.
For example, you can use this class to write an MPE visualiser.
If you want to write a real-time audio synth with MPE functionality,
you should instead use the classes MPESynthesiserBase, which adds
the ability to render audio and to manage voices.
@see MPENote, MPEZoneLayout, MPESynthesiser
*/
class JUCE_API MPEInstrument
{
public:
/** Constructor.
This will construct an MPE instrument with initially no MPE zones.
In order to process incoming MIDI, call setZoneLayout, define the layout
via MIDI RPN messages, or set the instrument to legacy mode.
*/
MPEInstrument() noexcept;
/** Destructor. */
virtual ~MPEInstrument();
//==============================================================================
/** Returns the current zone layout of the instrument.
This happens by value, to enforce thread-safety and class invariants.
Note: If the instrument is in legacy mode, the return value of this
method is unspecified.
*/
MPEZoneLayout getZoneLayout() const noexcept;
/** Re-sets the zone layout of the instrument to the one passed in.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This will also disable legacy mode in case it was enabled previously.
*/
void setZoneLayout (MPEZoneLayout newLayout);
/** Returns true if the given MIDI channel (1-16) is a note channel in any
of the MPEInstrument's MPE zones; false otherwise.
When in legacy mode, this will return true if the given channel is
contained in the current legacy mode channel range; false otherwise.
*/
bool isNoteChannel (int midiChannel) const noexcept;
/** Returns true if the given MIDI channel (1-16) is a master channel in any
of the MPEInstrument's MPE zones; false otherwise.
When in legacy mode, this will always return false.
*/
bool isMasterChannel (int midiChannel) const noexcept;
//==============================================================================
/** The MPE note tracking mode. In case there is more than one note playing
simultaneously on the same MIDI channel, this determines which of these
notes will be modulated by an incoming MPE message on that channel
(pressure, pitchbend, or timbre).
The default is lastNotePlayedOnChannel.
*/
enum TrackingMode
{
lastNotePlayedOnChannel, //! The most recent note on the channel that is still played (key down and/or sustained)
lowestNoteOnChannel, //! The lowest note (by initialNote) on the channel with the note key still down
highestNoteOnChannel, //! The highest note (by initialNote) on the channel with the note key still down
allNotesOnChannel //! All notes on the channel (key down and/or sustained)
};
/** Set the MPE tracking mode for the pressure dimension. */
void setPressureTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the pitchbend dimension. */
void setPitchbendTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the timbre dimension. */
void setTimbreTrackingMode (TrackingMode modeToUse);
//==============================================================================
/** Process a MIDI message and trigger the appropriate method calls
(noteOn, noteOff etc.)
You can override this method if you need some special MIDI message
treatment on top of the standard MPE logic implemented here.
*/
virtual void processNextMidiEvent (const MidiMessage& message);
//==============================================================================
/** Request a note-on on the given channel, with the given initial note
number and velocity.
If the message arrives on a valid note channel, this will create a
new MPENote and call the noteAdded callback.
*/
virtual void noteOn (int midiChannel, int midiNoteNumber, MPEValue midiNoteOnVelocity);
/** Request a note-off. If there is a matching playing note, this will
release the note (except if it is sustained by a sustain or sostenuto
pedal) and call the noteReleased callback.
*/
virtual void noteOff (int midiChannel, int midiNoteNumber, MPEValue midiNoteOffVelocity);
/** Request a pitchbend on the given channel with the given value (in units
of MIDI pitchwheel position).
Internally, this will determine whether the pitchwheel move is a
per-note pitchbend or a master pitchbend (depending on midiChannel),
take the correct per-note or master pitchbend range of the affected MPE
zone, and apply the resulting pitchbend to the affected note(s) (if any).
*/
virtual void pitchbend (int midiChannel, MPEValue pitchbend);
/** Request a pressure change on the given channel with the given value.
This will modify the pressure dimension of the note currently held down
on this channel (if any). If the channel is a zone master channel,
the pressure change will be broadcast to all notes in this zone.
*/
virtual void pressure (int midiChannel, MPEValue value);
/** Request a third dimension (timbre) change on the given channel with the
given value.
This will modify the timbre dimension of the note currently held down
on this channel (if any). If the channel is a zone master channel,
the timbre change will be broadcast to all notes in this zone.
*/
virtual void timbre (int midiChannel, MPEValue value);
/** Request a sustain pedal press or release. If midiChannel is a zone's
master channel, this will act on all notes in that zone; otherwise,
nothing will happen.
*/
virtual void sustainPedal (int midiChannel, bool isDown);
/** Request a sostenuto pedal press or release. If midiChannel is a zone's
master channel, this will act on all notes in that zone; otherwise,
nothing will happen.
*/
virtual void sostenutoPedal (int midiChannel, bool isDown);
/** Discard all currently playing notes.
This will also call the noteReleased listener callback for all of them.
*/
void releaseAllNotes();
//==============================================================================
/** Returns the number of MPE notes currently played by the
instrument.
*/
int getNumPlayingNotes() const noexcept;
/** Returns the note at the given index. If there is no such note, returns
an invalid MPENote. The notes are sorted such that the most recently
added note is the last element.
*/
MPENote getNote (int index) const noexcept;
/** Returns the note currently playing on the given midiChannel with the
specified initial MIDI note number, if there is such a note.
Otherwise, this returns an invalid MPENote
(check with note.isValid() before use!)
*/
MPENote getNote (int midiChannel, int midiNoteNumber) const noexcept;
/** Returns the most recent note that is playing on the given midiChannel
(this will be the note which has received the most recent note-on without
a corresponding note-off), if there is such a note.
Otherwise, this returns an invalid MPENote
(check with note.isValid() before use!)
*/
MPENote getMostRecentNote (int midiChannel) const noexcept;
/** Returns the most recent note that is not the note passed in.
If there is no such note, this returns an invalid MPENote
(check with note.isValid() before use!)
This helper method might be useful for some custom voice handling algorithms.
*/
MPENote getMostRecentNoteOtherThan (MPENote otherThanThisNote) const noexcept;
//==============================================================================
/** Derive from this class to be informed about any changes in the expressive
MIDI notes played by this instrument.
Note: This listener type receives its callbacks immediately, and not
via the message thread (so you might be for example in the MIDI thread).
Therefore you should never do heavy work such as graphics rendering etc.
inside those callbacks.
*/
class JUCE_API Listener
{
public:
/** Destructor. */
virtual ~Listener() {}
/** Implement this callback to be informed whenever a new expressive
MIDI note is triggered.
*/
virtual void noteAdded (MPENote newNote) = 0;
/** Implement this callback to be informed whenever a currently
playing MPE note's pressure value changes.
*/
virtual void notePressureChanged (MPENote changedNote) = 0;
/** Implement this callback to be informed whenever a currently
playing MPE note's pitchbend value changes.
Note: This can happen if the note itself is bent, if there is a
master channel pitchbend event, or if both occur simultaneously.
Call MPENote::getFrequencyInHertz to get the effective note frequency.
*/
virtual void notePitchbendChanged (MPENote changedNote) = 0;
/** Implement this callback to be informed whenever a currently
playing MPE note's timbre value changes.
*/
virtual void noteTimbreChanged (MPENote changedNote) = 0;
/** Implement this callback to be informed whether a currently playing
MPE note's key state (whether the key is down and/or the note is
sustained) has changed.
Note: if the key state changes to MPENote::off, noteReleased is
called instead.
*/
virtual void noteKeyStateChanged (MPENote changedNote) = 0;
/** Implement this callback to be informed whenever an MPE note
is released (either by a note-off message, or by a sustain/sostenuto
pedal release for a note that already received a note-off),
and should therefore stop playing.
*/
virtual void noteReleased (MPENote finishedNote) = 0;
};
//==============================================================================
/** Adds a listener. */
void addListener (Listener* listenerToAdd) noexcept;
/** Removes a listener. */
void removeListener (Listener* listenerToRemove) noexcept;
//==============================================================================
/** Puts the instrument into legacy mode.
As a side effect, this will discard all currently playing notes,
and call noteReleased for all of them.
This special zone layout mode is for backwards compatibility with
non-MPE MIDI devices. In this mode, the instrument will ignore the
current MPE zone layout. It will instead take a range of MIDI channels
(default: all channels 1-16) and treat them as note channels, with no
master channel. MIDI channels outside of this range will be ignored.
@param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
Must be between 0 and 96, otherwise behaviour is undefined.
The default pitchbend range in legacy mode is +/- 2 semitones.
@param channelRange The range of MIDI channels to use for notes when in legacy mode.
The default is to use all MIDI channels (1-16).
To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
*/
void enableLegacyMode (int pitchbendRange = 2,
Range<int> channelRange = Range<int> (1, 17));
/** Returns true if the instrument is in legacy mode, false otherwise. */
bool isLegacyModeEnabled() const noexcept;
/** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
Range<int> getLegacyModeChannelRange() const noexcept;
/** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
void setLegacyModeChannelRange (Range<int> channelRange);
/** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
int getLegacyModePitchbendRange() const noexcept;
/** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
void setLegacyModePitchbendRange (int pitchbendRange);
private:
//==============================================================================
CriticalSection lock;
Array<MPENote> notes;
MPEZoneLayout zoneLayout;
ListenerList<Listener> listeners;
uint8 lastPressureLowerBitReceivedOnChannel[16];
uint8 lastTimbreLowerBitReceivedOnChannel[16];
bool isNoteChannelSustained[16];
struct LegacyMode
{
bool isEnabled;
Range<int> channelRange;
int pitchbendRange;
};
struct MPEDimension
{
MPEDimension() noexcept : trackingMode (lastNotePlayedOnChannel) {}
TrackingMode trackingMode;
MPEValue lastValueReceivedOnChannel[16];
MPEValue MPENote::* value;
MPEValue& getValue (MPENote& note) noexcept { return note.*(value); }
};
LegacyMode legacyMode;
MPEDimension pitchbendDimension, pressureDimension, timbreDimension;
void updateDimension (int midiChannel, MPEDimension&, MPEValue);
void updateDimensionMaster (MPEZone&, MPEDimension&, MPEValue);
void updateDimensionForNote (MPENote&, MPEDimension&, MPEValue);
void callListenersDimensionChanged (MPENote&, MPEDimension&);
MPEValue getInitialValueForNewNote (int midiChannel, MPEDimension&) const;
void processMidiNoteOnMessage (const MidiMessage&);
void processMidiNoteOffMessage (const MidiMessage&);
void processMidiPitchWheelMessage (const MidiMessage&);
void processMidiChannelPressureMessage (const MidiMessage&);
void processMidiControllerMessage (const MidiMessage&);
void processMidiAllNotesOffMessage (const MidiMessage&);
void handlePressureMSB (int midiChannel, int value) noexcept;
void handlePressureLSB (int midiChannel, int value) noexcept;
void handleTimbreMSB (int midiChannel, int value) noexcept;
void handleTimbreLSB (int midiChannel, int value) noexcept;
void handleSustainOrSostenuto (int midiChannel, bool isDown, bool isSostenuto);
MPENote* getNotePtr (int midiChannel, int midiNoteNumber) const noexcept;
MPENote* getNotePtr (int midiChannel, TrackingMode) const noexcept;
MPENote* getLastNotePlayedPtr (int midiChannel) const noexcept;
MPENote* getHighestNotePtr (int midiChannel) const noexcept;
MPENote* getLowestNotePtr (int midiChannel) const noexcept;
void updateNoteTotalPitchbend (MPENote&);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPEInstrument)
};

View File

@@ -1,238 +1,238 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MidiBuffer MPEMessages::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
{
auto buffer = MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
buffer.addEvents (setLowerZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
buffer.addEvents (setLowerZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
return buffer;
}
MidiBuffer MPEMessages::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
{
auto buffer = MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
buffer.addEvents (setUpperZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
buffer.addEvents (setUpperZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
return buffer;
}
MidiBuffer MPEMessages::setLowerZonePerNotePitchbendRange (int perNotePitchbendRange)
{
return MidiRPNGenerator::generate (2, 0, perNotePitchbendRange, false, false);
}
MidiBuffer MPEMessages::setUpperZonePerNotePitchbendRange (int perNotePitchbendRange)
{
return MidiRPNGenerator::generate (15, 0, perNotePitchbendRange, false, false);
}
MidiBuffer MPEMessages::setLowerZoneMasterPitchbendRange (int masterPitchbendRange)
{
return MidiRPNGenerator::generate (1, 0, masterPitchbendRange, false, false);
}
MidiBuffer MPEMessages::setUpperZoneMasterPitchbendRange (int masterPitchbendRange)
{
return MidiRPNGenerator::generate (16, 0, masterPitchbendRange, false, false);
}
MidiBuffer MPEMessages::clearLowerZone()
{
return MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, 0, false, false);
}
MidiBuffer MPEMessages::clearUpperZone()
{
return MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, 0, false, false);
}
MidiBuffer MPEMessages::clearAllZones()
{
MidiBuffer buffer;
buffer.addEvents (clearLowerZone(), 0, -1, 0);
buffer.addEvents (clearUpperZone(), 0, -1, 0);
return buffer;
}
MidiBuffer MPEMessages::setZoneLayout (MPEZoneLayout layout)
{
MidiBuffer buffer;
buffer.addEvents (clearAllZones(), 0, -1, 0);
auto lowerZone = layout.getLowerZone();
if (lowerZone.isActive())
buffer.addEvents (setLowerZone (lowerZone.numMemberChannels,
lowerZone.perNotePitchbendRange,
lowerZone.masterPitchbendRange),
0, -1, 0);
auto upperZone = layout.getUpperZone();
if (upperZone.isActive())
buffer.addEvents (setUpperZone (upperZone.numMemberChannels,
upperZone.perNotePitchbendRange,
upperZone.masterPitchbendRange),
0, -1, 0);
return buffer;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPEMessagesTests : public UnitTest
{
public:
MPEMessagesTests()
: UnitTest ("MPEMessages class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("add zone");
{
{
MidiBuffer buffer = MPEMessages::setLowerZone (7);
const uint8 expectedBytes[] =
{
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set up zone
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x30, // per-note pbrange (default = 48)
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x02 // master pbrange (default = 2)
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
{
MidiBuffer buffer = MPEMessages::setUpperZone (5, 96, 0);
const uint8 expectedBytes[] =
{
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x05, // set up zone
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x60, // per-note pbrange (custom)
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // master pbrange (custom)
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
}
beginTest ("set per-note pitchbend range");
{
MidiBuffer buffer = MPEMessages::setLowerZonePerNotePitchbendRange (96);
const uint8 expectedBytes[] = { 0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60 };
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
beginTest ("set master pitchbend range");
{
MidiBuffer buffer = MPEMessages::setUpperZoneMasterPitchbendRange (60);
const uint8 expectedBytes[] = { 0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x3c };
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
beginTest ("clear all zones");
{
MidiBuffer buffer = MPEMessages::clearAllZones();
const uint8 expectedBytes[] = { 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // clear upper zone
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
beginTest ("set complete state");
{
MPEZoneLayout layout;
layout.setLowerZone (7, 96, 0);
layout.setUpperZone (7);
MidiBuffer buffer = MPEMessages::setZoneLayout (layout);
const uint8 expectedBytes[] = {
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00, // clear upper zone
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set lower zone
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60, // per-note pbrange (custom)
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // master pbrange (custom)
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x07, // set upper zone
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x30, // per-note pbrange (default = 48)
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x02 // master pbrange (default = 2)
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
}
private:
//==============================================================================
void testMidiBuffer (MidiBuffer& buffer, const uint8* expectedBytes, int expectedBytesSize)
{
uint8 actualBytes[128] = { 0 };
extractRawBinaryData (buffer, actualBytes, sizeof (actualBytes));
expectEquals (std::memcmp (actualBytes, expectedBytes, (std::size_t) expectedBytesSize), 0);
}
//==============================================================================
void extractRawBinaryData (const MidiBuffer& midiBuffer, const uint8* bufferToCopyTo, std::size_t maxBytes)
{
std::size_t pos = 0;
for (const auto metadata : midiBuffer)
{
const uint8* data = metadata.data;
std::size_t dataSize = (std::size_t) metadata.numBytes;
if (pos + dataSize > maxBytes)
return;
std::memcpy ((void*) (bufferToCopyTo + pos), data, dataSize);
pos += dataSize;
}
}
};
static MPEMessagesTests MPEMessagesUnitTests;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MidiBuffer MPEMessages::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
{
auto buffer = MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
buffer.addEvents (setLowerZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
buffer.addEvents (setLowerZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
return buffer;
}
MidiBuffer MPEMessages::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange)
{
auto buffer = MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, numMemberChannels, false, false);
buffer.addEvents (setUpperZonePerNotePitchbendRange (perNotePitchbendRange), 0, -1, 0);
buffer.addEvents (setUpperZoneMasterPitchbendRange (masterPitchbendRange), 0, -1, 0);
return buffer;
}
MidiBuffer MPEMessages::setLowerZonePerNotePitchbendRange (int perNotePitchbendRange)
{
return MidiRPNGenerator::generate (2, 0, perNotePitchbendRange, false, false);
}
MidiBuffer MPEMessages::setUpperZonePerNotePitchbendRange (int perNotePitchbendRange)
{
return MidiRPNGenerator::generate (15, 0, perNotePitchbendRange, false, false);
}
MidiBuffer MPEMessages::setLowerZoneMasterPitchbendRange (int masterPitchbendRange)
{
return MidiRPNGenerator::generate (1, 0, masterPitchbendRange, false, false);
}
MidiBuffer MPEMessages::setUpperZoneMasterPitchbendRange (int masterPitchbendRange)
{
return MidiRPNGenerator::generate (16, 0, masterPitchbendRange, false, false);
}
MidiBuffer MPEMessages::clearLowerZone()
{
return MidiRPNGenerator::generate (1, zoneLayoutMessagesRpnNumber, 0, false, false);
}
MidiBuffer MPEMessages::clearUpperZone()
{
return MidiRPNGenerator::generate (16, zoneLayoutMessagesRpnNumber, 0, false, false);
}
MidiBuffer MPEMessages::clearAllZones()
{
MidiBuffer buffer;
buffer.addEvents (clearLowerZone(), 0, -1, 0);
buffer.addEvents (clearUpperZone(), 0, -1, 0);
return buffer;
}
MidiBuffer MPEMessages::setZoneLayout (MPEZoneLayout layout)
{
MidiBuffer buffer;
buffer.addEvents (clearAllZones(), 0, -1, 0);
auto lowerZone = layout.getLowerZone();
if (lowerZone.isActive())
buffer.addEvents (setLowerZone (lowerZone.numMemberChannels,
lowerZone.perNotePitchbendRange,
lowerZone.masterPitchbendRange),
0, -1, 0);
auto upperZone = layout.getUpperZone();
if (upperZone.isActive())
buffer.addEvents (setUpperZone (upperZone.numMemberChannels,
upperZone.perNotePitchbendRange,
upperZone.masterPitchbendRange),
0, -1, 0);
return buffer;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPEMessagesTests : public UnitTest
{
public:
MPEMessagesTests()
: UnitTest ("MPEMessages class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("add zone");
{
{
MidiBuffer buffer = MPEMessages::setLowerZone (7);
const uint8 expectedBytes[] =
{
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set up zone
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x30, // per-note pbrange (default = 48)
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x02 // master pbrange (default = 2)
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
{
MidiBuffer buffer = MPEMessages::setUpperZone (5, 96, 0);
const uint8 expectedBytes[] =
{
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x05, // set up zone
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x60, // per-note pbrange (custom)
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // master pbrange (custom)
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
}
beginTest ("set per-note pitchbend range");
{
MidiBuffer buffer = MPEMessages::setLowerZonePerNotePitchbendRange (96);
const uint8 expectedBytes[] = { 0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60 };
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
beginTest ("set master pitchbend range");
{
MidiBuffer buffer = MPEMessages::setUpperZoneMasterPitchbendRange (60);
const uint8 expectedBytes[] = { 0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x3c };
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
beginTest ("clear all zones");
{
MidiBuffer buffer = MPEMessages::clearAllZones();
const uint8 expectedBytes[] = { 0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00 // clear upper zone
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
beginTest ("set complete state");
{
MPEZoneLayout layout;
layout.setLowerZone (7, 96, 0);
layout.setUpperZone (7);
MidiBuffer buffer = MPEMessages::setZoneLayout (layout);
const uint8 expectedBytes[] = {
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // clear lower zone
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x00, // clear upper zone
0xb0, 0x64, 0x06, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x07, // set lower zone
0xb1, 0x64, 0x00, 0xb1, 0x65, 0x00, 0xb1, 0x06, 0x60, // per-note pbrange (custom)
0xb0, 0x64, 0x00, 0xb0, 0x65, 0x00, 0xb0, 0x06, 0x00, // master pbrange (custom)
0xbf, 0x64, 0x06, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x07, // set upper zone
0xbe, 0x64, 0x00, 0xbe, 0x65, 0x00, 0xbe, 0x06, 0x30, // per-note pbrange (default = 48)
0xbf, 0x64, 0x00, 0xbf, 0x65, 0x00, 0xbf, 0x06, 0x02 // master pbrange (default = 2)
};
testMidiBuffer (buffer, expectedBytes, sizeof (expectedBytes));
}
}
private:
//==============================================================================
void testMidiBuffer (MidiBuffer& buffer, const uint8* expectedBytes, int expectedBytesSize)
{
uint8 actualBytes[128] = { 0 };
extractRawBinaryData (buffer, actualBytes, sizeof (actualBytes));
expectEquals (std::memcmp (actualBytes, expectedBytes, (std::size_t) expectedBytesSize), 0);
}
//==============================================================================
void extractRawBinaryData (const MidiBuffer& midiBuffer, const uint8* bufferToCopyTo, std::size_t maxBytes)
{
std::size_t pos = 0;
for (const auto metadata : midiBuffer)
{
const uint8* data = metadata.data;
std::size_t dataSize = (std::size_t) metadata.numBytes;
if (pos + dataSize > maxBytes)
return;
std::memcpy ((void*) (bufferToCopyTo + pos), data, dataSize);
pos += dataSize;
}
}
};
static MPEMessagesTests MPEMessagesUnitTests;
#endif
} // namespace juce

View File

@@ -1,116 +1,116 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This helper class contains the necessary helper functions to generate
MIDI messages that are exclusive to MPE, such as defining the upper and lower
MPE zones and setting per-note and master pitchbend ranges.
You can then send them to your MPE device using MidiOutput::sendBlockOfMessagesNow.
All other MPE messages like per-note pitchbend, pressure, and third
dimension, are ordinary MIDI messages that should be created using the MidiMessage
class instead. You just need to take care to send them to the appropriate
per-note MIDI channel.
Note: If you are working with an MPEZoneLayout object inside your app,
you should not use the message sequences provided here. Instead, you should
change the zone layout programmatically with the member functions provided in the
MPEZoneLayout class itself. You should also make sure that the Expressive
MIDI zone layout of your C++ code and of the MPE device are kept in sync.
@see MidiMessage, MPEZoneLayout
@tags{Audio}
*/
class JUCE_API MPEMessages
{
public:
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the lower MPE zone.
*/
static MidiBuffer setLowerZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the upper MPE zone.
*/
static MidiBuffer setUpperZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the per-note pitchbend range of the lower MPE zone.
*/
static MidiBuffer setLowerZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the per-note pitchbend range of the upper MPE zone.
*/
static MidiBuffer setUpperZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the master pitchbend range of the lower MPE zone.
*/
static MidiBuffer setLowerZoneMasterPitchbendRange (int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the master pitchbend range of the upper MPE zone.
*/
static MidiBuffer setUpperZoneMasterPitchbendRange (int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will clear the lower zone.
*/
static MidiBuffer clearLowerZone();
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will clear the upper zone.
*/
static MidiBuffer clearUpperZone();
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will clear the lower and upper zones.
*/
static MidiBuffer clearAllZones();
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will reset the whole MPE zone layout of the
device to the layout passed in. This will first clear the current lower and upper
zones, then then set the zones contained in the passed-in zone layout, and set their
per-note and master pitchbend ranges to their current values.
*/
static MidiBuffer setZoneLayout (MPEZoneLayout layout);
/** The RPN number used for MPE zone layout messages.
Pitchbend range messages (both per-note and master) are instead sent
on RPN 0 as in standard MIDI 1.0.
*/
static const int zoneLayoutMessagesRpnNumber = 6;
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This helper class contains the necessary helper functions to generate
MIDI messages that are exclusive to MPE, such as defining the upper and lower
MPE zones and setting per-note and master pitchbend ranges.
You can then send them to your MPE device using MidiOutput::sendBlockOfMessagesNow.
All other MPE messages like per-note pitchbend, pressure, and third
dimension, are ordinary MIDI messages that should be created using the MidiMessage
class instead. You just need to take care to send them to the appropriate
per-note MIDI channel.
Note: If you are working with an MPEZoneLayout object inside your app,
you should not use the message sequences provided here. Instead, you should
change the zone layout programmatically with the member functions provided in the
MPEZoneLayout class itself. You should also make sure that the Expressive
MIDI zone layout of your C++ code and of the MPE device are kept in sync.
@see MidiMessage, MPEZoneLayout
@tags{Audio}
*/
class JUCE_API MPEMessages
{
public:
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the lower MPE zone.
*/
static MidiBuffer setLowerZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the upper MPE zone.
*/
static MidiBuffer setUpperZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the per-note pitchbend range of the lower MPE zone.
*/
static MidiBuffer setLowerZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the per-note pitchbend range of the upper MPE zone.
*/
static MidiBuffer setUpperZonePerNotePitchbendRange (int perNotePitchbendRange = 48);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the master pitchbend range of the lower MPE zone.
*/
static MidiBuffer setLowerZoneMasterPitchbendRange (int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will set the master pitchbend range of the upper MPE zone.
*/
static MidiBuffer setUpperZoneMasterPitchbendRange (int masterPitchbendRange = 2);
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will clear the lower zone.
*/
static MidiBuffer clearLowerZone();
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will clear the upper zone.
*/
static MidiBuffer clearUpperZone();
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will clear the lower and upper zones.
*/
static MidiBuffer clearAllZones();
/** Returns the sequence of MIDI messages that, if sent to an Expressive
MIDI device, will reset the whole MPE zone layout of the
device to the layout passed in. This will first clear the current lower and upper
zones, then then set the zones contained in the passed-in zone layout, and set their
per-note and master pitchbend ranges to their current values.
*/
static MidiBuffer setZoneLayout (MPEZoneLayout layout);
/** The RPN number used for MPE zone layout messages.
Pitchbend range messages (both per-note and master) are instead sent
on RPN 0 as in standard MIDI 1.0.
*/
static const int zoneLayoutMessagesRpnNumber = 6;
};
} // namespace juce

View File

@@ -1,127 +1,127 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace
{
uint16 generateNoteID (int midiChannel, int midiNoteNumber) noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (midiNoteNumber >= 0 && midiNoteNumber < 128);
return uint16 ((midiChannel << 7) + midiNoteNumber);
}
}
//==============================================================================
MPENote::MPENote (int midiChannel_,
int initialNote_,
MPEValue noteOnVelocity_,
MPEValue pitchbend_,
MPEValue pressure_,
MPEValue timbre_,
KeyState keyState_) noexcept
: noteID (generateNoteID (midiChannel_, initialNote_)),
midiChannel (uint8 (midiChannel_)),
initialNote (uint8 (initialNote_)),
noteOnVelocity (noteOnVelocity_),
pitchbend (pitchbend_),
pressure (pressure_),
initialTimbre (timbre_),
timbre (timbre_),
keyState (keyState_)
{
jassert (keyState != MPENote::off);
jassert (isValid());
}
MPENote::MPENote() noexcept {}
//==============================================================================
bool MPENote::isValid() const noexcept
{
return midiChannel > 0 && midiChannel <= 16 && initialNote < 128;
}
//==============================================================================
double MPENote::getFrequencyInHertz (double frequencyOfA) const noexcept
{
auto pitchInSemitones = double (initialNote) + totalPitchbendInSemitones;
return frequencyOfA * std::pow (2.0, (pitchInSemitones - 69.0) / 12.0);
}
//==============================================================================
bool MPENote::operator== (const MPENote& other) const noexcept
{
jassert (isValid() && other.isValid());
return noteID == other.noteID;
}
bool MPENote::operator!= (const MPENote& other) const noexcept
{
jassert (isValid() && other.isValid());
return noteID != other.noteID;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPENoteTests : public UnitTest
{
public:
MPENoteTests()
: UnitTest ("MPENote class", UnitTestCategories::midi)
{}
//==============================================================================
void runTest() override
{
beginTest ("getFrequencyInHertz");
{
MPENote note;
note.initialNote = 60;
note.totalPitchbendInSemitones = -0.5;
expectEqualsWithinOneCent (note.getFrequencyInHertz(), 254.178);
}
}
private:
//==============================================================================
void expectEqualsWithinOneCent (double frequencyInHertzActual,
double frequencyInHertzExpected)
{
double ratio = frequencyInHertzActual / frequencyInHertzExpected;
double oneCent = 1.0005946;
expect (ratio < oneCent);
expect (ratio > 1.0 / oneCent);
}
};
static MPENoteTests MPENoteUnitTests;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
namespace
{
uint16 generateNoteID (int midiChannel, int midiNoteNumber) noexcept
{
jassert (midiChannel > 0 && midiChannel <= 16);
jassert (midiNoteNumber >= 0 && midiNoteNumber < 128);
return uint16 ((midiChannel << 7) + midiNoteNumber);
}
}
//==============================================================================
MPENote::MPENote (int midiChannel_,
int initialNote_,
MPEValue noteOnVelocity_,
MPEValue pitchbend_,
MPEValue pressure_,
MPEValue timbre_,
KeyState keyState_) noexcept
: noteID (generateNoteID (midiChannel_, initialNote_)),
midiChannel (uint8 (midiChannel_)),
initialNote (uint8 (initialNote_)),
noteOnVelocity (noteOnVelocity_),
pitchbend (pitchbend_),
pressure (pressure_),
initialTimbre (timbre_),
timbre (timbre_),
keyState (keyState_)
{
jassert (keyState != MPENote::off);
jassert (isValid());
}
MPENote::MPENote() noexcept {}
//==============================================================================
bool MPENote::isValid() const noexcept
{
return midiChannel > 0 && midiChannel <= 16 && initialNote < 128;
}
//==============================================================================
double MPENote::getFrequencyInHertz (double frequencyOfA) const noexcept
{
auto pitchInSemitones = double (initialNote) + totalPitchbendInSemitones;
return frequencyOfA * std::pow (2.0, (pitchInSemitones - 69.0) / 12.0);
}
//==============================================================================
bool MPENote::operator== (const MPENote& other) const noexcept
{
jassert (isValid() && other.isValid());
return noteID == other.noteID;
}
bool MPENote::operator!= (const MPENote& other) const noexcept
{
jassert (isValid() && other.isValid());
return noteID != other.noteID;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPENoteTests : public UnitTest
{
public:
MPENoteTests()
: UnitTest ("MPENote class", UnitTestCategories::midi)
{}
//==============================================================================
void runTest() override
{
beginTest ("getFrequencyInHertz");
{
MPENote note;
note.initialNote = 60;
note.totalPitchbendInSemitones = -0.5;
expectEqualsWithinOneCent (note.getFrequencyInHertz(), 254.178);
}
}
private:
//==============================================================================
void expectEqualsWithinOneCent (double frequencyInHertzActual,
double frequencyInHertzExpected)
{
double ratio = frequencyInHertzActual / frequencyInHertzExpected;
double oneCent = 1.0005946;
expect (ratio < oneCent);
expect (ratio > 1.0 / oneCent);
}
};
static MPENoteTests MPENoteUnitTests;
#endif
} // namespace juce

View File

@@ -1,184 +1,184 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This struct represents a playing MPE note.
A note is identified by a unique ID, or alternatively, by a MIDI channel
and an initial note. It is characterised by five dimensions of continuous
expressive control. Their current values are represented as
MPEValue objects.
@see MPEValue
@tags{Audio}
*/
struct JUCE_API MPENote
{
//==============================================================================
/** Possible values for the note key state. */
enum KeyState
{
off = 0, /**< The key is up (off). */
keyDown = 1, /**< The note key is currently down (pressed). */
sustained = 2, /**< The note is sustained (by a sustain or sostenuto pedal). */
keyDownAndSustained = 3 /**< The note key is down and sustained (by a sustain or sostenuto pedal). */
};
//==============================================================================
/** Constructor.
@param midiChannel The MIDI channel of the note, between 2 and 15.
(Channel 1 and channel 16 can never be note channels in MPE).
@param initialNote The MIDI note number, between 0 and 127.
@param velocity The note-on velocity of the note.
@param pitchbend The initial per-note pitchbend of the note.
@param pressure The initial pressure of the note.
@param timbre The timbre value of the note.
@param keyState The key state of the note (whether the key is down
and/or the note is sustained). This value must not
be MPENote::off, since you are triggering a new note.
(If not specified, the default value will be MPENote::keyDown.)
*/
MPENote (int midiChannel,
int initialNote,
MPEValue velocity,
MPEValue pitchbend,
MPEValue pressure,
MPEValue timbre,
KeyState keyState = MPENote::keyDown) noexcept;
/** Default constructor.
Constructs an invalid MPE note (a note with the key state MPENote::off
and an invalid MIDI channel. The only allowed use for such a note is to
call isValid() on it; everything else is undefined behaviour.
*/
MPENote() noexcept;
/** Checks whether the MPE note is valid. */
bool isValid() const noexcept;
//==============================================================================
// Invariants that define the note.
/** A unique ID. Useful to distinguish the note from other simultaneously
sounding notes that may use the same note number or MIDI channel.
This should never change during the lifetime of a note object.
*/
uint16 noteID = 0;
/** The MIDI channel which this note uses.
This should never change during the lifetime of an MPENote object.
*/
uint8 midiChannel = 0;
/** The MIDI note number that was sent when the note was triggered.
This should never change during the lifetime of an MPENote object.
*/
uint8 initialNote = 0;
//==============================================================================
// The five dimensions of continuous expressive control
/** The velocity ("strike") of the note-on.
This dimension will stay constant after the note has been turned on.
*/
MPEValue noteOnVelocity { MPEValue::minValue() };
/** Current per-note pitchbend of the note (in units of MIDI pitchwheel
position). This dimension can be modulated while the note sounds.
Note: This value is not aware of the currently used pitchbend range,
or an additional master pitchbend that may be simultaneously applied.
To compute the actual effective pitchbend of an MPENote, you should
probably use the member totalPitchbendInSemitones instead.
@see totalPitchbendInSemitones, getFrequencyInHertz
*/
MPEValue pitchbend { MPEValue::centreValue() };
/** Current pressure with which the note is held down.
This dimension can be modulated while the note sounds.
*/
MPEValue pressure { MPEValue::centreValue() };
/** Initial value of timbre when the note was triggered.
This should never change during the lifetime of an MPENote object.
*/
MPEValue initialTimbre { MPEValue::centreValue() };
/** Current value of the note's third expressive dimension, typically
encoding some kind of timbre parameter.
This dimension can be modulated while the note sounds.
*/
MPEValue timbre { MPEValue::centreValue() };
/** The release velocity ("lift") of the note after a note-off has been
received.
This dimension will only have a meaningful value after a note-off has
been received for the note (and keyState is set to MPENote::off or
MPENote::sustained). Initially, the value is undefined.
*/
MPEValue noteOffVelocity { MPEValue::minValue() };
//==============================================================================
/** Current effective pitchbend of the note in units of semitones, relative
to initialNote. You should use this to compute the actual effective pitch
of the note. This value is computed and set by an MPEInstrument to the
sum of the per-note pitchbend value (stored in MPEValue::pitchbend)
and the master pitchbend of the MPE zone, weighted with the per-note
pitchbend range and master pitchbend range of the zone, respectively.
@see getFrequencyInHertz
*/
double totalPitchbendInSemitones;
/** Current key state. Indicates whether the note key is currently down (pressed)
and/or the note is sustained (by a sustain or sostenuto pedal).
*/
KeyState keyState { MPENote::off };
//==============================================================================
/** Returns the current frequency of the note in Hertz. This is the sum of
the initialNote and the totalPitchbendInSemitones, converted to Hertz.
*/
double getFrequencyInHertz (double frequencyOfA = 440.0) const noexcept;
/** Returns true if two notes are the same, determined by their unique ID. */
bool operator== (const MPENote& other) const noexcept;
/** Returns true if two notes are different notes, determined by their unique ID. */
bool operator!= (const MPENote& other) const noexcept;
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This struct represents a playing MPE note.
A note is identified by a unique ID, or alternatively, by a MIDI channel
and an initial note. It is characterised by five dimensions of continuous
expressive control. Their current values are represented as
MPEValue objects.
@see MPEValue
@tags{Audio}
*/
struct JUCE_API MPENote
{
//==============================================================================
/** Possible values for the note key state. */
enum KeyState
{
off = 0, /**< The key is up (off). */
keyDown = 1, /**< The note key is currently down (pressed). */
sustained = 2, /**< The note is sustained (by a sustain or sostenuto pedal). */
keyDownAndSustained = 3 /**< The note key is down and sustained (by a sustain or sostenuto pedal). */
};
//==============================================================================
/** Constructor.
@param midiChannel The MIDI channel of the note, between 2 and 15.
(Channel 1 and channel 16 can never be note channels in MPE).
@param initialNote The MIDI note number, between 0 and 127.
@param velocity The note-on velocity of the note.
@param pitchbend The initial per-note pitchbend of the note.
@param pressure The initial pressure of the note.
@param timbre The timbre value of the note.
@param keyState The key state of the note (whether the key is down
and/or the note is sustained). This value must not
be MPENote::off, since you are triggering a new note.
(If not specified, the default value will be MPENote::keyDown.)
*/
MPENote (int midiChannel,
int initialNote,
MPEValue velocity,
MPEValue pitchbend,
MPEValue pressure,
MPEValue timbre,
KeyState keyState = MPENote::keyDown) noexcept;
/** Default constructor.
Constructs an invalid MPE note (a note with the key state MPENote::off
and an invalid MIDI channel. The only allowed use for such a note is to
call isValid() on it; everything else is undefined behaviour.
*/
MPENote() noexcept;
/** Checks whether the MPE note is valid. */
bool isValid() const noexcept;
//==============================================================================
// Invariants that define the note.
/** A unique ID. Useful to distinguish the note from other simultaneously
sounding notes that may use the same note number or MIDI channel.
This should never change during the lifetime of a note object.
*/
uint16 noteID = 0;
/** The MIDI channel which this note uses.
This should never change during the lifetime of an MPENote object.
*/
uint8 midiChannel = 0;
/** The MIDI note number that was sent when the note was triggered.
This should never change during the lifetime of an MPENote object.
*/
uint8 initialNote = 0;
//==============================================================================
// The five dimensions of continuous expressive control
/** The velocity ("strike") of the note-on.
This dimension will stay constant after the note has been turned on.
*/
MPEValue noteOnVelocity { MPEValue::minValue() };
/** Current per-note pitchbend of the note (in units of MIDI pitchwheel
position). This dimension can be modulated while the note sounds.
Note: This value is not aware of the currently used pitchbend range,
or an additional master pitchbend that may be simultaneously applied.
To compute the actual effective pitchbend of an MPENote, you should
probably use the member totalPitchbendInSemitones instead.
@see totalPitchbendInSemitones, getFrequencyInHertz
*/
MPEValue pitchbend { MPEValue::centreValue() };
/** Current pressure with which the note is held down.
This dimension can be modulated while the note sounds.
*/
MPEValue pressure { MPEValue::centreValue() };
/** Initial value of timbre when the note was triggered.
This should never change during the lifetime of an MPENote object.
*/
MPEValue initialTimbre { MPEValue::centreValue() };
/** Current value of the note's third expressive dimension, typically
encoding some kind of timbre parameter.
This dimension can be modulated while the note sounds.
*/
MPEValue timbre { MPEValue::centreValue() };
/** The release velocity ("lift") of the note after a note-off has been
received.
This dimension will only have a meaningful value after a note-off has
been received for the note (and keyState is set to MPENote::off or
MPENote::sustained). Initially, the value is undefined.
*/
MPEValue noteOffVelocity { MPEValue::minValue() };
//==============================================================================
/** Current effective pitchbend of the note in units of semitones, relative
to initialNote. You should use this to compute the actual effective pitch
of the note. This value is computed and set by an MPEInstrument to the
sum of the per-note pitchbend value (stored in MPEValue::pitchbend)
and the master pitchbend of the MPE zone, weighted with the per-note
pitchbend range and master pitchbend range of the zone, respectively.
@see getFrequencyInHertz
*/
double totalPitchbendInSemitones;
/** Current key state. Indicates whether the note key is currently down (pressed)
and/or the note is sustained (by a sustain or sostenuto pedal).
*/
KeyState keyState { MPENote::off };
//==============================================================================
/** Returns the current frequency of the note in Hertz. This is the sum of
the initialNote and the totalPitchbendInSemitones, converted to Hertz.
*/
double getFrequencyInHertz (double frequencyOfA = 440.0) const noexcept;
/** Returns true if two notes are the same, determined by their unique ID. */
bool operator== (const MPENote& other) const noexcept;
/** Returns true if two notes are different notes, determined by their unique ID. */
bool operator!= (const MPENote& other) const noexcept;
};
} // namespace juce

View File

@@ -1,343 +1,341 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPESynthesiser::MPESynthesiser()
{
MPEZoneLayout zoneLayout;
zoneLayout.setLowerZone (15);
setZoneLayout (zoneLayout);
}
MPESynthesiser::MPESynthesiser (MPEInstrument* mpeInstrument) : MPESynthesiserBase (mpeInstrument)
{
}
MPESynthesiser::~MPESynthesiser()
{
}
//==============================================================================
void MPESynthesiser::startVoice (MPESynthesiserVoice* voice, MPENote noteToStart)
{
jassert (voice != nullptr);
voice->currentlyPlayingNote = noteToStart;
voice->noteOnTime = lastNoteOnCounter++;
voice->noteStarted();
}
void MPESynthesiser::stopVoice (MPESynthesiserVoice* voice, MPENote noteToStop, bool allowTailOff)
{
jassert (voice != nullptr);
voice->currentlyPlayingNote = noteToStop;
voice->noteStopped (allowTailOff);
}
//==============================================================================
void MPESynthesiser::noteAdded (MPENote newNote)
{
const ScopedLock sl (voicesLock);
if (auto* voice = findFreeVoice (newNote, shouldStealVoices))
startVoice (voice, newNote);
}
void MPESynthesiser::notePressureChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->notePressureChanged();
}
}
}
void MPESynthesiser::notePitchbendChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->notePitchbendChanged();
}
}
}
void MPESynthesiser::noteTimbreChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->noteTimbreChanged();
}
}
}
void MPESynthesiser::noteKeyStateChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->noteKeyStateChanged();
}
}
}
void MPESynthesiser::noteReleased (MPENote finishedNote)
{
const ScopedLock sl (voicesLock);
for (auto i = voices.size(); --i >= 0;)
{
auto* voice = voices.getUnchecked (i);
if (voice->isCurrentlyPlayingNote (finishedNote))
stopVoice (voice, finishedNote, true);
}
}
void MPESynthesiser::setCurrentPlaybackSampleRate (const double newRate)
{
MPESynthesiserBase::setCurrentPlaybackSampleRate (newRate);
const ScopedLock sl (voicesLock);
turnOffAllVoices (false);
for (auto i = voices.size(); --i >= 0;)
voices.getUnchecked (i)->setCurrentSampleRate (newRate);
}
void MPESynthesiser::handleMidiEvent (const MidiMessage& m)
{
if (m.isController())
handleController (m.getChannel(), m.getControllerNumber(), m.getControllerValue());
else if (m.isProgramChange())
handleProgramChange (m.getChannel(), m.getProgramChangeNumber());
MPESynthesiserBase::handleMidiEvent (m);
}
MPESynthesiserVoice* MPESynthesiser::findFreeVoice (MPENote noteToFindVoiceFor, bool stealIfNoneAvailable) const
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (! voice->isActive())
return voice;
}
if (stealIfNoneAvailable)
return findVoiceToSteal (noteToFindVoiceFor);
return nullptr;
}
MPESynthesiserVoice* MPESynthesiser::findVoiceToSteal (MPENote noteToStealVoiceFor) const
{
// This voice-stealing algorithm applies the following heuristics:
// - Re-use the oldest notes first
// - Protect the lowest & topmost notes, even if sustained, but not if they've been released.
// apparently you are trying to render audio without having any voices...
jassert (voices.size() > 0);
// These are the voices we want to protect (ie: only steal if unavoidable)
MPESynthesiserVoice* low = nullptr; // Lowest sounding note, might be sustained, but NOT in release phase
MPESynthesiserVoice* top = nullptr; // Highest sounding note, might be sustained, but NOT in release phase
// this is a list of voices we can steal, sorted by how long they've been running
Array<MPESynthesiserVoice*> usableVoices;
usableVoices.ensureStorageAllocated (voices.size());
for (auto* voice : voices)
{
jassert (voice->isActive()); // We wouldn't be here otherwise
usableVoices.add (voice);
// NB: Using a functor rather than a lambda here due to scare-stories about
// compilers generating code containing heap allocations..
struct Sorter
{
bool operator() (const MPESynthesiserVoice* a, const MPESynthesiserVoice* b) const noexcept { return a->noteOnTime < b->noteOnTime; }
};
std::sort (usableVoices.begin(), usableVoices.end(), Sorter());
if (! voice->isPlayingButReleased()) // Don't protect released notes
{
auto noteNumber = voice->getCurrentlyPlayingNote().initialNote;
if (low == nullptr || noteNumber < low->getCurrentlyPlayingNote().initialNote)
low = voice;
if (top == nullptr || noteNumber > top->getCurrentlyPlayingNote().initialNote)
top = voice;
}
}
// Eliminate pathological cases (ie: only 1 note playing): we always give precedence to the lowest note(s)
if (top == low)
top = nullptr;
// If we want to re-use the voice to trigger a new note,
// then The oldest note that's playing the same note number is ideal.
if (noteToStealVoiceFor.isValid())
for (auto* voice : usableVoices)
if (voice->getCurrentlyPlayingNote().initialNote == noteToStealVoiceFor.initialNote)
return voice;
// Oldest voice that has been released (no finger on it and not held by sustain pedal)
for (auto* voice : usableVoices)
if (voice != low && voice != top && voice->isPlayingButReleased())
return voice;
// Oldest voice that doesn't have a finger on it:
for (auto* voice : usableVoices)
if (voice != low && voice != top
&& voice->getCurrentlyPlayingNote().keyState != MPENote::keyDown
&& voice->getCurrentlyPlayingNote().keyState != MPENote::keyDownAndSustained)
return voice;
// Oldest voice that isn't protected
for (auto* voice : usableVoices)
if (voice != low && voice != top)
return voice;
// We've only got "protected" voices now: lowest note takes priority
jassert (low != nullptr);
// Duophonic synth: give priority to the bass note:
if (top != nullptr)
return top;
return low;
}
//==============================================================================
void MPESynthesiser::addVoice (MPESynthesiserVoice* const newVoice)
{
const ScopedLock sl (voicesLock);
newVoice->setCurrentSampleRate (getSampleRate());
voices.add (newVoice);
}
void MPESynthesiser::clearVoices()
{
const ScopedLock sl (voicesLock);
voices.clear();
}
MPESynthesiserVoice* MPESynthesiser::getVoice (const int index) const
{
const ScopedLock sl (voicesLock);
return voices [index];
}
void MPESynthesiser::removeVoice (const int index)
{
const ScopedLock sl (voicesLock);
voices.remove (index);
}
void MPESynthesiser::reduceNumVoices (const int newNumVoices)
{
// we can't possibly get to a negative number of voices...
jassert (newNumVoices >= 0);
const ScopedLock sl (voicesLock);
while (voices.size() > newNumVoices)
{
if (auto* voice = findFreeVoice ({}, true))
voices.removeObject (voice);
else
voices.remove (0); // if there's no voice to steal, kill the oldest voice
}
}
void MPESynthesiser::turnOffAllVoices (bool allowTailOff)
{
{
const ScopedLock sl (voicesLock);
// first turn off all voices (it's more efficient to do this immediately
// rather than to go through the MPEInstrument for this).
for (auto* voice : voices)
{
voice->currentlyPlayingNote.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number
voice->currentlyPlayingNote.keyState = MPENote::off;
voice->noteStopped (allowTailOff);
}
}
// finally make sure the MPE Instrument also doesn't have any notes anymore.
instrument->releaseAllNotes();
}
//==============================================================================
void MPESynthesiser::renderNextSubBlock (AudioBuffer<float>& buffer, int startSample, int numSamples)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isActive())
voice->renderNextBlock (buffer, startSample, numSamples);
}
}
void MPESynthesiser::renderNextSubBlock (AudioBuffer<double>& buffer, int startSample, int numSamples)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isActive())
voice->renderNextBlock (buffer, startSample, numSamples);
}
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPESynthesiser::MPESynthesiser()
{
}
MPESynthesiser::MPESynthesiser (MPEInstrument& mpeInstrument)
: MPESynthesiserBase (mpeInstrument)
{
}
MPESynthesiser::~MPESynthesiser()
{
}
//==============================================================================
void MPESynthesiser::startVoice (MPESynthesiserVoice* voice, MPENote noteToStart)
{
jassert (voice != nullptr);
voice->currentlyPlayingNote = noteToStart;
voice->noteOnTime = lastNoteOnCounter++;
voice->noteStarted();
}
void MPESynthesiser::stopVoice (MPESynthesiserVoice* voice, MPENote noteToStop, bool allowTailOff)
{
jassert (voice != nullptr);
voice->currentlyPlayingNote = noteToStop;
voice->noteStopped (allowTailOff);
}
//==============================================================================
void MPESynthesiser::noteAdded (MPENote newNote)
{
const ScopedLock sl (voicesLock);
if (auto* voice = findFreeVoice (newNote, shouldStealVoices))
startVoice (voice, newNote);
}
void MPESynthesiser::notePressureChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->notePressureChanged();
}
}
}
void MPESynthesiser::notePitchbendChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->notePitchbendChanged();
}
}
}
void MPESynthesiser::noteTimbreChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->noteTimbreChanged();
}
}
}
void MPESynthesiser::noteKeyStateChanged (MPENote changedNote)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isCurrentlyPlayingNote (changedNote))
{
voice->currentlyPlayingNote = changedNote;
voice->noteKeyStateChanged();
}
}
}
void MPESynthesiser::noteReleased (MPENote finishedNote)
{
const ScopedLock sl (voicesLock);
for (auto i = voices.size(); --i >= 0;)
{
auto* voice = voices.getUnchecked (i);
if (voice->isCurrentlyPlayingNote (finishedNote))
stopVoice (voice, finishedNote, true);
}
}
void MPESynthesiser::setCurrentPlaybackSampleRate (const double newRate)
{
MPESynthesiserBase::setCurrentPlaybackSampleRate (newRate);
const ScopedLock sl (voicesLock);
turnOffAllVoices (false);
for (auto i = voices.size(); --i >= 0;)
voices.getUnchecked (i)->setCurrentSampleRate (newRate);
}
void MPESynthesiser::handleMidiEvent (const MidiMessage& m)
{
if (m.isController())
handleController (m.getChannel(), m.getControllerNumber(), m.getControllerValue());
else if (m.isProgramChange())
handleProgramChange (m.getChannel(), m.getProgramChangeNumber());
MPESynthesiserBase::handleMidiEvent (m);
}
MPESynthesiserVoice* MPESynthesiser::findFreeVoice (MPENote noteToFindVoiceFor, bool stealIfNoneAvailable) const
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (! voice->isActive())
return voice;
}
if (stealIfNoneAvailable)
return findVoiceToSteal (noteToFindVoiceFor);
return nullptr;
}
MPESynthesiserVoice* MPESynthesiser::findVoiceToSteal (MPENote noteToStealVoiceFor) const
{
// This voice-stealing algorithm applies the following heuristics:
// - Re-use the oldest notes first
// - Protect the lowest & topmost notes, even if sustained, but not if they've been released.
// apparently you are trying to render audio without having any voices...
jassert (voices.size() > 0);
// These are the voices we want to protect (ie: only steal if unavoidable)
MPESynthesiserVoice* low = nullptr; // Lowest sounding note, might be sustained, but NOT in release phase
MPESynthesiserVoice* top = nullptr; // Highest sounding note, might be sustained, but NOT in release phase
// this is a list of voices we can steal, sorted by how long they've been running
Array<MPESynthesiserVoice*> usableVoices;
usableVoices.ensureStorageAllocated (voices.size());
for (auto* voice : voices)
{
jassert (voice->isActive()); // We wouldn't be here otherwise
usableVoices.add (voice);
// NB: Using a functor rather than a lambda here due to scare-stories about
// compilers generating code containing heap allocations..
struct Sorter
{
bool operator() (const MPESynthesiserVoice* a, const MPESynthesiserVoice* b) const noexcept { return a->noteOnTime < b->noteOnTime; }
};
std::sort (usableVoices.begin(), usableVoices.end(), Sorter());
if (! voice->isPlayingButReleased()) // Don't protect released notes
{
auto noteNumber = voice->getCurrentlyPlayingNote().initialNote;
if (low == nullptr || noteNumber < low->getCurrentlyPlayingNote().initialNote)
low = voice;
if (top == nullptr || noteNumber > top->getCurrentlyPlayingNote().initialNote)
top = voice;
}
}
// Eliminate pathological cases (ie: only 1 note playing): we always give precedence to the lowest note(s)
if (top == low)
top = nullptr;
// If we want to re-use the voice to trigger a new note,
// then The oldest note that's playing the same note number is ideal.
if (noteToStealVoiceFor.isValid())
for (auto* voice : usableVoices)
if (voice->getCurrentlyPlayingNote().initialNote == noteToStealVoiceFor.initialNote)
return voice;
// Oldest voice that has been released (no finger on it and not held by sustain pedal)
for (auto* voice : usableVoices)
if (voice != low && voice != top && voice->isPlayingButReleased())
return voice;
// Oldest voice that doesn't have a finger on it:
for (auto* voice : usableVoices)
if (voice != low && voice != top
&& voice->getCurrentlyPlayingNote().keyState != MPENote::keyDown
&& voice->getCurrentlyPlayingNote().keyState != MPENote::keyDownAndSustained)
return voice;
// Oldest voice that isn't protected
for (auto* voice : usableVoices)
if (voice != low && voice != top)
return voice;
// We've only got "protected" voices now: lowest note takes priority
jassert (low != nullptr);
// Duophonic synth: give priority to the bass note:
if (top != nullptr)
return top;
return low;
}
//==============================================================================
void MPESynthesiser::addVoice (MPESynthesiserVoice* const newVoice)
{
const ScopedLock sl (voicesLock);
newVoice->setCurrentSampleRate (getSampleRate());
voices.add (newVoice);
}
void MPESynthesiser::clearVoices()
{
const ScopedLock sl (voicesLock);
voices.clear();
}
MPESynthesiserVoice* MPESynthesiser::getVoice (const int index) const
{
const ScopedLock sl (voicesLock);
return voices [index];
}
void MPESynthesiser::removeVoice (const int index)
{
const ScopedLock sl (voicesLock);
voices.remove (index);
}
void MPESynthesiser::reduceNumVoices (const int newNumVoices)
{
// we can't possibly get to a negative number of voices...
jassert (newNumVoices >= 0);
const ScopedLock sl (voicesLock);
while (voices.size() > newNumVoices)
{
if (auto* voice = findFreeVoice ({}, true))
voices.removeObject (voice);
else
voices.remove (0); // if there's no voice to steal, kill the oldest voice
}
}
void MPESynthesiser::turnOffAllVoices (bool allowTailOff)
{
{
const ScopedLock sl (voicesLock);
// first turn off all voices (it's more efficient to do this immediately
// rather than to go through the MPEInstrument for this).
for (auto* voice : voices)
{
voice->currentlyPlayingNote.noteOffVelocity = MPEValue::from7BitInt (64); // some reasonable number
voice->currentlyPlayingNote.keyState = MPENote::off;
voice->noteStopped (allowTailOff);
}
}
// finally make sure the MPE Instrument also doesn't have any notes anymore.
instrument.releaseAllNotes();
}
//==============================================================================
void MPESynthesiser::renderNextSubBlock (AudioBuffer<float>& buffer, int startSample, int numSamples)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isActive())
voice->renderNextBlock (buffer, startSample, numSamples);
}
}
void MPESynthesiser::renderNextSubBlock (AudioBuffer<double>& buffer, int startSample, int numSamples)
{
const ScopedLock sl (voicesLock);
for (auto* voice : voices)
{
if (voice->isActive())
voice->renderNextBlock (buffer, startSample, numSamples);
}
}
} // namespace juce

View File

@@ -1,312 +1,311 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Base class for an MPE-compatible musical device that can play sounds.
This class extends MPESynthesiserBase by adding the concept of voices,
each of which can play a sound triggered by a MPENote that can be modulated
by MPE dimensions like pressure, pitchbend, and timbre, while the note is
sounding.
To create a synthesiser, you'll need to create a subclass of MPESynthesiserVoice
which can play back one of these sounds at a time.
Then you can use the addVoice() methods to give the synthesiser a set of voices
it can use to play notes. If you only give it one voice it will be monophonic -
the more voices it has, the more polyphony it'll have available.
Then repeatedly call the renderNextBlock() method to produce the audio (inherited
from MPESynthesiserBase). The voices will be started, stopped, and modulated
automatically, based on the MPE/MIDI messages that the synthesiser receives.
Before rendering, be sure to call the setCurrentPlaybackSampleRate() to tell it
what the target playback rate is. This value is passed on to the voices so that
they can pitch their output correctly.
@see MPESynthesiserBase, MPESynthesiserVoice, MPENote, MPEInstrument
@tags{Audio}
*/
class JUCE_API MPESynthesiser : public MPESynthesiserBase
{
public:
//==============================================================================
/** Constructor.
You'll need to add some voices before it'll make any sound.
@see addVoice
*/
MPESynthesiser();
/** Constructor to pass to the synthesiser a custom MPEInstrument object
to handle the MPE note state, MIDI channel assignment etc.
(in case you need custom logic for this that goes beyond MIDI and MPE).
The synthesiser will take ownership of this object.
@see MPESynthesiserBase, MPEInstrument
*/
MPESynthesiser (MPEInstrument* instrumentToUse);
/** Destructor. */
~MPESynthesiser() override;
//==============================================================================
/** Deletes all voices. */
void clearVoices();
/** Returns the number of voices that have been added. */
int getNumVoices() const noexcept { return voices.size(); }
/** Returns one of the voices that have been added. */
MPESynthesiserVoice* getVoice (int index) const;
/** Adds a new voice to the synth.
All the voices should be the same class of object and are treated equally.
The object passed in will be managed by the synthesiser, which will delete
it later on when no longer needed. The caller should not retain a pointer to the
voice.
*/
void addVoice (MPESynthesiserVoice* newVoice);
/** Deletes one of the voices. */
void removeVoice (int index);
/** Reduces the number of voices to newNumVoices.
This will repeatedly call findVoiceToSteal() and remove that voice, until
the total number of voices equals newNumVoices. If newNumVoices is greater than
or equal to the current number of voices, this method does nothing.
*/
void reduceNumVoices (int newNumVoices);
/** Release all MPE notes and turn off all voices.
If allowTailOff is true, the voices will be allowed to fade out the notes gracefully
(if they can do). If this is false, the notes will all be cut off immediately.
This method is meant to be called by the user, for example to implement
a MIDI panic button in a synth.
*/
virtual void turnOffAllVoices (bool allowTailOff);
//==============================================================================
/** If set to true, then the synth will try to take over an existing voice if
it runs out and needs to play another note.
The value of this boolean is passed into findFreeVoice(), so the result will
depend on the implementation of this method.
*/
void setVoiceStealingEnabled (bool shouldSteal) noexcept { shouldStealVoices = shouldSteal; }
/** Returns true if note-stealing is enabled. */
bool isVoiceStealingEnabled() const noexcept { return shouldStealVoices; }
//==============================================================================
/** Tells the synthesiser what the sample rate is for the audio it's being used to render.
This overrides the implementation in MPESynthesiserBase, to additionally
propagate the new value to the voices so that they can use it to render the correct
pitches.
*/
void setCurrentPlaybackSampleRate (double newRate) override;
//==============================================================================
/** Handle incoming MIDI events.
This method will be called automatically according to the MIDI data passed
into renderNextBlock(), but you can also call it yourself to manually
inject MIDI events.
This implementation forwards program change messages and non-MPE-related
controller messages to handleProgramChange and handleController, respectively,
and then simply calls through to MPESynthesiserBase::handleMidiEvent to deal
with MPE-related MIDI messages used for MPE notes, zones etc.
This method can be overridden further if you need to do custom MIDI
handling on top of what is provided here.
*/
void handleMidiEvent (const MidiMessage&) override;
/** Callback for MIDI controller messages. The default implementation
provided here does nothing; override this method if you need custom
MIDI controller handling on top of MPE.
This method will be called automatically according to the midi data passed into
renderNextBlock().
*/
virtual void handleController (int /*midiChannel*/,
int /*controllerNumber*/,
int /*controllerValue*/) {}
/** Callback for MIDI program change messages. The default implementation
provided here does nothing; override this method if you need to handle
those messages.
This method will be called automatically according to the midi data passed into
renderNextBlock().
*/
virtual void handleProgramChange (int /*midiChannel*/,
int /*programNumber*/) {}
protected:
//==============================================================================
/** Attempts to start playing a new note.
The default method here will find a free voice that is appropriate for
playing the given MPENote, and use that voice to start playing the sound.
If isNoteStealingEnabled returns true (set this by calling setNoteStealingEnabled),
the synthesiser will use the voice stealing algorithm to find a free voice for
the note (if no voices are free otherwise).
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself, otherwise the internal MPE note state
will become inconsistent.
*/
void noteAdded (MPENote newNote) override;
/** Stops playing a note.
This will be called whenever an MPE note is released (either by a note-off message,
or by a sustain/sostenuto pedal release for a note that already received a note-off),
and should therefore stop playing.
This will find any voice that is currently playing finishedNote,
turn its currently playing note off, and call its noteStopped callback.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself, otherwise the internal MPE note state
will become inconsistent.
*/
void noteReleased (MPENote finishedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its notePressureChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void notePressureChanged (MPENote changedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its notePitchbendChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void notePitchbendChanged (MPENote changedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its noteTimbreChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void noteTimbreChanged (MPENote changedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its noteKeyStateChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void noteKeyStateChanged (MPENote changedNote) override;
//==============================================================================
/** This will simply call renderNextBlock for each currently active
voice and fill the buffer with the sum.
Override this method if you need to do more work to render your audio.
*/
void renderNextSubBlock (AudioBuffer<float>& outputAudio,
int startSample,
int numSamples) override;
/** This will simply call renderNextBlock for each currently active
voice and fill the buffer with the sum. (double-precision version)
Override this method if you need to do more work to render your audio.
*/
void renderNextSubBlock (AudioBuffer<double>& outputAudio,
int startSample,
int numSamples) override;
//==============================================================================
/** Searches through the voices to find one that's not currently playing, and
which can play the given MPE note.
If all voices are active and stealIfNoneAvailable is false, this returns
a nullptr. If all voices are active and stealIfNoneAvailable is true,
this will call findVoiceToSteal() to find a voice.
If you need to find a free voice for something else than playing a note
(e.g. for deleting it), you can pass an invalid (default-constructed) MPENote.
*/
virtual MPESynthesiserVoice* findFreeVoice (MPENote noteToFindVoiceFor,
bool stealIfNoneAvailable) const;
/** Chooses a voice that is most suitable for being re-used to play a new
note, or for being deleted by reduceNumVoices.
The default method will attempt to find the oldest voice that isn't the
bottom or top note being played. If that's not suitable for your synth,
you can override this method and do something more cunning instead.
If you pass a valid MPENote for the optional argument, then the note number
of that note will be taken into account for finding the ideal voice to steal.
If you pass an invalid (default-constructed) MPENote instead, this part of
the algorithm will be ignored.
*/
virtual MPESynthesiserVoice* findVoiceToSteal (MPENote noteToStealVoiceFor = MPENote()) const;
/** Starts a specified voice and tells it to play a particular MPENote.
You should never need to call this, it's called internally by
MPESynthesiserBase::instrument via the noteStarted callback,
but is protected in case it's useful for some custom subclasses.
*/
void startVoice (MPESynthesiserVoice* voice, MPENote noteToStart);
/** Stops a given voice and tells it to stop playing a particular MPENote
(which should be the same note it is actually playing).
You should never need to call this, it's called internally by
MPESynthesiserBase::instrument via the noteReleased callback,
but is protected in case it's useful for some custom subclasses.
*/
void stopVoice (MPESynthesiserVoice* voice, MPENote noteToStop, bool allowTailOff);
//==============================================================================
OwnedArray<MPESynthesiserVoice> voices;
CriticalSection voicesLock;
protected:
//==============================================================================
bool shouldStealVoices = false;
uint32 lastNoteOnCounter = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiser)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Base class for an MPE-compatible musical device that can play sounds.
This class extends MPESynthesiserBase by adding the concept of voices,
each of which can play a sound triggered by a MPENote that can be modulated
by MPE dimensions like pressure, pitchbend, and timbre, while the note is
sounding.
To create a synthesiser, you'll need to create a subclass of MPESynthesiserVoice
which can play back one of these sounds at a time.
Then you can use the addVoice() methods to give the synthesiser a set of voices
it can use to play notes. If you only give it one voice it will be monophonic -
the more voices it has, the more polyphony it'll have available.
Then repeatedly call the renderNextBlock() method to produce the audio (inherited
from MPESynthesiserBase). The voices will be started, stopped, and modulated
automatically, based on the MPE/MIDI messages that the synthesiser receives.
Before rendering, be sure to call the setCurrentPlaybackSampleRate() to tell it
what the target playback rate is. This value is passed on to the voices so that
they can pitch their output correctly.
@see MPESynthesiserBase, MPESynthesiserVoice, MPENote, MPEInstrument
@tags{Audio}
*/
class JUCE_API MPESynthesiser : public MPESynthesiserBase
{
public:
//==============================================================================
/** Constructor.
You'll need to add some voices before it'll make any sound.
@see addVoice
*/
MPESynthesiser();
/** Constructor to pass to the synthesiser a custom MPEInstrument object
to handle the MPE note state, MIDI channel assignment etc.
(in case you need custom logic for this that goes beyond MIDI and MPE).
@see MPESynthesiserBase, MPEInstrument
*/
MPESynthesiser (MPEInstrument& instrumentToUse);
/** Destructor. */
~MPESynthesiser() override;
//==============================================================================
/** Deletes all voices. */
void clearVoices();
/** Returns the number of voices that have been added. */
int getNumVoices() const noexcept { return voices.size(); }
/** Returns one of the voices that have been added. */
MPESynthesiserVoice* getVoice (int index) const;
/** Adds a new voice to the synth.
All the voices should be the same class of object and are treated equally.
The object passed in will be managed by the synthesiser, which will delete
it later on when no longer needed. The caller should not retain a pointer to the
voice.
*/
void addVoice (MPESynthesiserVoice* newVoice);
/** Deletes one of the voices. */
void removeVoice (int index);
/** Reduces the number of voices to newNumVoices.
This will repeatedly call findVoiceToSteal() and remove that voice, until
the total number of voices equals newNumVoices. If newNumVoices is greater than
or equal to the current number of voices, this method does nothing.
*/
void reduceNumVoices (int newNumVoices);
/** Release all MPE notes and turn off all voices.
If allowTailOff is true, the voices will be allowed to fade out the notes gracefully
(if they can do). If this is false, the notes will all be cut off immediately.
This method is meant to be called by the user, for example to implement
a MIDI panic button in a synth.
*/
virtual void turnOffAllVoices (bool allowTailOff);
//==============================================================================
/** If set to true, then the synth will try to take over an existing voice if
it runs out and needs to play another note.
The value of this boolean is passed into findFreeVoice(), so the result will
depend on the implementation of this method.
*/
void setVoiceStealingEnabled (bool shouldSteal) noexcept { shouldStealVoices = shouldSteal; }
/** Returns true if note-stealing is enabled. */
bool isVoiceStealingEnabled() const noexcept { return shouldStealVoices; }
//==============================================================================
/** Tells the synthesiser what the sample rate is for the audio it's being used to render.
This overrides the implementation in MPESynthesiserBase, to additionally
propagate the new value to the voices so that they can use it to render the correct
pitches.
*/
void setCurrentPlaybackSampleRate (double newRate) override;
//==============================================================================
/** Handle incoming MIDI events.
This method will be called automatically according to the MIDI data passed
into renderNextBlock(), but you can also call it yourself to manually
inject MIDI events.
This implementation forwards program change messages and non-MPE-related
controller messages to handleProgramChange and handleController, respectively,
and then simply calls through to MPESynthesiserBase::handleMidiEvent to deal
with MPE-related MIDI messages used for MPE notes, zones etc.
This method can be overridden further if you need to do custom MIDI
handling on top of what is provided here.
*/
void handleMidiEvent (const MidiMessage&) override;
/** Callback for MIDI controller messages. The default implementation
provided here does nothing; override this method if you need custom
MIDI controller handling on top of MPE.
This method will be called automatically according to the midi data passed into
renderNextBlock().
*/
virtual void handleController (int /*midiChannel*/,
int /*controllerNumber*/,
int /*controllerValue*/) {}
/** Callback for MIDI program change messages. The default implementation
provided here does nothing; override this method if you need to handle
those messages.
This method will be called automatically according to the midi data passed into
renderNextBlock().
*/
virtual void handleProgramChange (int /*midiChannel*/,
int /*programNumber*/) {}
protected:
//==============================================================================
/** Attempts to start playing a new note.
The default method here will find a free voice that is appropriate for
playing the given MPENote, and use that voice to start playing the sound.
If isNoteStealingEnabled returns true (set this by calling setNoteStealingEnabled),
the synthesiser will use the voice stealing algorithm to find a free voice for
the note (if no voices are free otherwise).
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself, otherwise the internal MPE note state
will become inconsistent.
*/
void noteAdded (MPENote newNote) override;
/** Stops playing a note.
This will be called whenever an MPE note is released (either by a note-off message,
or by a sustain/sostenuto pedal release for a note that already received a note-off),
and should therefore stop playing.
This will find any voice that is currently playing finishedNote,
turn its currently playing note off, and call its noteStopped callback.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself, otherwise the internal MPE note state
will become inconsistent.
*/
void noteReleased (MPENote finishedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its notePressureChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void notePressureChanged (MPENote changedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its notePitchbendChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void notePitchbendChanged (MPENote changedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its noteTimbreChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void noteTimbreChanged (MPENote changedNote) override;
/** Will find any voice that is currently playing changedNote, update its
currently playing note, and call its noteKeyStateChanged method.
This method will be called automatically according to the midi data passed into
renderNextBlock(). Do not call it yourself.
*/
void noteKeyStateChanged (MPENote changedNote) override;
//==============================================================================
/** This will simply call renderNextBlock for each currently active
voice and fill the buffer with the sum.
Override this method if you need to do more work to render your audio.
*/
void renderNextSubBlock (AudioBuffer<float>& outputAudio,
int startSample,
int numSamples) override;
/** This will simply call renderNextBlock for each currently active
voice and fill the buffer with the sum. (double-precision version)
Override this method if you need to do more work to render your audio.
*/
void renderNextSubBlock (AudioBuffer<double>& outputAudio,
int startSample,
int numSamples) override;
//==============================================================================
/** Searches through the voices to find one that's not currently playing, and
which can play the given MPE note.
If all voices are active and stealIfNoneAvailable is false, this returns
a nullptr. If all voices are active and stealIfNoneAvailable is true,
this will call findVoiceToSteal() to find a voice.
If you need to find a free voice for something else than playing a note
(e.g. for deleting it), you can pass an invalid (default-constructed) MPENote.
*/
virtual MPESynthesiserVoice* findFreeVoice (MPENote noteToFindVoiceFor,
bool stealIfNoneAvailable) const;
/** Chooses a voice that is most suitable for being re-used to play a new
note, or for being deleted by reduceNumVoices.
The default method will attempt to find the oldest voice that isn't the
bottom or top note being played. If that's not suitable for your synth,
you can override this method and do something more cunning instead.
If you pass a valid MPENote for the optional argument, then the note number
of that note will be taken into account for finding the ideal voice to steal.
If you pass an invalid (default-constructed) MPENote instead, this part of
the algorithm will be ignored.
*/
virtual MPESynthesiserVoice* findVoiceToSteal (MPENote noteToStealVoiceFor = MPENote()) const;
/** Starts a specified voice and tells it to play a particular MPENote.
You should never need to call this, it's called internally by
MPESynthesiserBase::instrument via the noteStarted callback,
but is protected in case it's useful for some custom subclasses.
*/
void startVoice (MPESynthesiserVoice* voice, MPENote noteToStart);
/** Stops a given voice and tells it to stop playing a particular MPENote
(which should be the same note it is actually playing).
You should never need to call this, it's called internally by
MPESynthesiserBase::instrument via the noteReleased callback,
but is protected in case it's useful for some custom subclasses.
*/
void stopVoice (MPESynthesiserVoice* voice, MPENote noteToStop, bool allowTailOff);
//==============================================================================
OwnedArray<MPESynthesiserVoice> voices;
CriticalSection voicesLock;
private:
//==============================================================================
std::atomic<bool> shouldStealVoices { false };
uint32 lastNoteOnCounter = 0;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiser)
};
} // namespace juce

View File

@@ -1,376 +1,375 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPESynthesiserBase::MPESynthesiserBase()
: instrument (new MPEInstrument)
{
instrument->addListener (this);
}
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument* inst)
: instrument (inst)
{
jassert (instrument != nullptr);
instrument->addListener (this);
}
//==============================================================================
MPEZoneLayout MPESynthesiserBase::getZoneLayout() const noexcept
{
return instrument->getZoneLayout();
}
void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout)
{
instrument->setZoneLayout (newLayout);
}
//==============================================================================
void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{
instrument->enableLegacyMode (pitchbendRange, channelRange);
}
bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept
{
return instrument->isLegacyModeEnabled();
}
Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept
{
return instrument->getLegacyModeChannelRange();
}
void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange)
{
instrument->setLegacyModeChannelRange (channelRange);
}
int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept
{
return instrument->getLegacyModePitchbendRange();
}
void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange)
{
instrument->setLegacyModePitchbendRange (pitchbendRange);
}
//==============================================================================
void MPESynthesiserBase::setPressureTrackingMode (TrackingMode modeToUse)
{
instrument->setPressureTrackingMode (modeToUse);
}
void MPESynthesiserBase::setPitchbendTrackingMode (TrackingMode modeToUse)
{
instrument->setPitchbendTrackingMode (modeToUse);
}
void MPESynthesiserBase::setTimbreTrackingMode (TrackingMode modeToUse)
{
instrument->setTimbreTrackingMode (modeToUse);
}
//==============================================================================
void MPESynthesiserBase::handleMidiEvent (const MidiMessage& m)
{
instrument->processNextMidiEvent (m);
}
//==============================================================================
template <typename floatType>
void MPESynthesiserBase::renderNextBlock (AudioBuffer<floatType>& outputAudio,
const MidiBuffer& inputMidi,
int startSample,
int numSamples)
{
// you must set the sample rate before using this!
jassert (sampleRate != 0);
const ScopedLock sl (noteStateLock);
auto prevSample = startSample;
const auto endSample = startSample + numSamples;
for (auto it = inputMidi.findNextSamplePosition (startSample); it != inputMidi.cend(); ++it)
{
const auto metadata = *it;
if (metadata.samplePosition >= endSample)
break;
const auto smallBlockAllowed = (prevSample == startSample && ! subBlockSubdivisionIsStrict);
const auto thisBlockSize = smallBlockAllowed ? 1 : minimumSubBlockSize;
if (metadata.samplePosition >= prevSample + thisBlockSize)
{
renderNextSubBlock (outputAudio, prevSample, metadata.samplePosition - prevSample);
prevSample = metadata.samplePosition;
}
handleMidiEvent (metadata.getMessage());
}
if (prevSample < endSample)
renderNextSubBlock (outputAudio, prevSample, endSample - prevSample);
}
// explicit instantiation for supported float types:
template void MPESynthesiserBase::renderNextBlock<float> (AudioBuffer<float>&, const MidiBuffer&, int, int);
template void MPESynthesiserBase::renderNextBlock<double> (AudioBuffer<double>&, const MidiBuffer&, int, int);
//==============================================================================
void MPESynthesiserBase::setCurrentPlaybackSampleRate (const double newRate)
{
if (sampleRate != newRate)
{
const ScopedLock sl (noteStateLock);
instrument->releaseAllNotes();
sampleRate = newRate;
}
}
//==============================================================================
void MPESynthesiserBase::setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict) noexcept
{
jassert (numSamples > 0); // it wouldn't make much sense for this to be less than 1
minimumSubBlockSize = numSamples;
subBlockSubdivisionIsStrict = shouldBeStrict;
}
#if JUCE_UNIT_TESTS
namespace
{
class MpeSynthesiserBaseTests : public UnitTest
{
enum class CallbackKind { process, midi };
struct StartAndLength
{
StartAndLength (int s, int l) : start (s), length (l) {}
int start = 0;
int length = 0;
std::tuple<const int&, const int&> tie() const noexcept { return std::tie (start, length); }
bool operator== (const StartAndLength& other) const noexcept { return tie() == other.tie(); }
bool operator!= (const StartAndLength& other) const noexcept { return tie() != other.tie(); }
bool operator< (const StartAndLength& other) const noexcept { return tie() < other.tie(); }
};
struct Events
{
std::vector<StartAndLength> blocks;
std::vector<MidiMessage> messages;
std::vector<CallbackKind> order;
};
class MockSynthesiser : public MPESynthesiserBase
{
public:
Events events;
void handleMidiEvent (const MidiMessage& m) override
{
events.messages.emplace_back (m);
events.order.emplace_back (CallbackKind::midi);
}
private:
using MPESynthesiserBase::renderNextSubBlock;
void renderNextSubBlock (AudioBuffer<float>&,
int startSample,
int numSamples) override
{
events.blocks.push_back ({ startSample, numSamples });
events.order.emplace_back (CallbackKind::process);
}
};
static MidiBuffer makeTestBuffer (const int bufferLength)
{
MidiBuffer result;
for (int i = 0; i != bufferLength; ++i)
result.addEvent ({}, i);
return result;
}
public:
MpeSynthesiserBaseTests()
: UnitTest ("MPE Synthesiser Base", UnitTestCategories::midi) {}
void runTest() override
{
const auto sumBlockLengths = [] (const std::vector<StartAndLength>& b)
{
const auto addBlock = [] (int acc, const StartAndLength& info) { return acc + info.length; };
return std::accumulate (b.begin(), b.end(), 0, addBlock);
};
beginTest ("Rendering sparse subblocks works");
{
const int blockSize = 512;
const auto midi = [&] { MidiBuffer b; b.addEvent ({}, blockSize / 2); return b; }();
AudioBuffer<float> audio (1, blockSize);
const auto processEvents = [&] (int start, int length)
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (1, false);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, midi, start, length);
return synth.events;
};
{
const auto e = processEvents (0, blockSize);
expect (e.blocks.size() == 2);
expect (e.messages.size() == 1);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == blockSize);
expect (e.order == std::vector<CallbackKind> { CallbackKind::process,
CallbackKind::midi,
CallbackKind::process });
}
}
beginTest ("Rendering subblocks processes only contained midi events");
{
const int blockSize = 512;
const auto midi = makeTestBuffer (blockSize);
AudioBuffer<float> audio (1, blockSize);
const auto processEvents = [&] (int start, int length)
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (1, false);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, midi, start, length);
return synth.events;
};
{
const int subBlockLength = 0;
const auto e = processEvents (0, subBlockLength);
expect (e.blocks.size() == 0);
expect (e.messages.size() == 0);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == subBlockLength);
}
{
const int subBlockLength = 0;
const auto e = processEvents (1, subBlockLength);
expect (e.blocks.size() == 0);
expect (e.messages.size() == 0);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == subBlockLength);
}
{
const int subBlockLength = 1;
const auto e = processEvents (1, subBlockLength);
expect (e.blocks.size() == 1);
expect (e.messages.size() == 1);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == subBlockLength);
expect (e.order == std::vector<CallbackKind> { CallbackKind::midi,
CallbackKind::process });
}
{
const auto e = processEvents (0, blockSize);
expect (e.blocks.size() == blockSize);
expect (e.messages.size() == blockSize);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == blockSize);
expect (e.order.front() == CallbackKind::midi);
}
}
beginTest ("Subblocks respect their minimum size");
{
const int blockSize = 512;
const auto midi = makeTestBuffer (blockSize);
AudioBuffer<float> audio (1, blockSize);
const auto blockLengthsAreValid = [] (const std::vector<StartAndLength>& info, int minLength, bool strict)
{
if (info.size() <= 1)
return true;
const auto lengthIsValid = [&] (const StartAndLength& s) { return minLength <= s.length; };
const auto begin = strict ? info.begin() : std::next (info.begin());
// The final block is allowed to be shorter than the minLength
return std::all_of (begin, std::prev (info.end()), lengthIsValid);
};
for (auto strict : { false, true })
{
for (auto subblockSize : { 1, 16, 32, 64, 1024 })
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (subblockSize, strict);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, midi, 0, blockSize);
const auto& e = synth.events;
expectWithinAbsoluteError (float (e.blocks.size()),
std::ceil ((float) blockSize / (float) subblockSize),
1.0f);
expect (e.messages.size() == blockSize);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == blockSize);
expect (blockLengthsAreValid (e.blocks, subblockSize, strict));
}
}
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (32, true);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, MidiBuffer{}, 0, 16);
expect (synth.events.blocks == std::vector<StartAndLength> { { 0, 16 } });
expect (synth.events.order == std::vector<CallbackKind> { CallbackKind::process });
expect (synth.events.messages.empty());
}
}
}
};
MpeSynthesiserBaseTests mpeSynthesiserBaseTests;
}
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPESynthesiserBase::MPESynthesiserBase()
: instrument (defaultInstrument)
{
instrument.addListener (this);
}
MPESynthesiserBase::MPESynthesiserBase (MPEInstrument& inst)
: instrument (inst)
{
instrument.addListener (this);
}
//==============================================================================
MPEZoneLayout MPESynthesiserBase::getZoneLayout() const noexcept
{
return instrument.getZoneLayout();
}
void MPESynthesiserBase::setZoneLayout (MPEZoneLayout newLayout)
{
instrument.setZoneLayout (newLayout);
}
//==============================================================================
void MPESynthesiserBase::enableLegacyMode (int pitchbendRange, Range<int> channelRange)
{
instrument.enableLegacyMode (pitchbendRange, channelRange);
}
bool MPESynthesiserBase::isLegacyModeEnabled() const noexcept
{
return instrument.isLegacyModeEnabled();
}
Range<int> MPESynthesiserBase::getLegacyModeChannelRange() const noexcept
{
return instrument.getLegacyModeChannelRange();
}
void MPESynthesiserBase::setLegacyModeChannelRange (Range<int> channelRange)
{
instrument.setLegacyModeChannelRange (channelRange);
}
int MPESynthesiserBase::getLegacyModePitchbendRange() const noexcept
{
return instrument.getLegacyModePitchbendRange();
}
void MPESynthesiserBase::setLegacyModePitchbendRange (int pitchbendRange)
{
instrument.setLegacyModePitchbendRange (pitchbendRange);
}
//==============================================================================
void MPESynthesiserBase::setPressureTrackingMode (TrackingMode modeToUse)
{
instrument.setPressureTrackingMode (modeToUse);
}
void MPESynthesiserBase::setPitchbendTrackingMode (TrackingMode modeToUse)
{
instrument.setPitchbendTrackingMode (modeToUse);
}
void MPESynthesiserBase::setTimbreTrackingMode (TrackingMode modeToUse)
{
instrument.setTimbreTrackingMode (modeToUse);
}
//==============================================================================
void MPESynthesiserBase::handleMidiEvent (const MidiMessage& m)
{
instrument.processNextMidiEvent (m);
}
//==============================================================================
template <typename floatType>
void MPESynthesiserBase::renderNextBlock (AudioBuffer<floatType>& outputAudio,
const MidiBuffer& inputMidi,
int startSample,
int numSamples)
{
// you must set the sample rate before using this!
jassert (sampleRate != 0);
const ScopedLock sl (noteStateLock);
auto prevSample = startSample;
const auto endSample = startSample + numSamples;
for (auto it = inputMidi.findNextSamplePosition (startSample); it != inputMidi.cend(); ++it)
{
const auto metadata = *it;
if (metadata.samplePosition >= endSample)
break;
const auto smallBlockAllowed = (prevSample == startSample && ! subBlockSubdivisionIsStrict);
const auto thisBlockSize = smallBlockAllowed ? 1 : minimumSubBlockSize;
if (metadata.samplePosition >= prevSample + thisBlockSize)
{
renderNextSubBlock (outputAudio, prevSample, metadata.samplePosition - prevSample);
prevSample = metadata.samplePosition;
}
handleMidiEvent (metadata.getMessage());
}
if (prevSample < endSample)
renderNextSubBlock (outputAudio, prevSample, endSample - prevSample);
}
// explicit instantiation for supported float types:
template void MPESynthesiserBase::renderNextBlock<float> (AudioBuffer<float>&, const MidiBuffer&, int, int);
template void MPESynthesiserBase::renderNextBlock<double> (AudioBuffer<double>&, const MidiBuffer&, int, int);
//==============================================================================
void MPESynthesiserBase::setCurrentPlaybackSampleRate (const double newRate)
{
if (sampleRate != newRate)
{
const ScopedLock sl (noteStateLock);
instrument.releaseAllNotes();
sampleRate = newRate;
}
}
//==============================================================================
void MPESynthesiserBase::setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict) noexcept
{
jassert (numSamples > 0); // it wouldn't make much sense for this to be less than 1
minimumSubBlockSize = numSamples;
subBlockSubdivisionIsStrict = shouldBeStrict;
}
#if JUCE_UNIT_TESTS
namespace
{
class MpeSynthesiserBaseTests : public UnitTest
{
enum class CallbackKind { process, midi };
struct StartAndLength
{
StartAndLength (int s, int l) : start (s), length (l) {}
int start = 0;
int length = 0;
std::tuple<const int&, const int&> tie() const noexcept { return std::tie (start, length); }
bool operator== (const StartAndLength& other) const noexcept { return tie() == other.tie(); }
bool operator!= (const StartAndLength& other) const noexcept { return tie() != other.tie(); }
bool operator< (const StartAndLength& other) const noexcept { return tie() < other.tie(); }
};
struct Events
{
std::vector<StartAndLength> blocks;
std::vector<MidiMessage> messages;
std::vector<CallbackKind> order;
};
class MockSynthesiser : public MPESynthesiserBase
{
public:
Events events;
void handleMidiEvent (const MidiMessage& m) override
{
events.messages.emplace_back (m);
events.order.emplace_back (CallbackKind::midi);
}
private:
using MPESynthesiserBase::renderNextSubBlock;
void renderNextSubBlock (AudioBuffer<float>&,
int startSample,
int numSamples) override
{
events.blocks.push_back ({ startSample, numSamples });
events.order.emplace_back (CallbackKind::process);
}
};
static MidiBuffer makeTestBuffer (const int bufferLength)
{
MidiBuffer result;
for (int i = 0; i != bufferLength; ++i)
result.addEvent ({}, i);
return result;
}
public:
MpeSynthesiserBaseTests()
: UnitTest ("MPE Synthesiser Base", UnitTestCategories::midi) {}
void runTest() override
{
const auto sumBlockLengths = [] (const std::vector<StartAndLength>& b)
{
const auto addBlock = [] (int acc, const StartAndLength& info) { return acc + info.length; };
return std::accumulate (b.begin(), b.end(), 0, addBlock);
};
beginTest ("Rendering sparse subblocks works");
{
const int blockSize = 512;
const auto midi = [&] { MidiBuffer b; b.addEvent ({}, blockSize / 2); return b; }();
AudioBuffer<float> audio (1, blockSize);
const auto processEvents = [&] (int start, int length)
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (1, false);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, midi, start, length);
return synth.events;
};
{
const auto e = processEvents (0, blockSize);
expect (e.blocks.size() == 2);
expect (e.messages.size() == 1);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == blockSize);
expect (e.order == std::vector<CallbackKind> { CallbackKind::process,
CallbackKind::midi,
CallbackKind::process });
}
}
beginTest ("Rendering subblocks processes only contained midi events");
{
const int blockSize = 512;
const auto midi = makeTestBuffer (blockSize);
AudioBuffer<float> audio (1, blockSize);
const auto processEvents = [&] (int start, int length)
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (1, false);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, midi, start, length);
return synth.events;
};
{
const int subBlockLength = 0;
const auto e = processEvents (0, subBlockLength);
expect (e.blocks.size() == 0);
expect (e.messages.size() == 0);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == subBlockLength);
}
{
const int subBlockLength = 0;
const auto e = processEvents (1, subBlockLength);
expect (e.blocks.size() == 0);
expect (e.messages.size() == 0);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == subBlockLength);
}
{
const int subBlockLength = 1;
const auto e = processEvents (1, subBlockLength);
expect (e.blocks.size() == 1);
expect (e.messages.size() == 1);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == subBlockLength);
expect (e.order == std::vector<CallbackKind> { CallbackKind::midi,
CallbackKind::process });
}
{
const auto e = processEvents (0, blockSize);
expect (e.blocks.size() == blockSize);
expect (e.messages.size() == blockSize);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == blockSize);
expect (e.order.front() == CallbackKind::midi);
}
}
beginTest ("Subblocks respect their minimum size");
{
const int blockSize = 512;
const auto midi = makeTestBuffer (blockSize);
AudioBuffer<float> audio (1, blockSize);
const auto blockLengthsAreValid = [] (const std::vector<StartAndLength>& info, int minLength, bool strict)
{
if (info.size() <= 1)
return true;
const auto lengthIsValid = [&] (const StartAndLength& s) { return minLength <= s.length; };
const auto begin = strict ? info.begin() : std::next (info.begin());
// The final block is allowed to be shorter than the minLength
return std::all_of (begin, std::prev (info.end()), lengthIsValid);
};
for (auto strict : { false, true })
{
for (auto subblockSize : { 1, 16, 32, 64, 1024 })
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (subblockSize, strict);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, midi, 0, blockSize);
const auto& e = synth.events;
expectWithinAbsoluteError (float (e.blocks.size()),
std::ceil ((float) blockSize / (float) subblockSize),
1.0f);
expect (e.messages.size() == blockSize);
expect (std::is_sorted (e.blocks.begin(), e.blocks.end()));
expect (sumBlockLengths (e.blocks) == blockSize);
expect (blockLengthsAreValid (e.blocks, subblockSize, strict));
}
}
{
MockSynthesiser synth;
synth.setMinimumRenderingSubdivisionSize (32, true);
synth.setCurrentPlaybackSampleRate (44100);
synth.renderNextBlock (audio, MidiBuffer{}, 0, 16);
expect (synth.events.blocks == std::vector<StartAndLength> { { 0, 16 } });
expect (synth.events.order == std::vector<CallbackKind> { CallbackKind::process });
expect (synth.events.messages.empty());
}
}
}
};
MpeSynthesiserBaseTests mpeSynthesiserBaseTests;
}
#endif
} // namespace juce

View File

@@ -1,215 +1,216 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Derive from this class to create a basic audio generator capable of MPE.
Implement the callbacks of MPEInstrument::Listener (noteAdded, notePressureChanged
etc.) to let your audio generator know that MPE notes were triggered, modulated,
or released. What to do inside them, and how that influences your audio generator,
is up to you!
This class uses an instance of MPEInstrument internally to handle the MPE
note state logic.
This class is a very low-level base class for an MPE instrument. If you need
something more sophisticated, have a look at MPESynthesiser. This class extends
MPESynthesiserBase by adding the concept of voices that can play notes,
a voice stealing algorithm, and much more.
@see MPESynthesiser, MPEInstrument
@tags{Audio}
*/
struct JUCE_API MPESynthesiserBase : public MPEInstrument::Listener
{
public:
//==============================================================================
/** Constructor. */
MPESynthesiserBase();
/** Constructor.
If you use this constructor, the synthesiser will take ownership of the
provided instrument object, and will use it internally to handle the
MPE note state logic.
This is useful if you want to use an instance of your own class derived
from MPEInstrument for the MPE logic.
*/
MPESynthesiserBase (MPEInstrument* instrument);
//==============================================================================
/** Returns the synthesiser's internal MPE zone layout.
This happens by value, to enforce thread-safety and class invariants.
*/
MPEZoneLayout getZoneLayout() const noexcept;
/** Re-sets the synthesiser's internal MPE zone layout to the one passed in.
As a side effect, this will discard all currently playing notes,
call noteReleased for all of them, and disable legacy mode (if previously enabled).
*/
void setZoneLayout (MPEZoneLayout newLayout);
//==============================================================================
/** Tells the synthesiser what the sample rate is for the audio it's being
used to render.
*/
virtual void setCurrentPlaybackSampleRate (double sampleRate);
/** Returns the current target sample rate at which rendering is being done.
Subclasses may need to know this so that they can pitch things correctly.
*/
double getSampleRate() const noexcept { return sampleRate; }
//==============================================================================
/** Creates the next block of audio output.
Call this to make sound. This will chop up the AudioBuffer into subBlock
pieces separated by events in the MIDI buffer, and then call
renderNextSubBlock on each one of them. In between you will get calls
to noteAdded/Changed/Finished, where you can update parameters that
depend on those notes to use for your audio rendering.
@param outputAudio Buffer into which audio will be rendered
@param inputMidi MIDI events to process
@param startSample The first sample to process in both buffers
@param numSamples The number of samples to process
*/
template <typename floatType>
void renderNextBlock (AudioBuffer<floatType>& outputAudio,
const MidiBuffer& inputMidi,
int startSample,
int numSamples);
//==============================================================================
/** Handle incoming MIDI events (called from renderNextBlock).
The default implementation provided here simply forwards everything
to MPEInstrument::processNextMidiEvent, where it is used to update the
MPE notes, zones etc. MIDI messages not relevant for MPE are ignored.
This method can be overridden if you need to do custom MIDI handling
on top of MPE. The MPESynthesiser class overrides this to implement
callbacks for MIDI program changes and non-MPE-related MIDI controller
messages.
*/
virtual void handleMidiEvent (const MidiMessage&);
//==============================================================================
/** Sets a minimum limit on the size to which audio sub-blocks will be divided when rendering.
When rendering, the audio blocks that are passed into renderNextBlock() will be split up
into smaller blocks that lie between all the incoming midi messages, and it is these smaller
sub-blocks that are rendered with multiple calls to renderVoices().
Obviously in a pathological case where there are midi messages on every sample, then
renderVoices() could be called once per sample and lead to poor performance, so this
setting allows you to set a lower limit on the block size.
The default setting is 32, which means that midi messages are accurate to about < 1ms
accuracy, which is probably fine for most purposes, but you may want to increase or
decrease this value for your synth.
If shouldBeStrict is true, the audio sub-blocks will strictly never be smaller than numSamples.
If shouldBeStrict is false (default), the first audio sub-block in the buffer is allowed
to be smaller, to make sure that the first MIDI event in a buffer will always be sample-accurate
(this can sometimes help to avoid quantisation or phasing issues).
*/
void setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict = false) noexcept;
//==============================================================================
/** Puts the synthesiser into legacy mode.
@param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
Must be between 0 and 96, otherwise behaviour is undefined.
The default pitchbend range in legacy mode is +/- 2 semitones.
@param channelRange The range of MIDI channels to use for notes when in legacy mode.
The default is to use all MIDI channels (1-16).
To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
*/
void enableLegacyMode (int pitchbendRange = 2,
Range<int> channelRange = Range<int> (1, 17));
/** Returns true if the instrument is in legacy mode, false otherwise. */
bool isLegacyModeEnabled() const noexcept;
/** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
Range<int> getLegacyModeChannelRange() const noexcept;
/** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
void setLegacyModeChannelRange (Range<int> channelRange);
/** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
int getLegacyModePitchbendRange() const noexcept;
/** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
void setLegacyModePitchbendRange (int pitchbendRange);
//==============================================================================
using TrackingMode = MPEInstrument::TrackingMode;
/** Set the MPE tracking mode for the pressure dimension. */
void setPressureTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the pitchbend dimension. */
void setPitchbendTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the timbre dimension. */
void setTimbreTrackingMode (TrackingMode modeToUse);
protected:
//==============================================================================
/** Implement this method to render your audio inside.
@see renderNextBlock
*/
virtual void renderNextSubBlock (AudioBuffer<float>& outputAudio,
int startSample,
int numSamples) = 0;
/** Implement this method if you want to render 64-bit audio as well;
otherwise leave blank.
*/
virtual void renderNextSubBlock (AudioBuffer<double>& /*outputAudio*/,
int /*startSample*/,
int /*numSamples*/) {}
protected:
//==============================================================================
/** @internal */
std::unique_ptr<MPEInstrument> instrument;
private:
//==============================================================================
CriticalSection noteStateLock;
double sampleRate = 0.0;
int minimumSubBlockSize = 32;
bool subBlockSubdivisionIsStrict = false;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiserBase)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Derive from this class to create a basic audio generator capable of MPE.
Implement the callbacks of MPEInstrument::Listener (noteAdded, notePressureChanged
etc.) to let your audio generator know that MPE notes were triggered, modulated,
or released. What to do inside them, and how that influences your audio generator,
is up to you!
This class uses an instance of MPEInstrument internally to handle the MPE
note state logic.
This class is a very low-level base class for an MPE instrument. If you need
something more sophisticated, have a look at MPESynthesiser. This class extends
MPESynthesiserBase by adding the concept of voices that can play notes,
a voice stealing algorithm, and much more.
@see MPESynthesiser, MPEInstrument
@tags{Audio}
*/
struct JUCE_API MPESynthesiserBase : public MPEInstrument::Listener
{
public:
//==============================================================================
/** Constructor. */
MPESynthesiserBase();
/** Constructor.
If you use this constructor, the synthesiser will use the provided instrument
object to handle the MPE note state logic.
This is useful if you want to use an instance of your own class derived
from MPEInstrument for the MPE logic.
*/
MPESynthesiserBase (MPEInstrument& instrument);
//==============================================================================
/** Returns the synthesiser's internal MPE zone layout.
This happens by value, to enforce thread-safety and class invariants.
*/
MPEZoneLayout getZoneLayout() const noexcept;
/** Re-sets the synthesiser's internal MPE zone layout to the one passed in.
As a side effect, this will discard all currently playing notes,
call noteReleased for all of them, and disable legacy mode (if previously enabled).
*/
void setZoneLayout (MPEZoneLayout newLayout);
//==============================================================================
/** Tells the synthesiser what the sample rate is for the audio it's being
used to render.
*/
virtual void setCurrentPlaybackSampleRate (double sampleRate);
/** Returns the current target sample rate at which rendering is being done.
Subclasses may need to know this so that they can pitch things correctly.
*/
double getSampleRate() const noexcept { return sampleRate; }
//==============================================================================
/** Creates the next block of audio output.
Call this to make sound. This will chop up the AudioBuffer into subBlock
pieces separated by events in the MIDI buffer, and then call
renderNextSubBlock on each one of them. In between you will get calls
to noteAdded/Changed/Finished, where you can update parameters that
depend on those notes to use for your audio rendering.
@param outputAudio Buffer into which audio will be rendered
@param inputMidi MIDI events to process
@param startSample The first sample to process in both buffers
@param numSamples The number of samples to process
*/
template <typename floatType>
void renderNextBlock (AudioBuffer<floatType>& outputAudio,
const MidiBuffer& inputMidi,
int startSample,
int numSamples);
//==============================================================================
/** Handle incoming MIDI events (called from renderNextBlock).
The default implementation provided here simply forwards everything
to MPEInstrument::processNextMidiEvent, where it is used to update the
MPE notes, zones etc. MIDI messages not relevant for MPE are ignored.
This method can be overridden if you need to do custom MIDI handling
on top of MPE. The MPESynthesiser class overrides this to implement
callbacks for MIDI program changes and non-MPE-related MIDI controller
messages.
*/
virtual void handleMidiEvent (const MidiMessage&);
//==============================================================================
/** Sets a minimum limit on the size to which audio sub-blocks will be divided when rendering.
When rendering, the audio blocks that are passed into renderNextBlock() will be split up
into smaller blocks that lie between all the incoming midi messages, and it is these smaller
sub-blocks that are rendered with multiple calls to renderVoices().
Obviously in a pathological case where there are midi messages on every sample, then
renderVoices() could be called once per sample and lead to poor performance, so this
setting allows you to set a lower limit on the block size.
The default setting is 32, which means that midi messages are accurate to about < 1ms
accuracy, which is probably fine for most purposes, but you may want to increase or
decrease this value for your synth.
If shouldBeStrict is true, the audio sub-blocks will strictly never be smaller than numSamples.
If shouldBeStrict is false (default), the first audio sub-block in the buffer is allowed
to be smaller, to make sure that the first MIDI event in a buffer will always be sample-accurate
(this can sometimes help to avoid quantisation or phasing issues).
*/
void setMinimumRenderingSubdivisionSize (int numSamples, bool shouldBeStrict = false) noexcept;
//==============================================================================
/** Puts the synthesiser into legacy mode.
@param pitchbendRange The note pitchbend range in semitones to use when in legacy mode.
Must be between 0 and 96, otherwise behaviour is undefined.
The default pitchbend range in legacy mode is +/- 2 semitones.
@param channelRange The range of MIDI channels to use for notes when in legacy mode.
The default is to use all MIDI channels (1-16).
To get out of legacy mode, set a new MPE zone layout using setZoneLayout.
*/
void enableLegacyMode (int pitchbendRange = 2,
Range<int> channelRange = Range<int> (1, 17));
/** Returns true if the instrument is in legacy mode, false otherwise. */
bool isLegacyModeEnabled() const noexcept;
/** Returns the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
Range<int> getLegacyModeChannelRange() const noexcept;
/** Re-sets the range of MIDI channels (1-16) to be used for notes when in legacy mode. */
void setLegacyModeChannelRange (Range<int> channelRange);
/** Returns the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
int getLegacyModePitchbendRange() const noexcept;
/** Re-sets the pitchbend range in semitones (0-96) to be used for notes when in legacy mode. */
void setLegacyModePitchbendRange (int pitchbendRange);
//==============================================================================
using TrackingMode = MPEInstrument::TrackingMode;
/** Set the MPE tracking mode for the pressure dimension. */
void setPressureTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the pitchbend dimension. */
void setPitchbendTrackingMode (TrackingMode modeToUse);
/** Set the MPE tracking mode for the timbre dimension. */
void setTimbreTrackingMode (TrackingMode modeToUse);
protected:
//==============================================================================
/** Implement this method to render your audio inside.
@see renderNextBlock
*/
virtual void renderNextSubBlock (AudioBuffer<float>& outputAudio,
int startSample,
int numSamples) = 0;
/** Implement this method if you want to render 64-bit audio as well;
otherwise leave blank.
*/
virtual void renderNextSubBlock (AudioBuffer<double>& /*outputAudio*/,
int /*startSample*/,
int /*numSamples*/) {}
protected:
//==============================================================================
/** @internal */
MPEInstrument& instrument;
private:
//==============================================================================
MPEInstrument defaultInstrument { MPEZone (MPEZone::Type::lower, 15) };
CriticalSection noteStateLock;
double sampleRate = 0.0;
int minimumSubBlockSize = 32;
bool subBlockSubdivisionIsStrict = false;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiserBase)
};
} // namespace juce

View File

@@ -1,50 +1,50 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPESynthesiserVoice::MPESynthesiserVoice()
{
}
MPESynthesiserVoice::~MPESynthesiserVoice()
{
}
//==============================================================================
bool MPESynthesiserVoice::isCurrentlyPlayingNote (MPENote note) const noexcept
{
return isActive() && currentlyPlayingNote.noteID == note.noteID;
}
bool MPESynthesiserVoice::isPlayingButReleased() const noexcept
{
return isActive() && currentlyPlayingNote.keyState == MPENote::off;
}
void MPESynthesiserVoice::clearCurrentNote() noexcept
{
currentlyPlayingNote = MPENote();
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPESynthesiserVoice::MPESynthesiserVoice()
{
}
MPESynthesiserVoice::~MPESynthesiserVoice()
{
}
//==============================================================================
bool MPESynthesiserVoice::isCurrentlyPlayingNote (MPENote note) const noexcept
{
return isActive() && currentlyPlayingNote.noteID == note.noteID;
}
bool MPESynthesiserVoice::isPlayingButReleased() const noexcept
{
return isActive() && currentlyPlayingNote.keyState == MPENote::off;
}
void MPESynthesiserVoice::clearCurrentNote() noexcept
{
currentlyPlayingNote = MPENote();
}
} // namespace juce

View File

@@ -1,191 +1,191 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Represents an MPE voice that an MPESynthesiser can use to play a sound.
A voice plays a single sound at a time, and a synthesiser holds an array of
voices so that it can play polyphonically.
@see MPESynthesiser, MPENote
@tags{Audio}
*/
class JUCE_API MPESynthesiserVoice
{
public:
//==============================================================================
/** Constructor. */
MPESynthesiserVoice();
/** Destructor. */
virtual ~MPESynthesiserVoice();
/** Returns the MPENote that this voice is currently playing.
Returns an invalid MPENote if no note is playing
(you can check this using MPENote::isValid() or MPEVoice::isActive()).
*/
MPENote getCurrentlyPlayingNote() const noexcept { return currentlyPlayingNote; }
/** Returns true if the voice is currently playing the given MPENote
(as identified by the note's initial note number and MIDI channel).
*/
bool isCurrentlyPlayingNote (MPENote note) const noexcept;
/** Returns true if this voice is currently busy playing a sound.
By default this just checks whether getCurrentlyPlayingNote()
returns a valid MPE note, but can be overridden for more advanced checking.
*/
virtual bool isActive() const { return currentlyPlayingNote.isValid(); }
/** Returns true if a voice is sounding in its release phase. **/
bool isPlayingButReleased() const noexcept;
/** Called by the MPESynthesiser to let the voice know that a new note has started on it.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void noteStarted() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note has stopped.
This will be called during the rendering callback, so must be fast and thread-safe.
If allowTailOff is false or the voice doesn't want to tail-off, then it must stop all
sound immediately, and must call clearCurrentNote() to reset the state of this voice
and allow the synth to reassign it another sound.
If allowTailOff is true and the voice decides to do a tail-off, then it's allowed to
begin fading out its sound, and it can stop playing until it's finished. As soon as it
finishes playing (during the rendering callback), it must make sure that it calls
clearCurrentNote().
*/
virtual void noteStopped (bool allowTailOff) = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its pressure value.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void notePressureChanged() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its pitchbend value.
This will be called during the rendering callback, so must be fast and thread-safe.
Note: You can call currentlyPlayingNote.getFrequencyInHertz() to find out the effective frequency
of the note, as a sum of the initial note number, the per-note pitchbend and the master pitchbend.
*/
virtual void notePitchbendChanged() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its timbre value.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void noteTimbreChanged() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its key state.
This typically happens when a sustain or sostenuto pedal is pressed or released (on
an MPE channel relevant for this note), or if the note key is lifted while the sustained
or sostenuto pedal is still held down.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void noteKeyStateChanged() = 0;
/** Renders the next block of data for this voice.
The output audio data must be added to the current contents of the buffer provided.
Only the region of the buffer between startSample and (startSample + numSamples)
should be altered by this method.
If the voice is currently silent, it should just return without doing anything.
If the sound that the voice is playing finishes during the course of this rendered
block, it must call clearCurrentNote(), to tell the synthesiser that it has finished.
The size of the blocks that are rendered can change each time it is called, and may
involve rendering as little as 1 sample at a time. In between rendering callbacks,
the voice's methods will be called to tell it about note and controller events.
*/
virtual void renderNextBlock (AudioBuffer<float>& outputBuffer,
int startSample,
int numSamples) = 0;
/** Renders the next block of 64-bit data for this voice.
Support for 64-bit audio is optional. You can choose to not override this method if
you don't need it (the default implementation simply does nothing).
*/
virtual void renderNextBlock (AudioBuffer<double>& /*outputBuffer*/,
int /*startSample*/,
int /*numSamples*/) {}
/** Changes the voice's reference sample rate.
The rate is set so that subclasses know the output rate and can set their pitch
accordingly.
This method is called by the synth, and subclasses can access the current rate with
the currentSampleRate member.
*/
virtual void setCurrentSampleRate (double newRate) { currentSampleRate = newRate; }
/** Returns the current target sample rate at which rendering is being done.
Subclasses may need to know this so that they can pitch things correctly.
*/
double getSampleRate() const noexcept { return currentSampleRate; }
/** This will be set to an incrementing counter value in MPESynthesiser::startVoice()
and can be used to determine the order in which voices started.
*/
uint32 noteOnTime = 0;
protected:
//==============================================================================
/** Resets the state of this voice after a sound has finished playing.
The subclass must call this when it finishes playing a note and becomes available
to play new ones.
It must either call it in the stopNote() method, or if the voice is tailing off,
then it should call it later during the renderNextBlock method, as soon as it
finishes its tail-off.
It can also be called at any time during the render callback if the sound happens
to have finished, e.g. if it's playing a sample and the sample finishes.
*/
void clearCurrentNote() noexcept;
//==============================================================================
double currentSampleRate = 0.0;
MPENote currentlyPlayingNote;
private:
//==============================================================================
friend class MPESynthesiser;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiserVoice)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Represents an MPE voice that an MPESynthesiser can use to play a sound.
A voice plays a single sound at a time, and a synthesiser holds an array of
voices so that it can play polyphonically.
@see MPESynthesiser, MPENote
@tags{Audio}
*/
class JUCE_API MPESynthesiserVoice
{
public:
//==============================================================================
/** Constructor. */
MPESynthesiserVoice();
/** Destructor. */
virtual ~MPESynthesiserVoice();
/** Returns the MPENote that this voice is currently playing.
Returns an invalid MPENote if no note is playing
(you can check this using MPENote::isValid() or MPEVoice::isActive()).
*/
MPENote getCurrentlyPlayingNote() const noexcept { return currentlyPlayingNote; }
/** Returns true if the voice is currently playing the given MPENote
(as identified by the note's initial note number and MIDI channel).
*/
bool isCurrentlyPlayingNote (MPENote note) const noexcept;
/** Returns true if this voice is currently busy playing a sound.
By default this just checks whether getCurrentlyPlayingNote()
returns a valid MPE note, but can be overridden for more advanced checking.
*/
virtual bool isActive() const { return currentlyPlayingNote.isValid(); }
/** Returns true if a voice is sounding in its release phase. **/
bool isPlayingButReleased() const noexcept;
/** Called by the MPESynthesiser to let the voice know that a new note has started on it.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void noteStarted() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note has stopped.
This will be called during the rendering callback, so must be fast and thread-safe.
If allowTailOff is false or the voice doesn't want to tail-off, then it must stop all
sound immediately, and must call clearCurrentNote() to reset the state of this voice
and allow the synth to reassign it another sound.
If allowTailOff is true and the voice decides to do a tail-off, then it's allowed to
begin fading out its sound, and it can stop playing until it's finished. As soon as it
finishes playing (during the rendering callback), it must make sure that it calls
clearCurrentNote().
*/
virtual void noteStopped (bool allowTailOff) = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its pressure value.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void notePressureChanged() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its pitchbend value.
This will be called during the rendering callback, so must be fast and thread-safe.
Note: You can call currentlyPlayingNote.getFrequencyInHertz() to find out the effective frequency
of the note, as a sum of the initial note number, the per-note pitchbend and the master pitchbend.
*/
virtual void notePitchbendChanged() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its timbre value.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void noteTimbreChanged() = 0;
/** Called by the MPESynthesiser to let the voice know that its currently playing note
has changed its key state.
This typically happens when a sustain or sostenuto pedal is pressed or released (on
an MPE channel relevant for this note), or if the note key is lifted while the sustained
or sostenuto pedal is still held down.
This will be called during the rendering callback, so must be fast and thread-safe.
*/
virtual void noteKeyStateChanged() = 0;
/** Renders the next block of data for this voice.
The output audio data must be added to the current contents of the buffer provided.
Only the region of the buffer between startSample and (startSample + numSamples)
should be altered by this method.
If the voice is currently silent, it should just return without doing anything.
If the sound that the voice is playing finishes during the course of this rendered
block, it must call clearCurrentNote(), to tell the synthesiser that it has finished.
The size of the blocks that are rendered can change each time it is called, and may
involve rendering as little as 1 sample at a time. In between rendering callbacks,
the voice's methods will be called to tell it about note and controller events.
*/
virtual void renderNextBlock (AudioBuffer<float>& outputBuffer,
int startSample,
int numSamples) = 0;
/** Renders the next block of 64-bit data for this voice.
Support for 64-bit audio is optional. You can choose to not override this method if
you don't need it (the default implementation simply does nothing).
*/
virtual void renderNextBlock (AudioBuffer<double>& /*outputBuffer*/,
int /*startSample*/,
int /*numSamples*/) {}
/** Changes the voice's reference sample rate.
The rate is set so that subclasses know the output rate and can set their pitch
accordingly.
This method is called by the synth, and subclasses can access the current rate with
the currentSampleRate member.
*/
virtual void setCurrentSampleRate (double newRate) { currentSampleRate = newRate; }
/** Returns the current target sample rate at which rendering is being done.
Subclasses may need to know this so that they can pitch things correctly.
*/
double getSampleRate() const noexcept { return currentSampleRate; }
/** This will be set to an incrementing counter value in MPESynthesiser::startVoice()
and can be used to determine the order in which voices started.
*/
uint32 noteOnTime = 0;
protected:
//==============================================================================
/** Resets the state of this voice after a sound has finished playing.
The subclass must call this when it finishes playing a note and becomes available
to play new ones.
It must either call it in the stopNote() method, or if the voice is tailing off,
then it should call it later during the renderNextBlock method, as soon as it
finishes its tail-off.
It can also be called at any time during the render callback if the sound happens
to have finished, e.g. if it's playing a sample and the sample finishes.
*/
void clearCurrentNote() noexcept;
//==============================================================================
double currentSampleRate = 0.0;
MPENote currentlyPlayingNote;
private:
//==============================================================================
friend class MPESynthesiser;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MPESynthesiserVoice)
};
} // namespace juce

File diff suppressed because it is too large Load Diff

View File

@@ -1,153 +1,158 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class handles the assignment of new MIDI notes to member channels of an active
MPE zone.
To use it, create an instance passing in the MPE zone that it should operate on
and then call use the findMidiChannelForNewNote() method for all note-on messages
and the noteOff() method for all note-off messages.
@tags{Audio}
*/
class MPEChannelAssigner
{
public:
/** Constructor.
This will assign channels within the range of the specified MPE zone.
*/
MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse);
/** Legacy mode constructor.
This will assign channels within the specified range.
*/
MPEChannelAssigner (Range<int> channelRange = Range<int> (1, 17));
/** This method will use a set of rules recommended in the MPE specification to
determine which member channel the specified MIDI note should be assigned to
and will return this channel number.
The rules have the following precedence:
- find a free channel on which the last note played was the same as the one specified
- find the next free channel in round-robin assignment
- find the channel number that is currently playing the closest note (but not the same)
@param noteNumber the MIDI note number to be assigned to a channel
@returns the zone's member channel that this note should be assigned to
*/
int findMidiChannelForNewNote (int noteNumber) noexcept;
/** You must call this method for all note-offs that you receive so that this class
can keep track of the currently playing notes internally.
You can specify the channel number the note off happened on. If you don't, it will
look through all channels to find the registered midi note matching the given note number.
*/
void noteOff (int noteNumber, int midiChannel = -1);
/** Call this to clear all currently playing notes. */
void allNotesOff();
private:
bool isLegacy = false;
std::unique_ptr<MPEZoneLayout::Zone> zone;
int channelIncrement, numChannels, firstChannel, lastChannel, midiChannelLastAssigned;
//==============================================================================
struct MidiChannel
{
Array<int> notes;
int lastNotePlayed = -1;
bool isFree() const noexcept { return notes.isEmpty(); }
};
MidiChannel midiChannels[17];
//==============================================================================
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;
};
//==============================================================================
/**
This class handles the logic for remapping MIDI note messages from multiple MPE
sources onto a specified MPE zone.
@tags{Audio}
*/
class MPEChannelRemapper
{
public:
/** Used to indicate that a particular source & channel combination is not currently using MPE. */
static const uint32 notMPE = 0;
/** Constructor */
MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap);
//==============================================================================
/** Remaps the MIDI channel of the specified MIDI message (if necessary).
Note that the MidiMessage object passed in will have it's channel changed if it
needs to be remapped.
@param message the message to be remapped
@param mpeSourceID the ID of the MPE source of the message. This is up to the
user to define and keep constant
*/
void remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept;
//==============================================================================
/** Resets all the source & channel combinations. */
void reset() noexcept;
/** Clears a specified channel of this MPE zone. */
void clearChannel (int channel) noexcept;
/** Clears all channels in use by a specified source. */
void clearSource (uint32 mpeSourceID);
private:
MPEZoneLayout::Zone zone;
int channelIncrement;
int firstChannel, lastChannel;
uint32 sourceAndChannel[17];
uint32 lastUsed[17];
uint32 counter = 0;
//==============================================================================
bool applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept;
int getBestChanToReuse() const noexcept;
void zeroArrays();
//==============================================================================
bool messageIsNoteData (const MidiMessage& m) { return (*m.getRawData() & 0xf0) != 0xf0; }
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class handles the assignment of new MIDI notes to member channels of an active
MPE zone.
To use it, create an instance passing in the MPE zone that it should operate on
and then call use the findMidiChannelForNewNote() method for all note-on messages
and the noteOff() method for all note-off messages.
@tags{Audio}
*/
class MPEChannelAssigner
{
public:
/** Constructor.
This will assign channels within the range of the specified MPE zone.
*/
MPEChannelAssigner (MPEZoneLayout::Zone zoneToUse);
/** Legacy mode constructor.
This will assign channels within the specified range.
*/
MPEChannelAssigner (Range<int> channelRange = Range<int> (1, 17));
/** This method will use a set of rules recommended in the MPE specification to
determine which member channel the specified MIDI note should be assigned to
and will return this channel number.
The rules have the following precedence:
- find a free channel on which the last note played was the same as the one specified
- find the next free channel in round-robin assignment
- find the channel number that is currently playing the closest note (but not the same)
@param noteNumber the MIDI note number to be assigned to a channel
@returns the zone's member channel that this note should be assigned to
*/
int findMidiChannelForNewNote (int noteNumber) noexcept;
/** If a note has been added using findMidiChannelForNewNote() this will return the channel
to which it was assigned, otherwise it will return -1.
*/
int findMidiChannelForExistingNote (int initialNoteOnNumber) noexcept;
/** You must call this method for all note-offs that you receive so that this class
can keep track of the currently playing notes internally.
You can specify the channel number the note off happened on. If you don't, it will
look through all channels to find the registered midi note matching the given note number.
*/
void noteOff (int noteNumber, int midiChannel = -1);
/** Call this to clear all currently playing notes. */
void allNotesOff();
private:
bool isLegacy = false;
std::unique_ptr<MPEZoneLayout::Zone> zone;
int channelIncrement, numChannels, firstChannel, lastChannel, midiChannelLastAssigned;
//==============================================================================
struct MidiChannel
{
Array<int> notes;
int lastNotePlayed = -1;
bool isFree() const noexcept { return notes.isEmpty(); }
};
std::array<MidiChannel, 17> midiChannels;
//==============================================================================
int findMidiChannelPlayingClosestNonequalNote (int noteNumber) noexcept;
};
//==============================================================================
/**
This class handles the logic for remapping MIDI note messages from multiple MPE
sources onto a specified MPE zone.
@tags{Audio}
*/
class MPEChannelRemapper
{
public:
/** Used to indicate that a particular source & channel combination is not currently using MPE. */
static const uint32 notMPE = 0;
/** Constructor */
MPEChannelRemapper (MPEZoneLayout::Zone zoneToRemap);
//==============================================================================
/** Remaps the MIDI channel of the specified MIDI message (if necessary).
Note that the MidiMessage object passed in will have it's channel changed if it
needs to be remapped.
@param message the message to be remapped
@param mpeSourceID the ID of the MPE source of the message. This is up to the
user to define and keep constant
*/
void remapMidiChannelIfNeeded (MidiMessage& message, uint32 mpeSourceID) noexcept;
//==============================================================================
/** Resets all the source & channel combinations. */
void reset() noexcept;
/** Clears a specified channel of this MPE zone. */
void clearChannel (int channel) noexcept;
/** Clears all channels in use by a specified source. */
void clearSource (uint32 mpeSourceID);
private:
MPEZoneLayout::Zone zone;
int channelIncrement;
int firstChannel, lastChannel;
uint32 sourceAndChannel[17];
uint32 lastUsed[17];
uint32 counter = 0;
//==============================================================================
bool applyRemapIfExisting (int channel, uint32 sourceAndChannelID, MidiMessage& m) noexcept;
int getBestChanToReuse() const noexcept;
void zeroArrays();
//==============================================================================
bool messageIsNoteData (const MidiMessage& m) { return (*m.getRawData() & 0xf0) != 0xf0; }
};
} // namespace juce

View File

@@ -1,173 +1,193 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPEValue::MPEValue() noexcept {}
MPEValue::MPEValue (int value) : normalisedValue (value) {}
//==============================================================================
MPEValue MPEValue::from7BitInt (int value) noexcept
{
jassert (value >= 0 && value <= 127);
auto valueAs14Bit = value <= 64 ? value << 7
: int (jmap<float> (float (value - 64), 0.0f, 63.0f, 0.0f, 8191.0f)) + 8192;
return { valueAs14Bit };
}
MPEValue MPEValue::from14BitInt (int value) noexcept
{
jassert (value >= 0 && value <= 16383);
return { value };
}
//==============================================================================
MPEValue MPEValue::minValue() noexcept { return MPEValue::from7BitInt (0); }
MPEValue MPEValue::centreValue() noexcept { return MPEValue::from7BitInt (64); }
MPEValue MPEValue::maxValue() noexcept { return MPEValue::from7BitInt (127); }
int MPEValue::as7BitInt() const noexcept
{
return normalisedValue >> 7;
}
int MPEValue::as14BitInt() const noexcept
{
return normalisedValue;
}
//==============================================================================
float MPEValue::asSignedFloat() const noexcept
{
return (normalisedValue < 8192)
? jmap<float> (float (normalisedValue), 0.0f, 8192.0f, -1.0f, 0.0f)
: jmap<float> (float (normalisedValue), 8192.0f, 16383.0f, 0.0f, 1.0f);
}
float MPEValue::asUnsignedFloat() const noexcept
{
return jmap<float> (float (normalisedValue), 0.0f, 16383.0f, 0.0f, 1.0f);
}
//==============================================================================
bool MPEValue::operator== (const MPEValue& other) const noexcept
{
return normalisedValue == other.normalisedValue;
}
bool MPEValue::operator!= (const MPEValue& other) const noexcept
{
return ! operator== (other);
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPEValueTests : public UnitTest
{
public:
MPEValueTests()
: UnitTest ("MPEValue class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("comparison operator");
{
MPEValue value1 = MPEValue::from7BitInt (7);
MPEValue value2 = MPEValue::from7BitInt (7);
MPEValue value3 = MPEValue::from7BitInt (8);
expect (value1 == value1);
expect (value1 == value2);
expect (value1 != value3);
}
beginTest ("special values");
{
expectEquals (MPEValue::minValue().as7BitInt(), 0);
expectEquals (MPEValue::minValue().as14BitInt(), 0);
expectEquals (MPEValue::centreValue().as7BitInt(), 64);
expectEquals (MPEValue::centreValue().as14BitInt(), 8192);
expectEquals (MPEValue::maxValue().as7BitInt(), 127);
expectEquals (MPEValue::maxValue().as14BitInt(), 16383);
}
beginTest ("zero/minimum value");
{
expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f);
}
beginTest ("maximum value");
{
expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f);
}
beginTest ("centre value");
{
expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f);
}
beginTest ("value halfway between min and centre");
{
expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f);
}
}
private:
//==============================================================================
void expectValuesConsistent (MPEValue value,
int expectedValueAs7BitInt,
int expectedValueAs14BitInt,
float expectedValueAsSignedFloat,
float expectedValueAsUnsignedFloat)
{
expectEquals (value.as7BitInt(), expectedValueAs7BitInt);
expectEquals (value.as14BitInt(), expectedValueAs14BitInt);
expectFloatWithinRelativeError (value.asSignedFloat(), expectedValueAsSignedFloat, 0.0001f);
expectFloatWithinRelativeError (value.asUnsignedFloat(), expectedValueAsUnsignedFloat, 0.0001f);
}
//==============================================================================
void expectFloatWithinRelativeError (float actualValue, float expectedValue, float maxRelativeError)
{
const float maxAbsoluteError = jmax (1.0f, std::abs (expectedValue)) * maxRelativeError;
expect (std::abs (expectedValue - actualValue) < maxAbsoluteError);
}
};
static MPEValueTests MPEValueUnitTests;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPEValue::MPEValue() noexcept {}
MPEValue::MPEValue (int value) : normalisedValue (value) {}
//==============================================================================
MPEValue MPEValue::from7BitInt (int value) noexcept
{
jassert (value >= 0 && value <= 127);
auto valueAs14Bit = value <= 64 ? value << 7
: int (jmap<float> (float (value - 64), 0.0f, 63.0f, 0.0f, 8191.0f)) + 8192;
return { valueAs14Bit };
}
MPEValue MPEValue::from14BitInt (int value) noexcept
{
jassert (value >= 0 && value <= 16383);
return { value };
}
MPEValue MPEValue::fromUnsignedFloat (float value) noexcept
{
jassert (0.0f <= value && value <= 1.0f);
return { roundToInt (value * 16383.0f) };
}
MPEValue MPEValue::fromSignedFloat (float value) noexcept
{
jassert (-1.0f <= value && value <= 1.0f);
return { roundToInt (((value + 1.0f) * 16383.0f) / 2.0f) };
}
//==============================================================================
MPEValue MPEValue::minValue() noexcept { return MPEValue::from7BitInt (0); }
MPEValue MPEValue::centreValue() noexcept { return MPEValue::from7BitInt (64); }
MPEValue MPEValue::maxValue() noexcept { return MPEValue::from7BitInt (127); }
int MPEValue::as7BitInt() const noexcept
{
return normalisedValue >> 7;
}
int MPEValue::as14BitInt() const noexcept
{
return normalisedValue;
}
//==============================================================================
float MPEValue::asSignedFloat() const noexcept
{
return (normalisedValue < 8192)
? jmap<float> (float (normalisedValue), 0.0f, 8192.0f, -1.0f, 0.0f)
: jmap<float> (float (normalisedValue), 8192.0f, 16383.0f, 0.0f, 1.0f);
}
float MPEValue::asUnsignedFloat() const noexcept
{
return jmap<float> (float (normalisedValue), 0.0f, 16383.0f, 0.0f, 1.0f);
}
//==============================================================================
bool MPEValue::operator== (const MPEValue& other) const noexcept
{
return normalisedValue == other.normalisedValue;
}
bool MPEValue::operator!= (const MPEValue& other) const noexcept
{
return ! operator== (other);
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPEValueTests : public UnitTest
{
public:
MPEValueTests()
: UnitTest ("MPEValue class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("comparison operator");
{
MPEValue value1 = MPEValue::from7BitInt (7);
MPEValue value2 = MPEValue::from7BitInt (7);
MPEValue value3 = MPEValue::from7BitInt (8);
expect (value1 == value1);
expect (value1 == value2);
expect (value1 != value3);
}
beginTest ("special values");
{
expectEquals (MPEValue::minValue().as7BitInt(), 0);
expectEquals (MPEValue::minValue().as14BitInt(), 0);
expectEquals (MPEValue::centreValue().as7BitInt(), 64);
expectEquals (MPEValue::centreValue().as14BitInt(), 8192);
expectEquals (MPEValue::maxValue().as7BitInt(), 127);
expectEquals (MPEValue::maxValue().as14BitInt(), 16383);
}
beginTest ("zero/minimum value");
{
expectValuesConsistent (MPEValue::from7BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::from14BitInt (0), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.0f), 0, 0, -1.0f, 0.0f);
expectValuesConsistent (MPEValue::fromSignedFloat (-1.0f), 0, 0, -1.0f, 0.0f);
}
beginTest ("maximum value");
{
expectValuesConsistent (MPEValue::from7BitInt (127), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::from14BitInt (16383), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
expectValuesConsistent (MPEValue::fromSignedFloat (1.0f), 127, 16383, 1.0f, 1.0f);
}
beginTest ("centre value");
{
expectValuesConsistent (MPEValue::from7BitInt (64), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::from14BitInt (8192), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.5f), 64, 8192, 0.0f, 0.5f);
expectValuesConsistent (MPEValue::fromSignedFloat (0.0f), 64, 8192, 0.0f, 0.5f);
}
beginTest ("value halfway between min and centre");
{
expectValuesConsistent (MPEValue::from7BitInt (32), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::from14BitInt (4096), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::fromUnsignedFloat (0.25f), 32, 4096, -0.5f, 0.25f);
expectValuesConsistent (MPEValue::fromSignedFloat (-0.5f), 32, 4096, -0.5f, 0.25f);
}
}
private:
//==============================================================================
void expectValuesConsistent (MPEValue value,
int expectedValueAs7BitInt,
int expectedValueAs14BitInt,
float expectedValueAsSignedFloat,
float expectedValueAsUnsignedFloat)
{
expectEquals (value.as7BitInt(), expectedValueAs7BitInt);
expectEquals (value.as14BitInt(), expectedValueAs14BitInt);
expectFloatWithinRelativeError (value.asSignedFloat(), expectedValueAsSignedFloat, 0.0001f);
expectFloatWithinRelativeError (value.asUnsignedFloat(), expectedValueAsUnsignedFloat, 0.0001f);
}
//==============================================================================
void expectFloatWithinRelativeError (float actualValue, float expectedValue, float maxRelativeError)
{
const float maxAbsoluteError = jmax (1.0f, std::abs (expectedValue)) * maxRelativeError;
expect (std::abs (expectedValue - actualValue) < maxAbsoluteError);
}
};
static MPEValueTests MPEValueUnitTests;
#endif
} // namespace juce

View File

@@ -1,97 +1,103 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class represents a single value for any of the MPE
dimensions of control. It supports values with 7-bit or 14-bit resolutions
(corresponding to 1 or 2 MIDI bytes, respectively). It also offers helper
functions to query the value in a variety of representations that can be
useful in an audio or MIDI context.
@tags{Audio}
*/
class JUCE_API MPEValue
{
public:
//==============================================================================
/** Default constructor.
Constructs an MPEValue corresponding to the centre value.
*/
MPEValue() noexcept;
/** Constructs an MPEValue from an integer between 0 and 127
(using 7-bit precision).
*/
static MPEValue from7BitInt (int value) noexcept;
/** Constructs an MPEValue from an integer between 0 and 16383
(using 14-bit precision).
*/
static MPEValue from14BitInt (int value) noexcept;
/** Constructs an MPEValue corresponding to the centre value. */
static MPEValue centreValue() noexcept;
/** Constructs an MPEValue corresponding to the minimum value. */
static MPEValue minValue() noexcept;
/** Constructs an MPEValue corresponding to the maximum value. */
static MPEValue maxValue() noexcept;
/** Retrieves the current value as an integer between 0 and 127.
Information will be lost if the value was initialised with a precision
higher than 7-bit.
*/
int as7BitInt() const noexcept;
/** Retrieves the current value as an integer between 0 and 16383.
Resolution will be lost if the value was initialised with a precision
higher than 14-bit.
*/
int as14BitInt() const noexcept;
/** Retrieves the current value mapped to a float between -1.0f and 1.0f. */
float asSignedFloat() const noexcept;
/** Retrieves the current value mapped to a float between 0.0f and 1.0f. */
float asUnsignedFloat() const noexcept;
/** Returns true if two values are equal. */
bool operator== (const MPEValue& other) const noexcept;
/** Returns true if two values are not equal. */
bool operator!= (const MPEValue& other) const noexcept;
private:
//==============================================================================
MPEValue (int normalisedValue);
int normalisedValue = 8192;
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class represents a single value for any of the MPE
dimensions of control. It supports values with 7-bit or 14-bit resolutions
(corresponding to 1 or 2 MIDI bytes, respectively). It also offers helper
functions to query the value in a variety of representations that can be
useful in an audio or MIDI context.
@tags{Audio}
*/
class JUCE_API MPEValue
{
public:
//==============================================================================
/** Default constructor.
Constructs an MPEValue corresponding to the centre value.
*/
MPEValue() noexcept;
/** Constructs an MPEValue from an integer between 0 and 127
(using 7-bit precision).
*/
static MPEValue from7BitInt (int value) noexcept;
/** Constructs an MPEValue from an integer between 0 and 16383
(using 14-bit precision).
*/
static MPEValue from14BitInt (int value) noexcept;
/** Constructs an MPEValue from a float between 0.0f and 1.0f. */
static MPEValue fromUnsignedFloat (float value) noexcept;
/** Constructs an MPEValue from a float between -1.0f and 1.0f. */
static MPEValue fromSignedFloat (float value) noexcept;
/** Constructs an MPEValue corresponding to the centre value. */
static MPEValue centreValue() noexcept;
/** Constructs an MPEValue corresponding to the minimum value. */
static MPEValue minValue() noexcept;
/** Constructs an MPEValue corresponding to the maximum value. */
static MPEValue maxValue() noexcept;
/** Retrieves the current value as an integer between 0 and 127.
Information will be lost if the value was initialised with a precision
higher than 7-bit.
*/
int as7BitInt() const noexcept;
/** Retrieves the current value as an integer between 0 and 16383.
Resolution will be lost if the value was initialised with a precision
higher than 14-bit.
*/
int as14BitInt() const noexcept;
/** Retrieves the current value mapped to a float between -1.0f and 1.0f. */
float asSignedFloat() const noexcept;
/** Retrieves the current value mapped to a float between 0.0f and 1.0f. */
float asUnsignedFloat() const noexcept;
/** Returns true if two values are equal. */
bool operator== (const MPEValue& other) const noexcept;
/** Returns true if two values are not equal. */
bool operator!= (const MPEValue& other) const noexcept;
private:
//==============================================================================
MPEValue (int normalisedValue);
int normalisedValue = 8192;
};
} // namespace juce

View File

@@ -1,386 +1,396 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPEZoneLayout::MPEZoneLayout() noexcept {}
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other)
: lowerZone (other.lowerZone),
upperZone (other.upperZone)
{
}
MPEZoneLayout& MPEZoneLayout::operator= (const MPEZoneLayout& other)
{
lowerZone = other.lowerZone;
upperZone = other.upperZone;
sendLayoutChangeMessage();
return *this;
}
void MPEZoneLayout::sendLayoutChangeMessage()
{
listeners.call ([this] (Listener& l) { l.zoneLayoutChanged (*this); });
}
//==============================================================================
void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
checkAndLimitZoneParameters (0, 15, numMemberChannels);
checkAndLimitZoneParameters (0, 96, perNotePitchbendRange);
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
if (isLower)
lowerZone = { true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
else
upperZone = { false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
if (numMemberChannels > 0)
{
auto totalChannels = lowerZone.numMemberChannels + upperZone.numMemberChannels;
if (totalChannels >= 15)
{
if (isLower)
upperZone.numMemberChannels = 14 - numMemberChannels;
else
lowerZone.numMemberChannels = 14 - numMemberChannels;
}
}
sendLayoutChangeMessage();
}
void MPEZoneLayout::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
setZone (true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
}
void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
setZone (false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
}
void MPEZoneLayout::clearAllZones()
{
lowerZone = { true, 0 };
upperZone = { false, 0 };
sendLayoutChangeMessage();
}
//==============================================================================
void MPEZoneLayout::processNextMidiEvent (const MidiMessage& message)
{
if (! message.isController())
return;
MidiRPNMessage rpn;
if (rpnDetector.parseControllerMessage (message.getChannel(),
message.getControllerNumber(),
message.getControllerValue(),
rpn))
{
processRpnMessage (rpn);
}
}
void MPEZoneLayout::processRpnMessage (MidiRPNMessage rpn)
{
if (rpn.parameterNumber == MPEMessages::zoneLayoutMessagesRpnNumber)
processZoneLayoutRpnMessage (rpn);
else if (rpn.parameterNumber == 0)
processPitchbendRangeRpnMessage (rpn);
}
void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn)
{
if (rpn.value < 16)
{
if (rpn.channel == 1)
setLowerZone (rpn.value);
else if (rpn.channel == 16)
setUpperZone (rpn.value);
}
}
void MPEZoneLayout::updateMasterPitchbend (Zone& zone, int value)
{
if (zone.masterPitchbendRange != value)
{
checkAndLimitZoneParameters (0, 96, zone.masterPitchbendRange);
zone.masterPitchbendRange = value;
sendLayoutChangeMessage();
}
}
void MPEZoneLayout::updatePerNotePitchbendRange (Zone& zone, int value)
{
if (zone.perNotePitchbendRange != value)
{
checkAndLimitZoneParameters (0, 96, zone.perNotePitchbendRange);
zone.perNotePitchbendRange = value;
sendLayoutChangeMessage();
}
}
void MPEZoneLayout::processPitchbendRangeRpnMessage (MidiRPNMessage rpn)
{
if (rpn.channel == 1)
{
updateMasterPitchbend (lowerZone, rpn.value);
}
else if (rpn.channel == 16)
{
updateMasterPitchbend (upperZone, rpn.value);
}
else
{
if (lowerZone.isUsingChannelAsMemberChannel (rpn.channel))
updatePerNotePitchbendRange (lowerZone, rpn.value);
else if (upperZone.isUsingChannelAsMemberChannel (rpn.channel))
updatePerNotePitchbendRange (upperZone, rpn.value);
}
}
void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer)
{
for (const auto metadata : buffer)
processNextMidiEvent (metadata.getMessage());
}
//==============================================================================
void MPEZoneLayout::addListener (Listener* const listenerToAdd) noexcept
{
listeners.add (listenerToAdd);
}
void MPEZoneLayout::removeListener (Listener* const listenerToRemove) noexcept
{
listeners.remove (listenerToRemove);
}
//==============================================================================
void MPEZoneLayout::checkAndLimitZoneParameters (int minValue, int maxValue,
int& valueToCheckAndLimit) noexcept
{
if (valueToCheckAndLimit < minValue || valueToCheckAndLimit > maxValue)
{
// if you hit this, one of the parameters you supplied for this zone
// was not within the allowed range!
// we fit this back into the allowed range here to maintain a valid
// state for the zone, but probably the resulting zone is not what you
// wanted it to be!
jassertfalse;
valueToCheckAndLimit = jlimit (minValue, maxValue, valueToCheckAndLimit);
}
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPEZoneLayoutTests : public UnitTest
{
public:
MPEZoneLayoutTests()
: UnitTest ("MPEZoneLayout class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("initialisation");
{
MPEZoneLayout layout;
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("adding zones");
{
MPEZoneLayout layout;
layout.setLowerZone (7);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
layout.setUpperZone (7);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
layout.setLowerZone (3);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
layout.setUpperZone (3);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 3);
layout.setLowerZone (15);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 15);
}
beginTest ("clear all zones");
{
MPEZoneLayout layout;
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
layout.setLowerZone (7);
layout.setUpperZone (2);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
layout.clearAllZones();
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("process MIDI buffers");
{
MPEZoneLayout layout;
MidiBuffer buffer;
buffer = MPEMessages::setLowerZone (7);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
buffer = MPEMessages::setUpperZone (7);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
{
buffer = MPEMessages::setLowerZone (10);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 10);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 4);
buffer = MPEMessages::setLowerZone (10, 33, 44);
layout.processNextMidiBuffer (buffer);
expectEquals (layout.getLowerZone().numMemberChannels, 10);
expectEquals (layout.getLowerZone().perNotePitchbendRange, 33);
expectEquals (layout.getLowerZone().masterPitchbendRange, 44);
}
{
buffer = MPEMessages::setUpperZone (10);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 4);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 10);
buffer = MPEMessages::setUpperZone (10, 33, 44);
layout.processNextMidiBuffer (buffer);
expectEquals (layout.getUpperZone().numMemberChannels, 10);
expectEquals (layout.getUpperZone().perNotePitchbendRange, 33);
expectEquals (layout.getUpperZone().masterPitchbendRange, 44);
}
buffer = MPEMessages::clearAllZones();
layout.processNextMidiBuffer (buffer);
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("process individual MIDI messages");
{
MPEZoneLayout layout;
layout.processNextMidiEvent ({ 0x80, 0x59, 0xd0 }); // unrelated note-off msg
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x06 }); // RPN part 1
layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // RPN part 2
layout.processNextMidiEvent ({ 0xb8, 0x0b, 0x66 }); // unrelated CC msg
layout.processNextMidiEvent ({ 0xb0, 0x06, 0x03 }); // RPN part 3
layout.processNextMidiEvent ({ 0x90, 0x60, 0x00 }); // unrelated note-on msg
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getLowerZone().perNotePitchbendRange, 48);
expectEquals (layout.getLowerZone().masterPitchbendRange, 2);
}
}
};
static MPEZoneLayoutTests MPEZoneLayoutUnitTests;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MPEZoneLayout::MPEZoneLayout (MPEZone lower, MPEZone upper)
: lowerZone (lower), upperZone (upper)
{
}
MPEZoneLayout::MPEZoneLayout (MPEZone zone)
: lowerZone (zone.isLowerZone() ? zone : MPEZone()),
upperZone (! zone.isLowerZone() ? zone : MPEZone())
{
}
MPEZoneLayout::MPEZoneLayout (const MPEZoneLayout& other)
: lowerZone (other.lowerZone),
upperZone (other.upperZone)
{
}
MPEZoneLayout& MPEZoneLayout::operator= (const MPEZoneLayout& other)
{
lowerZone = other.lowerZone;
upperZone = other.upperZone;
sendLayoutChangeMessage();
return *this;
}
void MPEZoneLayout::sendLayoutChangeMessage()
{
listeners.call ([this] (Listener& l) { l.zoneLayoutChanged (*this); });
}
//==============================================================================
void MPEZoneLayout::setZone (bool isLower, int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
checkAndLimitZoneParameters (0, 15, numMemberChannels);
checkAndLimitZoneParameters (0, 96, perNotePitchbendRange);
checkAndLimitZoneParameters (0, 96, masterPitchbendRange);
if (isLower)
lowerZone = { MPEZone::Type::lower, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
else
upperZone = { MPEZone::Type::upper, numMemberChannels, perNotePitchbendRange, masterPitchbendRange };
if (numMemberChannels > 0)
{
auto totalChannels = lowerZone.numMemberChannels + upperZone.numMemberChannels;
if (totalChannels >= 15)
{
if (isLower)
upperZone.numMemberChannels = 14 - numMemberChannels;
else
lowerZone.numMemberChannels = 14 - numMemberChannels;
}
}
sendLayoutChangeMessage();
}
void MPEZoneLayout::setLowerZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
setZone (true, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
}
void MPEZoneLayout::setUpperZone (int numMemberChannels, int perNotePitchbendRange, int masterPitchbendRange) noexcept
{
setZone (false, numMemberChannels, perNotePitchbendRange, masterPitchbendRange);
}
void MPEZoneLayout::clearAllZones()
{
lowerZone = { MPEZone::Type::lower, 0 };
upperZone = { MPEZone::Type::upper, 0 };
sendLayoutChangeMessage();
}
//==============================================================================
void MPEZoneLayout::processNextMidiEvent (const MidiMessage& message)
{
if (! message.isController())
return;
MidiRPNMessage rpn;
if (rpnDetector.parseControllerMessage (message.getChannel(),
message.getControllerNumber(),
message.getControllerValue(),
rpn))
{
processRpnMessage (rpn);
}
}
void MPEZoneLayout::processRpnMessage (MidiRPNMessage rpn)
{
if (rpn.parameterNumber == MPEMessages::zoneLayoutMessagesRpnNumber)
processZoneLayoutRpnMessage (rpn);
else if (rpn.parameterNumber == 0)
processPitchbendRangeRpnMessage (rpn);
}
void MPEZoneLayout::processZoneLayoutRpnMessage (MidiRPNMessage rpn)
{
if (rpn.value < 16)
{
if (rpn.channel == 1)
setLowerZone (rpn.value);
else if (rpn.channel == 16)
setUpperZone (rpn.value);
}
}
void MPEZoneLayout::updateMasterPitchbend (MPEZone& zone, int value)
{
if (zone.masterPitchbendRange != value)
{
checkAndLimitZoneParameters (0, 96, zone.masterPitchbendRange);
zone.masterPitchbendRange = value;
sendLayoutChangeMessage();
}
}
void MPEZoneLayout::updatePerNotePitchbendRange (MPEZone& zone, int value)
{
if (zone.perNotePitchbendRange != value)
{
checkAndLimitZoneParameters (0, 96, zone.perNotePitchbendRange);
zone.perNotePitchbendRange = value;
sendLayoutChangeMessage();
}
}
void MPEZoneLayout::processPitchbendRangeRpnMessage (MidiRPNMessage rpn)
{
if (rpn.channel == 1)
{
updateMasterPitchbend (lowerZone, rpn.value);
}
else if (rpn.channel == 16)
{
updateMasterPitchbend (upperZone, rpn.value);
}
else
{
if (lowerZone.isUsingChannelAsMemberChannel (rpn.channel))
updatePerNotePitchbendRange (lowerZone, rpn.value);
else if (upperZone.isUsingChannelAsMemberChannel (rpn.channel))
updatePerNotePitchbendRange (upperZone, rpn.value);
}
}
void MPEZoneLayout::processNextMidiBuffer (const MidiBuffer& buffer)
{
for (const auto metadata : buffer)
processNextMidiEvent (metadata.getMessage());
}
//==============================================================================
void MPEZoneLayout::addListener (Listener* const listenerToAdd) noexcept
{
listeners.add (listenerToAdd);
}
void MPEZoneLayout::removeListener (Listener* const listenerToRemove) noexcept
{
listeners.remove (listenerToRemove);
}
//==============================================================================
void MPEZoneLayout::checkAndLimitZoneParameters (int minValue, int maxValue,
int& valueToCheckAndLimit) noexcept
{
if (valueToCheckAndLimit < minValue || valueToCheckAndLimit > maxValue)
{
// if you hit this, one of the parameters you supplied for this zone
// was not within the allowed range!
// we fit this back into the allowed range here to maintain a valid
// state for the zone, but probably the resulting zone is not what you
// wanted it to be!
jassertfalse;
valueToCheckAndLimit = jlimit (minValue, maxValue, valueToCheckAndLimit);
}
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
class MPEZoneLayoutTests : public UnitTest
{
public:
MPEZoneLayoutTests()
: UnitTest ("MPEZoneLayout class", UnitTestCategories::midi)
{}
void runTest() override
{
beginTest ("initialisation");
{
MPEZoneLayout layout;
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("adding zones");
{
MPEZoneLayout layout;
layout.setLowerZone (7);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
layout.setUpperZone (7);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
layout.setLowerZone (3);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
layout.setUpperZone (3);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 3);
layout.setLowerZone (15);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 15);
}
beginTest ("clear all zones");
{
MPEZoneLayout layout;
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
layout.setLowerZone (7);
layout.setUpperZone (2);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
layout.clearAllZones();
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("process MIDI buffers");
{
MPEZoneLayout layout;
MidiBuffer buffer;
buffer = MPEMessages::setLowerZone (7);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
buffer = MPEMessages::setUpperZone (7);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 7);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 7);
{
buffer = MPEMessages::setLowerZone (10);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 10);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 4);
buffer = MPEMessages::setLowerZone (10, 33, 44);
layout.processNextMidiBuffer (buffer);
expectEquals (layout.getLowerZone().numMemberChannels, 10);
expectEquals (layout.getLowerZone().perNotePitchbendRange, 33);
expectEquals (layout.getLowerZone().masterPitchbendRange, 44);
}
{
buffer = MPEMessages::setUpperZone (10);
layout.processNextMidiBuffer (buffer);
expect (layout.getLowerZone().isActive());
expect (layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 4);
expectEquals (layout.getUpperZone().getMasterChannel(), 16);
expectEquals (layout.getUpperZone().numMemberChannels, 10);
buffer = MPEMessages::setUpperZone (10, 33, 44);
layout.processNextMidiBuffer (buffer);
expectEquals (layout.getUpperZone().numMemberChannels, 10);
expectEquals (layout.getUpperZone().perNotePitchbendRange, 33);
expectEquals (layout.getUpperZone().masterPitchbendRange, 44);
}
buffer = MPEMessages::clearAllZones();
layout.processNextMidiBuffer (buffer);
expect (! layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
}
beginTest ("process individual MIDI messages");
{
MPEZoneLayout layout;
layout.processNextMidiEvent ({ 0x80, 0x59, 0xd0 }); // unrelated note-off msg
layout.processNextMidiEvent ({ 0xb0, 0x64, 0x06 }); // RPN part 1
layout.processNextMidiEvent ({ 0xb0, 0x65, 0x00 }); // RPN part 2
layout.processNextMidiEvent ({ 0xb8, 0x0b, 0x66 }); // unrelated CC msg
layout.processNextMidiEvent ({ 0xb0, 0x06, 0x03 }); // RPN part 3
layout.processNextMidiEvent ({ 0x90, 0x60, 0x00 }); // unrelated note-on msg
expect (layout.getLowerZone().isActive());
expect (! layout.getUpperZone().isActive());
expectEquals (layout.getLowerZone().getMasterChannel(), 1);
expectEquals (layout.getLowerZone().numMemberChannels, 3);
expectEquals (layout.getLowerZone().perNotePitchbendRange, 48);
expectEquals (layout.getLowerZone().masterPitchbendRange, 2);
}
}
};
static MPEZoneLayoutTests MPEZoneLayoutUnitTests;
#endif
} // namespace juce

View File

@@ -1,225 +1,241 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This class represents the current MPE zone layout of a device capable of handling MPE.
An MPE device can have up to two zones: a lower zone with master channel 1 and
allocated MIDI channels increasing from channel 2, and an upper zone with master
channel 16 and allocated MIDI channels decreasing from channel 15. MPE mode is
enabled on a device when one of these zones is active and disabled when both
are inactive.
Use the MPEMessages helper class to convert the zone layout represented
by this object to MIDI message sequences that you can send to an Expressive
MIDI device to set its zone layout, add zones etc.
@see MPEInstrument
@tags{Audio}
*/
class JUCE_API MPEZoneLayout
{
public:
/** Default constructor.
This will create a layout with inactive lower and upper zones, representing
a device with MPE mode disabled.
You can set the lower or upper MPE zones using the setZone() method.
@see setZone
*/
MPEZoneLayout() noexcept;
/** Copy constuctor.
This will not copy the listeners registered to the MPEZoneLayout.
*/
MPEZoneLayout (const MPEZoneLayout& other);
/** Copy assignment operator.
This will not copy the listeners registered to the MPEZoneLayout.
*/
MPEZoneLayout& operator= (const MPEZoneLayout& other);
//==============================================================================
/**
This struct represents an MPE zone.
It can either be a lower or an upper zone, where:
- A lower zone encompasses master channel 1 and an arbitrary number of ascending
MIDI channels, increasing from channel 2.
- An upper zone encompasses master channel 16 and an arbitrary number of descending
MIDI channels, decreasing from channel 15.
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
master pitchbends, respectively.
*/
struct Zone
{
Zone (const Zone& other) = default;
bool isLowerZone() const noexcept { return lowerZone; }
bool isUpperZone() const noexcept { return ! lowerZone; }
bool isActive() const noexcept { return numMemberChannels > 0; }
int getMasterChannel() const noexcept { return lowerZone ? 1 : 16; }
int getFirstMemberChannel() const noexcept { return lowerZone ? 2 : 15; }
int getLastMemberChannel() const noexcept { return lowerZone ? (1 + numMemberChannels)
: (16 - numMemberChannels); }
bool isUsingChannelAsMemberChannel (int channel) const noexcept
{
return lowerZone ? (channel > 1 && channel <= 1 + numMemberChannels)
: (channel < 16 && channel >= 16 - numMemberChannels);
}
bool isUsing (int channel) const noexcept
{
return isUsingChannelAsMemberChannel (channel) || channel == getMasterChannel();
}
bool operator== (const Zone& other) const noexcept { return lowerZone == other.lowerZone
&& numMemberChannels == other.numMemberChannels
&& perNotePitchbendRange == other.perNotePitchbendRange
&& masterPitchbendRange == other.masterPitchbendRange; }
bool operator!= (const Zone& other) const noexcept { return ! operator== (other); }
int numMemberChannels;
int perNotePitchbendRange;
int masterPitchbendRange;
private:
friend class MPEZoneLayout;
Zone (bool lower, int memberChans = 0, int perNotePb = 48, int masterPb = 2) noexcept
: numMemberChannels (memberChans),
perNotePitchbendRange (perNotePb),
masterPitchbendRange (masterPb),
lowerZone (lower)
{
}
bool lowerZone;
};
/** Sets the lower zone of this layout. */
void setLowerZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2) noexcept;
/** Sets the upper zone of this layout. */
void setUpperZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2) noexcept;
/** Returns a struct representing the lower MPE zone. */
const Zone getLowerZone() const noexcept { return lowerZone; }
/** Returns a struct representing the upper MPE zone. */
const Zone getUpperZone() const noexcept { return upperZone; }
/** Clears the lower and upper zones of this layout, making them both inactive
and disabling MPE mode.
*/
void clearAllZones();
//==============================================================================
/** Pass incoming MIDI messages to an object of this class if you want the
zone layout to properly react to MPE RPN messages like an
MPE device.
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
set the per-note or master pitchbend ranges.
Any other MIDI messages will be ignored by this class.
@see MPEMessages
*/
void processNextMidiEvent (const MidiMessage& message);
/** Pass incoming MIDI buffers to an object of this class if you want the
zone layout to properly react to MPE RPN messages like an
MPE device.
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
set the per-note or master pitchbend ranges.
Any other MIDI messages will be ignored by this class.
@see MPEMessages
*/
void processNextMidiBuffer (const MidiBuffer& buffer);
//==============================================================================
/** Listener class. Derive from this class to allow your class to be
notified about changes to the zone layout.
*/
class Listener
{
public:
/** Destructor. */
virtual ~Listener() = default;
/** Implement this callback to be notified about any changes to this
MPEZoneLayout. Will be called whenever a zone is added, zones are
removed, or any zone's master or note pitchbend ranges change.
*/
virtual void zoneLayoutChanged (const MPEZoneLayout& layout) = 0;
};
//==============================================================================
/** Adds a listener. */
void addListener (Listener* const listenerToAdd) noexcept;
/** Removes a listener. */
void removeListener (Listener* const listenerToRemove) noexcept;
private:
//==============================================================================
Zone lowerZone { true, 0 };
Zone upperZone { false, 0 };
MidiRPNDetector rpnDetector;
ListenerList<Listener> listeners;
//==============================================================================
void setZone (bool, int, int, int) noexcept;
void processRpnMessage (MidiRPNMessage);
void processZoneLayoutRpnMessage (MidiRPNMessage);
void processPitchbendRangeRpnMessage (MidiRPNMessage);
void updateMasterPitchbend (Zone&, int);
void updatePerNotePitchbendRange (Zone&, int);
void sendLayoutChangeMessage();
void checkAndLimitZoneParameters (int, int, int&) noexcept;
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
This struct represents an MPE zone.
It can either be a lower or an upper zone, where:
- A lower zone encompasses master channel 1 and an arbitrary number of ascending
MIDI channels, increasing from channel 2.
- An upper zone encompasses master channel 16 and an arbitrary number of descending
MIDI channels, decreasing from channel 15.
It also defines a pitchbend range (in semitones) to be applied for per-note pitchbends and
master pitchbends, respectively.
*/
struct MPEZone
{
enum class Type { lower, upper };
MPEZone() = default;
MPEZone (Type type, int memberChannels = 0, int perNotePitchbend = 48, int masterPitchbend = 2)
: zoneType (type),
numMemberChannels (memberChannels),
perNotePitchbendRange (perNotePitchbend),
masterPitchbendRange (masterPitchbend)
{}
bool isLowerZone() const noexcept { return zoneType == Type::lower; }
bool isUpperZone() const noexcept { return zoneType == Type::upper; }
bool isActive() const noexcept { return numMemberChannels > 0; }
int getMasterChannel() const noexcept { return isLowerZone() ? lowerZoneMasterChannel : upperZoneMasterChannel; }
int getFirstMemberChannel() const noexcept { return isLowerZone() ? lowerZoneMasterChannel + 1 : upperZoneMasterChannel - 1; }
int getLastMemberChannel() const noexcept { return isLowerZone() ? (lowerZoneMasterChannel + numMemberChannels)
: (upperZoneMasterChannel - numMemberChannels); }
bool isUsingChannelAsMemberChannel (int channel) const noexcept
{
return isLowerZone() ? (lowerZoneMasterChannel < channel && channel <= getLastMemberChannel())
: (channel < upperZoneMasterChannel && getLastMemberChannel() <= channel);
}
bool isUsing (int channel) const noexcept
{
return isUsingChannelAsMemberChannel (channel) || channel == getMasterChannel();
}
static auto tie (const MPEZone& z)
{
return std::tie (z.zoneType,
z.numMemberChannels,
z.perNotePitchbendRange,
z.masterPitchbendRange);
}
bool operator== (const MPEZone& other) const
{
return tie (*this) == tie (other);
}
bool operator!= (const MPEZone& other) const
{
return tie (*this) != tie (other);
}
//==============================================================================
static constexpr int lowerZoneMasterChannel = 1,
upperZoneMasterChannel = 16;
Type zoneType = Type::lower;
int numMemberChannels = 0;
int perNotePitchbendRange = 48;
int masterPitchbendRange = 2;
};
//==============================================================================
/**
This class represents the current MPE zone layout of a device capable of handling MPE.
An MPE device can have up to two zones: a lower zone with master channel 1 and
allocated MIDI channels increasing from channel 2, and an upper zone with master
channel 16 and allocated MIDI channels decreasing from channel 15. MPE mode is
enabled on a device when one of these zones is active and disabled when both
are inactive.
Use the MPEMessages helper class to convert the zone layout represented
by this object to MIDI message sequences that you can send to an Expressive
MIDI device to set its zone layout, add zones etc.
@see MPEInstrument
@tags{Audio}
*/
class JUCE_API MPEZoneLayout
{
public:
//==============================================================================
/** Creates a layout with inactive upper and lower zones. */
MPEZoneLayout() = default;
/** Creates a layout with the given upper and lower zones. */
MPEZoneLayout (MPEZone lower, MPEZone upper);
/** Creates a layout with a single upper or lower zone, leaving the other zone uninitialised. */
MPEZoneLayout (MPEZone singleZone);
MPEZoneLayout (const MPEZoneLayout& other);
MPEZoneLayout& operator= (const MPEZoneLayout& other);
bool operator== (const MPEZoneLayout& other) const { return lowerZone == other.lowerZone && upperZone == other.upperZone; }
bool operator!= (const MPEZoneLayout& other) const { return ! operator== (other); }
//==============================================================================
/** Returns a struct representing the lower MPE zone. */
MPEZone getLowerZone() const noexcept { return lowerZone; }
/** Returns a struct representing the upper MPE zone. */
MPEZone getUpperZone() const noexcept { return upperZone; }
/** Sets the lower zone of this layout. */
void setLowerZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2) noexcept;
/** Sets the upper zone of this layout. */
void setUpperZone (int numMemberChannels = 0,
int perNotePitchbendRange = 48,
int masterPitchbendRange = 2) noexcept;
/** Clears the lower and upper zones of this layout, making them both inactive
and disabling MPE mode.
*/
void clearAllZones();
/** Returns true if either of the zones are active. */
bool isActive() const { return lowerZone.isActive() || upperZone.isActive(); }
//==============================================================================
/** Pass incoming MIDI messages to an object of this class if you want the
zone layout to properly react to MPE RPN messages like an
MPE device.
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
set the per-note or master pitchbend ranges.
Any other MIDI messages will be ignored by this class.
@see MPEMessages
*/
void processNextMidiEvent (const MidiMessage& message);
/** Pass incoming MIDI buffers to an object of this class if you want the
zone layout to properly react to MPE RPN messages like an
MPE device.
MPEMessages::rpnNumber will add or remove zones; RPN 0 will
set the per-note or master pitchbend ranges.
Any other MIDI messages will be ignored by this class.
@see MPEMessages
*/
void processNextMidiBuffer (const MidiBuffer& buffer);
//==============================================================================
/** Listener class. Derive from this class to allow your class to be
notified about changes to the zone layout.
*/
class Listener
{
public:
/** Destructor. */
virtual ~Listener() = default;
/** Implement this callback to be notified about any changes to this
MPEZoneLayout. Will be called whenever a zone is added, zones are
removed, or any zone's master or note pitchbend ranges change.
*/
virtual void zoneLayoutChanged (const MPEZoneLayout& layout) = 0;
};
//==============================================================================
/** Adds a listener. */
void addListener (Listener* const listenerToAdd) noexcept;
/** Removes a listener. */
void removeListener (Listener* const listenerToRemove) noexcept;
#ifndef DOXYGEN
using Zone = MPEZone;
#endif
private:
//==============================================================================
MPEZone lowerZone { MPEZone::Type::lower, 0 };
MPEZone upperZone { MPEZone::Type::upper, 0 };
MidiRPNDetector rpnDetector;
ListenerList<Listener> listeners;
//==============================================================================
void setZone (bool, int, int, int) noexcept;
void processRpnMessage (MidiRPNMessage);
void processZoneLayoutRpnMessage (MidiRPNMessage);
void processPitchbendRangeRpnMessage (MidiRPNMessage);
void updateMasterPitchbend (MPEZone&, int);
void updatePerNotePitchbendRange (MPEZone&, int);
void sendLayoutChangeMessage();
void checkAndLimitZoneParameters (int, int, int&) noexcept;
};
} // namespace juce

View File

@@ -1,330 +1,344 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
#if ! defined (DOXYGEN) && (JUCE_MAC || JUCE_IOS)
struct CoreAudioLayouts
{
//==============================================================================
enum
{
coreAudioHOASN3DLayoutTag = (190U<<16) | 0 // kAudioChannelLayoutTag_HOA_ACN_SN3D
};
//==============================================================================
/** Convert CoreAudio's native AudioChannelLayout to JUCE's AudioChannelSet.
Note that this method cannot preserve the order of channels.
*/
static AudioChannelSet fromCoreAudio (const AudioChannelLayout& layout)
{
return AudioChannelSet::channelSetWithChannels (getCoreAudioLayoutChannels (layout));
}
/** Convert CoreAudio's native AudioChannelLayoutTag to JUCE's AudioChannelSet.
Note that this method cannot preserve the order of channels.
*/
static AudioChannelSet fromCoreAudio (AudioChannelLayoutTag layoutTag)
{
return AudioChannelSet::channelSetWithChannels (getSpeakerLayoutForCoreAudioTag (layoutTag));
}
/** Convert JUCE's AudioChannelSet to CoreAudio's AudioChannelLayoutTag.
Note that this method cannot preserve the order of channels.
*/
static AudioChannelLayoutTag toCoreAudio (const AudioChannelSet& set)
{
if (set.getAmbisonicOrder() >= 0)
return coreAudioHOASN3DLayoutTag | static_cast<unsigned> (set.size());
for (auto* tbl = SpeakerLayoutTable::get(); tbl->tag != 0; ++tbl)
{
AudioChannelSet caSet;
for (int i = 0; i < numElementsInArray (tbl->channelTypes)
&& tbl->channelTypes[i] != AudioChannelSet::unknown; ++i)
caSet.addChannel (tbl->channelTypes[i]);
if (caSet == set)
return tbl->tag;
}
return kAudioChannelLayoutTag_DiscreteInOrder | static_cast<AudioChannelLayoutTag> (set.size());
}
static const Array<AudioChannelLayoutTag>& getKnownCoreAudioTags()
{
static Array<AudioChannelLayoutTag> tags (createKnownCoreAudioTags());
return tags;
}
//==============================================================================
/** Convert CoreAudio's native AudioChannelLayout to an array of JUCE ChannelTypes. */
static Array<AudioChannelSet::ChannelType> getCoreAudioLayoutChannels (const AudioChannelLayout& layout)
{
switch (layout.mChannelLayoutTag & 0xffff0000)
{
case kAudioChannelLayoutTag_UseChannelBitmap:
return AudioChannelSet::fromWaveChannelMask (static_cast<int> (layout.mChannelBitmap)).getChannelTypes();
case kAudioChannelLayoutTag_UseChannelDescriptions:
{
Array<AudioChannelSet::ChannelType> channels;
for (UInt32 i = 0; i < layout.mNumberChannelDescriptions; ++i)
channels.addIfNotAlreadyThere (getChannelTypeFromAudioChannelLabel (layout.mChannelDescriptions[i].mChannelLabel));
// different speaker mappings may point to the same JUCE speaker so fill up
// this array with discrete channels
for (int j = 0; channels.size() < static_cast<int> (layout.mNumberChannelDescriptions); ++j)
channels.addIfNotAlreadyThere (static_cast<AudioChannelSet::ChannelType> (AudioChannelSet::discreteChannel0 + j));
return channels;
}
case kAudioChannelLayoutTag_DiscreteInOrder:
return AudioChannelSet::discreteChannels (static_cast<int> (layout.mChannelLayoutTag) & 0xffff).getChannelTypes();
default:
break;
}
return getSpeakerLayoutForCoreAudioTag (layout.mChannelLayoutTag);
}
static Array<AudioChannelSet::ChannelType> getSpeakerLayoutForCoreAudioTag (AudioChannelLayoutTag tag)
{
// You need to specify the full AudioChannelLayout when using
// the UseChannelBitmap and UseChannelDescriptions layout tag
jassert (tag != kAudioChannelLayoutTag_UseChannelBitmap && tag != kAudioChannelLayoutTag_UseChannelDescriptions);
Array<AudioChannelSet::ChannelType> speakers;
for (auto* tbl = SpeakerLayoutTable::get(); tbl->tag != 0; ++tbl)
{
if (tag == tbl->tag)
{
for (int i = 0; i < numElementsInArray (tbl->channelTypes)
&& tbl->channelTypes[i] != AudioChannelSet::unknown; ++i)
speakers.add (tbl->channelTypes[i]);
return speakers;
}
}
auto numChannels = tag & 0xffff;
if (tag >= coreAudioHOASN3DLayoutTag && tag <= (coreAudioHOASN3DLayoutTag | 0xffff))
{
auto sqrtMinusOne = std::sqrt (static_cast<float> (numChannels)) - 1.0f;
auto ambisonicOrder = jmax (0, static_cast<int> (std::floor (sqrtMinusOne)));
if (static_cast<float> (ambisonicOrder) == sqrtMinusOne)
return AudioChannelSet::ambisonic (ambisonicOrder).getChannelTypes();
}
for (UInt32 i = 0; i < numChannels; ++i)
speakers.add (static_cast<AudioChannelSet::ChannelType> (AudioChannelSet::discreteChannel0 + i));
return speakers;
}
private:
//==============================================================================
struct LayoutTagSpeakerList
{
AudioChannelLayoutTag tag;
AudioChannelSet::ChannelType channelTypes[16];
};
static Array<AudioChannelLayoutTag> createKnownCoreAudioTags()
{
Array<AudioChannelLayoutTag> tags;
for (auto* tbl = SpeakerLayoutTable::get(); tbl->tag != 0; ++tbl)
tags.addIfNotAlreadyThere (tbl->tag);
for (unsigned order = 0; order <= 5; ++order)
tags.addIfNotAlreadyThere (coreAudioHOASN3DLayoutTag | ((order + 1) * (order + 1)));
return tags;
}
//==============================================================================
// This list has been derived from https://pastebin.com/24dQ4BPJ
// Apple channel labels have been replaced by JUCE channel names
// This means that some layouts will be identical in JUCE but not in CoreAudio
// In Apple's official definition the following tags exist with the same speaker layout and order
// even when *not* represented in JUCE channels
// kAudioChannelLayoutTag_Binaural = kAudioChannelLayoutTag_Stereo
// kAudioChannelLayoutTag_MPEG_5_0_B = kAudioChannelLayoutTag_Pentagonal
// kAudioChannelLayoutTag_ITU_2_2 = kAudioChannelLayoutTag_Quadraphonic
// kAudioChannelLayoutTag_AudioUnit_6_0 = kAudioChannelLayoutTag_Hexagonal
struct SpeakerLayoutTable : AudioChannelSet // save us some typing
{
static LayoutTagSpeakerList* get() noexcept
{
static LayoutTagSpeakerList tbl[] = {
// list layouts for which there is a corresponding named AudioChannelSet first
{ kAudioChannelLayoutTag_Mono, { centre } },
{ kAudioChannelLayoutTag_Stereo, { left, right } },
{ kAudioChannelLayoutTag_MPEG_3_0_A, { left, right, centre } },
{ kAudioChannelLayoutTag_ITU_2_1, { left, right, centreSurround } },
{ kAudioChannelLayoutTag_MPEG_4_0_A, { left, right, centre, centreSurround } },
{ kAudioChannelLayoutTag_MPEG_5_0_A, { left, right, centre, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_MPEG_5_1_A, { left, right, centre, LFE, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_AudioUnit_6_0, { left, right, leftSurround, rightSurround, centre, centreSurround } },
{ kAudioChannelLayoutTag_MPEG_6_1_A, { left, right, centre, LFE, leftSurround, rightSurround, centreSurround } },
{ kAudioChannelLayoutTag_DTS_6_0_A, { leftSurroundSide, rightSurroundSide, left, right, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_DTS_6_1_A, { leftSurroundSide, rightSurroundSide, left, right, leftSurround, rightSurround, LFE } },
{ kAudioChannelLayoutTag_AudioUnit_7_0, { left, right, leftSurroundSide, rightSurroundSide, centre, leftSurroundRear, rightSurroundRear } },
{ kAudioChannelLayoutTag_AudioUnit_7_0_Front, { left, right, leftSurround, rightSurround, centre, leftCentre, rightCentre } },
{ kAudioChannelLayoutTag_MPEG_7_1_C, { left, right, centre, LFE, leftSurroundSide, rightSurroundSide, leftSurroundRear, rightSurroundRear } },
{ kAudioChannelLayoutTag_MPEG_7_1_A, { left, right, centre, LFE, leftSurround, rightSurround, leftCentre, rightCentre } },
{ kAudioChannelLayoutTag_Ambisonic_B_Format, { ambisonicW, ambisonicX, ambisonicY, ambisonicZ } },
{ kAudioChannelLayoutTag_Quadraphonic, { left, right, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_Pentagonal, { left, right, leftSurroundRear, rightSurroundRear, centre } },
{ kAudioChannelLayoutTag_Hexagonal, { left, right, leftSurroundRear, rightSurroundRear, centre, centreSurround } },
{ kAudioChannelLayoutTag_Octagonal, { left, right, leftSurround, rightSurround, centre, centreSurround, wideLeft, wideRight } },
// more uncommon layouts
{ kAudioChannelLayoutTag_StereoHeadphones, { left, right } },
{ kAudioChannelLayoutTag_MatrixStereo, { left, right } },
{ kAudioChannelLayoutTag_MidSide, { centre, discreteChannel0 } },
{ kAudioChannelLayoutTag_XY, { ambisonicX, ambisonicY } },
{ kAudioChannelLayoutTag_Binaural, { left, right } },
{ kAudioChannelLayoutTag_Cube, { left, right, leftSurround, rightSurround, topFrontLeft, topFrontRight, topRearLeft, topRearRight } },
{ kAudioChannelLayoutTag_MPEG_3_0_B, { centre, left, right } },
{ kAudioChannelLayoutTag_MPEG_4_0_B, { centre, left, right, centreSurround } },
{ kAudioChannelLayoutTag_MPEG_5_0_B, { left, right, leftSurround, rightSurround, centre } },
{ kAudioChannelLayoutTag_MPEG_5_0_C, { left, centre, right, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_MPEG_5_0_D, { centre, left, right, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_MPEG_5_1_B, { left, right, leftSurround, rightSurround, centre, LFE } },
{ kAudioChannelLayoutTag_MPEG_5_1_C, { left, centre, right, leftSurround, rightSurround, LFE } },
{ kAudioChannelLayoutTag_MPEG_5_1_D, { centre, left, right, leftSurround, rightSurround, LFE } },
{ kAudioChannelLayoutTag_MPEG_7_1_B, { centre, leftCentre, rightCentre, left, right, leftSurround, rightSurround, LFE } },
{ kAudioChannelLayoutTag_Emagic_Default_7_1, { left, right, leftSurround, rightSurround, centre, LFE, leftCentre, rightCentre } },
{ kAudioChannelLayoutTag_SMPTE_DTV, { left, right, centre, LFE, leftSurround, rightSurround, discreteChannel0 /* leftMatrixTotal */, (ChannelType) (discreteChannel0 + 1) /* rightMatrixTotal */} },
{ kAudioChannelLayoutTag_ITU_2_2, { left, right, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_DVD_4, { left, right, LFE } },
{ kAudioChannelLayoutTag_DVD_5, { left, right, LFE, centreSurround } },
{ kAudioChannelLayoutTag_DVD_6, { left, right, LFE, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_DVD_10, { left, right, centre, LFE } },
{ kAudioChannelLayoutTag_DVD_11, { left, right, centre, LFE, centreSurround } },
{ kAudioChannelLayoutTag_DVD_18, { left, right, leftSurround, rightSurround, LFE } },
{ kAudioChannelLayoutTag_AAC_6_0, { centre, left, right, leftSurround, rightSurround, centreSurround } },
{ kAudioChannelLayoutTag_AAC_6_1, { centre, left, right, leftSurround, rightSurround, centreSurround, LFE } },
{ kAudioChannelLayoutTag_AAC_7_0, { centre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear } },
{ kAudioChannelLayoutTag_AAC_7_1_B, { centre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear, LFE } },
{ kAudioChannelLayoutTag_AAC_7_1_C, { centre, left, right, leftSurround, rightSurround, LFE, topFrontLeft, topFrontRight } },
{ kAudioChannelLayoutTag_AAC_Octagonal, { centre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear, centreSurround } },
{ kAudioChannelLayoutTag_TMH_10_2_std, { left, right, centre, topFrontCentre, leftSurroundSide, rightSurroundSide, leftSurround, rightSurround, topFrontLeft, topFrontRight, wideLeft, wideRight, topRearCentre, centreSurround, LFE, LFE2 } },
{ kAudioChannelLayoutTag_AC3_1_0_1, { centre, LFE } },
{ kAudioChannelLayoutTag_AC3_3_0, { left, centre, right } },
{ kAudioChannelLayoutTag_AC3_3_1, { left, centre, right, centreSurround } },
{ kAudioChannelLayoutTag_AC3_3_0_1, { left, centre, right, LFE } },
{ kAudioChannelLayoutTag_AC3_2_1_1, { left, right, centreSurround, LFE } },
{ kAudioChannelLayoutTag_AC3_3_1_1, { left, centre, right, centreSurround, LFE } },
{ kAudioChannelLayoutTag_EAC_6_0_A, { left, centre, right, leftSurround, rightSurround, centreSurround } },
{ kAudioChannelLayoutTag_EAC_7_0_A, { left, centre, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear } },
{ kAudioChannelLayoutTag_EAC3_6_1_A, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround } },
{ kAudioChannelLayoutTag_EAC3_6_1_B, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround } },
{ kAudioChannelLayoutTag_EAC3_6_1_C, { left, centre, right, leftSurround, rightSurround, LFE, topFrontCentre } },
{ kAudioChannelLayoutTag_EAC3_7_1_A, { left, centre, right, leftSurround, rightSurround, LFE, leftSurroundRear, rightSurroundRear } },
{ kAudioChannelLayoutTag_EAC3_7_1_B, { left, centre, right, leftSurround, rightSurround, LFE, leftCentre, rightCentre } },
{ kAudioChannelLayoutTag_EAC3_7_1_C, { left, centre, right, leftSurround, rightSurround, LFE, leftSurroundSide, rightSurroundSide } },
{ kAudioChannelLayoutTag_EAC3_7_1_D, { left, centre, right, leftSurround, rightSurround, LFE, wideLeft, wideRight } },
{ kAudioChannelLayoutTag_EAC3_7_1_E, { left, centre, right, leftSurround, rightSurround, LFE, topFrontLeft, topFrontRight } },
{ kAudioChannelLayoutTag_EAC3_7_1_F, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround, topMiddle } },
{ kAudioChannelLayoutTag_EAC3_7_1_G, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround, topFrontCentre } },
{ kAudioChannelLayoutTag_EAC3_7_1_H, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround, topFrontCentre } },
{ kAudioChannelLayoutTag_DTS_3_1, { centre, left, right, LFE } },
{ kAudioChannelLayoutTag_DTS_4_1, { centre, left, right, centreSurround, LFE } },
{ kAudioChannelLayoutTag_DTS_6_0_B, { centre, left, right, leftSurroundRear, rightSurroundRear, centreSurround } },
{ kAudioChannelLayoutTag_DTS_6_0_C, { centre, centreSurround, left, right, leftSurroundRear, rightSurroundRear } },
{ kAudioChannelLayoutTag_DTS_6_1_B, { centre, left, right, leftSurroundRear, rightSurroundRear, centreSurround, LFE } },
{ kAudioChannelLayoutTag_DTS_6_1_C, { centre, centreSurround, left, right, leftSurroundRear, rightSurroundRear, LFE } },
{ kAudioChannelLayoutTag_DTS_6_1_D, { centre, left, right, leftSurround, rightSurround, LFE, centreSurround } },
{ kAudioChannelLayoutTag_DTS_7_0, { leftCentre, centre, rightCentre, left, right, leftSurround, rightSurround } },
{ kAudioChannelLayoutTag_DTS_7_1, { leftCentre, centre, rightCentre, left, right, leftSurround, rightSurround, LFE } },
{ kAudioChannelLayoutTag_DTS_8_0_A, { leftCentre, rightCentre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear } },
{ kAudioChannelLayoutTag_DTS_8_0_B, { leftCentre, centre, rightCentre, left, right, leftSurround, centreSurround, rightSurround } },
{ kAudioChannelLayoutTag_DTS_8_1_A, { leftCentre, rightCentre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear, LFE } },
{ kAudioChannelLayoutTag_DTS_8_1_B, { leftCentre, centre, rightCentre, left, right, leftSurround, centreSurround, rightSurround, LFE } },
{ 0, {} }
};
return tbl;
}
};
//==============================================================================
static AudioChannelSet::ChannelType getChannelTypeFromAudioChannelLabel (AudioChannelLabel label) noexcept
{
if (label >= kAudioChannelLabel_Discrete_0 && label <= kAudioChannelLabel_Discrete_65535)
{
const unsigned int discreteChannelNum = label - kAudioChannelLabel_Discrete_0;
return static_cast<AudioChannelSet::ChannelType> (AudioChannelSet::discreteChannel0 + discreteChannelNum);
}
switch (label)
{
case kAudioChannelLabel_Center:
case kAudioChannelLabel_Mono: return AudioChannelSet::centre;
case kAudioChannelLabel_Left:
case kAudioChannelLabel_HeadphonesLeft: return AudioChannelSet::left;
case kAudioChannelLabel_Right:
case kAudioChannelLabel_HeadphonesRight: return AudioChannelSet::right;
case kAudioChannelLabel_LFEScreen: return AudioChannelSet::LFE;
case kAudioChannelLabel_LeftSurround: return AudioChannelSet::leftSurround;
case kAudioChannelLabel_RightSurround: return AudioChannelSet::rightSurround;
case kAudioChannelLabel_LeftCenter: return AudioChannelSet::leftCentre;
case kAudioChannelLabel_RightCenter: return AudioChannelSet::rightCentre;
case kAudioChannelLabel_CenterSurround: return AudioChannelSet::surround;
case kAudioChannelLabel_LeftSurroundDirect: return AudioChannelSet::leftSurroundSide;
case kAudioChannelLabel_RightSurroundDirect: return AudioChannelSet::rightSurroundSide;
case kAudioChannelLabel_TopCenterSurround: return AudioChannelSet::topMiddle;
case kAudioChannelLabel_VerticalHeightLeft: return AudioChannelSet::topFrontLeft;
case kAudioChannelLabel_VerticalHeightRight: return AudioChannelSet::topFrontRight;
case kAudioChannelLabel_VerticalHeightCenter: return AudioChannelSet::topFrontCentre;
case kAudioChannelLabel_TopBackLeft: return AudioChannelSet::topRearLeft;
case kAudioChannelLabel_RearSurroundLeft: return AudioChannelSet::leftSurroundRear;
case kAudioChannelLabel_TopBackRight: return AudioChannelSet::topRearRight;
case kAudioChannelLabel_RearSurroundRight: return AudioChannelSet::rightSurroundRear;
case kAudioChannelLabel_TopBackCenter: return AudioChannelSet::topRearCentre;
case kAudioChannelLabel_LFE2: return AudioChannelSet::LFE2;
case kAudioChannelLabel_LeftWide: return AudioChannelSet::wideLeft;
case kAudioChannelLabel_RightWide: return AudioChannelSet::wideRight;
case kAudioChannelLabel_Ambisonic_W: return AudioChannelSet::ambisonicW;
case kAudioChannelLabel_Ambisonic_X: return AudioChannelSet::ambisonicX;
case kAudioChannelLabel_Ambisonic_Y: return AudioChannelSet::ambisonicY;
case kAudioChannelLabel_Ambisonic_Z: return AudioChannelSet::ambisonicZ;
default: return AudioChannelSet::unknown;
}
}
};
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
#if ! defined (DOXYGEN) && (JUCE_MAC || JUCE_IOS)
struct CoreAudioLayouts
{
//==============================================================================
struct LayoutTagSpeakerList
{
AudioChannelLayoutTag tag;
AudioChannelSet::ChannelType channelTypes[16];
};
//==============================================================================
// This list has been derived from https://pastebin.com/24dQ4BPJ
// Apple channel labels have been replaced by JUCE channel names
// This means that some layouts will be identical in JUCE but not in CoreAudio
// In Apple's official definition the following tags exist with the same speaker layout and order
// even when *not* represented in JUCE channels
// kAudioChannelLayoutTag_Binaural = kAudioChannelLayoutTag_Stereo
// kAudioChannelLayoutTag_MPEG_5_0_B = kAudioChannelLayoutTag_Pentagonal
// kAudioChannelLayoutTag_ITU_2_2 = kAudioChannelLayoutTag_Quadraphonic
// kAudioChannelLayoutTag_AudioUnit_6_0 = kAudioChannelLayoutTag_Hexagonal
struct SpeakerLayoutTable : AudioChannelSet // save us some typing
{
template <typename... Items>
static constexpr auto getArray (Items... items)
{
return std::array<LayoutTagSpeakerList, sizeof... (items)> { { items... } };
}
static constexpr auto get()
{
using List = LayoutTagSpeakerList;
return getArray (List { kAudioChannelLayoutTag_Mono, { centre } },
List { kAudioChannelLayoutTag_Stereo, { left, right } },
List { kAudioChannelLayoutTag_MPEG_3_0_A, { left, right, centre } },
List { kAudioChannelLayoutTag_ITU_2_1, { left, right, centreSurround } },
List { kAudioChannelLayoutTag_MPEG_4_0_A, { left, right, centre, centreSurround } },
List { kAudioChannelLayoutTag_MPEG_5_0_A, { left, right, centre, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_MPEG_5_1_A, { left, right, centre, LFE, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_AudioUnit_6_0, { left, right, leftSurround, rightSurround, centre, centreSurround } },
List { kAudioChannelLayoutTag_MPEG_6_1_A, { left, right, centre, LFE, leftSurround, rightSurround, centreSurround } },
List { kAudioChannelLayoutTag_DTS_6_0_A, { leftSurroundSide, rightSurroundSide, left, right, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_DTS_6_1_A, { leftSurroundSide, rightSurroundSide, left, right, leftSurround, rightSurround, LFE } },
List { kAudioChannelLayoutTag_AudioUnit_7_0, { left, right, leftSurroundSide, rightSurroundSide, centre, leftSurroundRear, rightSurroundRear } },
List { kAudioChannelLayoutTag_AudioUnit_7_0_Front, { left, right, leftSurround, rightSurround, centre, leftCentre, rightCentre } },
List { kAudioChannelLayoutTag_MPEG_7_1_C, { left, right, centre, LFE, leftSurroundSide, rightSurroundSide, leftSurroundRear, rightSurroundRear } },
List { kAudioChannelLayoutTag_MPEG_7_1_A, { left, right, centre, LFE, leftSurround, rightSurround, leftCentre, rightCentre } },
List { kAudioChannelLayoutTag_Ambisonic_B_Format, { ambisonicW, ambisonicX, ambisonicY, ambisonicZ } },
List { kAudioChannelLayoutTag_Quadraphonic, { left, right, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_Pentagonal, { left, right, leftSurroundRear, rightSurroundRear, centre } },
List { kAudioChannelLayoutTag_Hexagonal, { left, right, leftSurroundRear, rightSurroundRear, centre, centreSurround } },
List { kAudioChannelLayoutTag_Octagonal, { left, right, leftSurround, rightSurround, centre, centreSurround, wideLeft, wideRight } },
#if defined (MAC_OS_VERSION_11_0)
List { kAudioChannelLayoutTag_Atmos_5_1_4, { left, right, centre, LFE, leftSurround, rightSurround, topFrontLeft, topFrontRight, topRearLeft, topRearRight } },
List { kAudioChannelLayoutTag_Atmos_7_1_2, { left, right, centre, LFE, leftSurroundSide, rightSurroundSide, leftSurroundRear, rightSurroundRear, topSideLeft, topSideRight } },
#endif
#if defined (MAC_OS_X_VERSION_10_15)
List { kAudioChannelLayoutTag_Atmos_5_1_2, { left, right, centre, LFE, leftSurround, rightSurround, topSideLeft, topSideRight } },
List { kAudioChannelLayoutTag_Atmos_7_1_4, { left, right, centre, LFE, leftSurroundSide, rightSurroundSide, leftSurroundRear, rightSurroundRear, topFrontLeft, topFrontRight, topRearLeft, topRearRight } },
List { kAudioChannelLayoutTag_Atmos_9_1_6, { left, right, centre, LFE, leftSurroundSide, rightSurroundSide, leftSurroundRear, rightSurroundRear, wideLeft, wideRight, topFrontLeft, topFrontRight, topSideLeft, topSideRight, topRearLeft, topRearRight } },
#endif
// More uncommon layouts...
List { kAudioChannelLayoutTag_StereoHeadphones, { left, right } },
List { kAudioChannelLayoutTag_MatrixStereo, { left, right } },
List { kAudioChannelLayoutTag_MidSide, { centre, discreteChannel0 } },
List { kAudioChannelLayoutTag_XY, { ambisonicX, ambisonicY } },
List { kAudioChannelLayoutTag_Binaural, { left, right } },
List { kAudioChannelLayoutTag_Cube, { left, right, leftSurround, rightSurround, topFrontLeft, topFrontRight, topRearLeft, topRearRight } },
List { kAudioChannelLayoutTag_MPEG_3_0_B, { centre, left, right } },
List { kAudioChannelLayoutTag_MPEG_4_0_B, { centre, left, right, centreSurround } },
List { kAudioChannelLayoutTag_MPEG_5_0_B, { left, right, leftSurround, rightSurround, centre } },
List { kAudioChannelLayoutTag_MPEG_5_0_C, { left, centre, right, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_MPEG_5_0_D, { centre, left, right, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_MPEG_5_1_B, { left, right, leftSurround, rightSurround, centre, LFE } },
List { kAudioChannelLayoutTag_MPEG_5_1_C, { left, centre, right, leftSurround, rightSurround, LFE } },
List { kAudioChannelLayoutTag_MPEG_5_1_D, { centre, left, right, leftSurround, rightSurround, LFE } },
List { kAudioChannelLayoutTag_MPEG_7_1_B, { centre, leftCentre, rightCentre, left, right, leftSurround, rightSurround, LFE } },
List { kAudioChannelLayoutTag_Emagic_Default_7_1, { left, right, leftSurround, rightSurround, centre, LFE, leftCentre, rightCentre } },
List { kAudioChannelLayoutTag_SMPTE_DTV, { left, right, centre, LFE, leftSurround, rightSurround, discreteChannel0 /* leftMatrixTotal */, (ChannelType) (discreteChannel0 + 1) /* rightMatrixTotal */} },
List { kAudioChannelLayoutTag_ITU_2_2, { left, right, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_DVD_4, { left, right, LFE } },
List { kAudioChannelLayoutTag_DVD_5, { left, right, LFE, centreSurround } },
List { kAudioChannelLayoutTag_DVD_6, { left, right, LFE, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_DVD_10, { left, right, centre, LFE } },
List { kAudioChannelLayoutTag_DVD_11, { left, right, centre, LFE, centreSurround } },
List { kAudioChannelLayoutTag_DVD_18, { left, right, leftSurround, rightSurround, LFE } },
List { kAudioChannelLayoutTag_AAC_6_0, { centre, left, right, leftSurround, rightSurround, centreSurround } },
List { kAudioChannelLayoutTag_AAC_6_1, { centre, left, right, leftSurround, rightSurround, centreSurround, LFE } },
List { kAudioChannelLayoutTag_AAC_7_0, { centre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear } },
List { kAudioChannelLayoutTag_AAC_7_1_B, { centre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear, LFE } },
List { kAudioChannelLayoutTag_AAC_7_1_C, { centre, left, right, leftSurround, rightSurround, LFE, topFrontLeft, topFrontRight } },
List { kAudioChannelLayoutTag_AAC_Octagonal, { centre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear, centreSurround } },
List { kAudioChannelLayoutTag_TMH_10_2_std, { left, right, centre, topFrontCentre, leftSurroundSide, rightSurroundSide, leftSurround, rightSurround, topFrontLeft, topFrontRight, wideLeft, wideRight, topRearCentre, centreSurround, LFE, LFE2 } },
List { kAudioChannelLayoutTag_AC3_1_0_1, { centre, LFE } },
List { kAudioChannelLayoutTag_AC3_3_0, { left, centre, right } },
List { kAudioChannelLayoutTag_AC3_3_1, { left, centre, right, centreSurround } },
List { kAudioChannelLayoutTag_AC3_3_0_1, { left, centre, right, LFE } },
List { kAudioChannelLayoutTag_AC3_2_1_1, { left, right, centreSurround, LFE } },
List { kAudioChannelLayoutTag_AC3_3_1_1, { left, centre, right, centreSurround, LFE } },
List { kAudioChannelLayoutTag_EAC_6_0_A, { left, centre, right, leftSurround, rightSurround, centreSurround } },
List { kAudioChannelLayoutTag_EAC_7_0_A, { left, centre, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear } },
List { kAudioChannelLayoutTag_EAC3_6_1_A, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround } },
List { kAudioChannelLayoutTag_EAC3_6_1_B, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround } },
List { kAudioChannelLayoutTag_EAC3_6_1_C, { left, centre, right, leftSurround, rightSurround, LFE, topFrontCentre } },
List { kAudioChannelLayoutTag_EAC3_7_1_A, { left, centre, right, leftSurround, rightSurround, LFE, leftSurroundRear, rightSurroundRear } },
List { kAudioChannelLayoutTag_EAC3_7_1_B, { left, centre, right, leftSurround, rightSurround, LFE, leftCentre, rightCentre } },
List { kAudioChannelLayoutTag_EAC3_7_1_C, { left, centre, right, leftSurround, rightSurround, LFE, leftSurroundSide, rightSurroundSide } },
List { kAudioChannelLayoutTag_EAC3_7_1_D, { left, centre, right, leftSurround, rightSurround, LFE, wideLeft, wideRight } },
List { kAudioChannelLayoutTag_EAC3_7_1_E, { left, centre, right, leftSurround, rightSurround, LFE, topFrontLeft, topFrontRight } },
List { kAudioChannelLayoutTag_EAC3_7_1_F, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround, topMiddle } },
List { kAudioChannelLayoutTag_EAC3_7_1_G, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround, topFrontCentre } },
List { kAudioChannelLayoutTag_EAC3_7_1_H, { left, centre, right, leftSurround, rightSurround, LFE, centreSurround, topFrontCentre } },
List { kAudioChannelLayoutTag_DTS_3_1, { centre, left, right, LFE } },
List { kAudioChannelLayoutTag_DTS_4_1, { centre, left, right, centreSurround, LFE } },
List { kAudioChannelLayoutTag_DTS_6_0_B, { centre, left, right, leftSurroundRear, rightSurroundRear, centreSurround } },
List { kAudioChannelLayoutTag_DTS_6_0_C, { centre, centreSurround, left, right, leftSurroundRear, rightSurroundRear } },
List { kAudioChannelLayoutTag_DTS_6_1_B, { centre, left, right, leftSurroundRear, rightSurroundRear, centreSurround, LFE } },
List { kAudioChannelLayoutTag_DTS_6_1_C, { centre, centreSurround, left, right, leftSurroundRear, rightSurroundRear, LFE } },
List { kAudioChannelLayoutTag_DTS_6_1_D, { centre, left, right, leftSurround, rightSurround, LFE, centreSurround } },
List { kAudioChannelLayoutTag_DTS_7_0, { leftCentre, centre, rightCentre, left, right, leftSurround, rightSurround } },
List { kAudioChannelLayoutTag_DTS_7_1, { leftCentre, centre, rightCentre, left, right, leftSurround, rightSurround, LFE } },
List { kAudioChannelLayoutTag_DTS_8_0_A, { leftCentre, rightCentre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear } },
List { kAudioChannelLayoutTag_DTS_8_0_B, { leftCentre, centre, rightCentre, left, right, leftSurround, centreSurround, rightSurround } },
List { kAudioChannelLayoutTag_DTS_8_1_A, { leftCentre, rightCentre, left, right, leftSurround, rightSurround, leftSurroundRear, rightSurroundRear, LFE } },
List { kAudioChannelLayoutTag_DTS_8_1_B, { leftCentre, centre, rightCentre, left, right, leftSurround, centreSurround, rightSurround, LFE } });
}
};
public:
//==============================================================================
enum
{
coreAudioHOASN3DLayoutTag = (190U<<16) | 0 // kAudioChannelLayoutTag_HOA_ACN_SN3D
};
//==============================================================================
/** Convert CoreAudio's native AudioChannelLayout to JUCE's AudioChannelSet.
Note that this method cannot preserve the order of channels.
*/
static AudioChannelSet fromCoreAudio (const AudioChannelLayout& layout)
{
return AudioChannelSet::channelSetWithChannels (getCoreAudioLayoutChannels (layout));
}
/** Convert CoreAudio's native AudioChannelLayoutTag to JUCE's AudioChannelSet.
Note that this method cannot preserve the order of channels.
*/
static AudioChannelSet fromCoreAudio (AudioChannelLayoutTag layoutTag)
{
return AudioChannelSet::channelSetWithChannels (getSpeakerLayoutForCoreAudioTag (layoutTag));
}
/** Convert JUCE's AudioChannelSet to CoreAudio's AudioChannelLayoutTag.
Note that this method cannot preserve the order of channels.
*/
static AudioChannelLayoutTag toCoreAudio (const AudioChannelSet& set)
{
if (set.getAmbisonicOrder() >= 0)
return coreAudioHOASN3DLayoutTag | static_cast<unsigned> (set.size());
for (const auto& item : SpeakerLayoutTable::get())
{
AudioChannelSet caSet;
for (int i = 0; i < numElementsInArray (item.channelTypes)
&& item.channelTypes[i] != AudioChannelSet::unknown; ++i)
caSet.addChannel (item.channelTypes[i]);
if (caSet == set)
return item.tag;
}
return kAudioChannelLayoutTag_DiscreteInOrder | static_cast<AudioChannelLayoutTag> (set.size());
}
static const Array<AudioChannelLayoutTag>& getKnownCoreAudioTags()
{
static Array<AudioChannelLayoutTag> tags (createKnownCoreAudioTags());
return tags;
}
//==============================================================================
/** Convert CoreAudio's native AudioChannelLayout to an array of JUCE ChannelTypes. */
static Array<AudioChannelSet::ChannelType> getCoreAudioLayoutChannels (const AudioChannelLayout& layout)
{
switch (layout.mChannelLayoutTag & 0xffff0000)
{
case kAudioChannelLayoutTag_UseChannelBitmap:
return AudioChannelSet::fromWaveChannelMask (static_cast<int> (layout.mChannelBitmap)).getChannelTypes();
case kAudioChannelLayoutTag_UseChannelDescriptions:
{
Array<AudioChannelSet::ChannelType> channels;
for (UInt32 i = 0; i < layout.mNumberChannelDescriptions; ++i)
channels.addIfNotAlreadyThere (getChannelTypeFromAudioChannelLabel (layout.mChannelDescriptions[i].mChannelLabel));
// different speaker mappings may point to the same JUCE speaker so fill up
// this array with discrete channels
for (int j = 0; channels.size() < static_cast<int> (layout.mNumberChannelDescriptions); ++j)
channels.addIfNotAlreadyThere (static_cast<AudioChannelSet::ChannelType> (AudioChannelSet::discreteChannel0 + j));
return channels;
}
case kAudioChannelLayoutTag_DiscreteInOrder:
return AudioChannelSet::discreteChannels (static_cast<int> (layout.mChannelLayoutTag) & 0xffff).getChannelTypes();
default:
break;
}
return getSpeakerLayoutForCoreAudioTag (layout.mChannelLayoutTag);
}
static Array<AudioChannelSet::ChannelType> getSpeakerLayoutForCoreAudioTag (AudioChannelLayoutTag tag)
{
// You need to specify the full AudioChannelLayout when using
// the UseChannelBitmap and UseChannelDescriptions layout tag
jassert (tag != kAudioChannelLayoutTag_UseChannelBitmap && tag != kAudioChannelLayoutTag_UseChannelDescriptions);
Array<AudioChannelSet::ChannelType> speakers;
for (const auto& item : SpeakerLayoutTable::get())
{
if (tag == item.tag)
{
for (int i = 0; i < numElementsInArray (item.channelTypes)
&& item.channelTypes[i] != AudioChannelSet::unknown; ++i)
speakers.add (item.channelTypes[i]);
return speakers;
}
}
auto numChannels = tag & 0xffff;
if (tag >= coreAudioHOASN3DLayoutTag && tag <= (coreAudioHOASN3DLayoutTag | 0xffff))
{
auto sqrtMinusOne = std::sqrt (static_cast<float> (numChannels)) - 1.0f;
auto ambisonicOrder = jmax (0, static_cast<int> (std::floor (sqrtMinusOne)));
if (static_cast<float> (ambisonicOrder) == sqrtMinusOne)
return AudioChannelSet::ambisonic (ambisonicOrder).getChannelTypes();
}
for (UInt32 i = 0; i < numChannels; ++i)
speakers.add (static_cast<AudioChannelSet::ChannelType> (AudioChannelSet::discreteChannel0 + i));
return speakers;
}
private:
static Array<AudioChannelLayoutTag> createKnownCoreAudioTags()
{
Array<AudioChannelLayoutTag> tags;
for (const auto& item : SpeakerLayoutTable::get())
tags.addIfNotAlreadyThere (item.tag);
for (unsigned order = 0; order <= 5; ++order)
tags.addIfNotAlreadyThere (coreAudioHOASN3DLayoutTag | ((order + 1) * (order + 1)));
return tags;
}
//==============================================================================
static AudioChannelSet::ChannelType getChannelTypeFromAudioChannelLabel (AudioChannelLabel label) noexcept
{
if (label >= kAudioChannelLabel_Discrete_0 && label <= kAudioChannelLabel_Discrete_65535)
{
const unsigned int discreteChannelNum = label - kAudioChannelLabel_Discrete_0;
return static_cast<AudioChannelSet::ChannelType> (AudioChannelSet::discreteChannel0 + discreteChannelNum);
}
switch (label)
{
case kAudioChannelLabel_Center:
case kAudioChannelLabel_Mono: return AudioChannelSet::centre;
case kAudioChannelLabel_Left:
case kAudioChannelLabel_HeadphonesLeft: return AudioChannelSet::left;
case kAudioChannelLabel_Right:
case kAudioChannelLabel_HeadphonesRight: return AudioChannelSet::right;
case kAudioChannelLabel_LFEScreen: return AudioChannelSet::LFE;
case kAudioChannelLabel_LeftSurround: return AudioChannelSet::leftSurround;
case kAudioChannelLabel_RightSurround: return AudioChannelSet::rightSurround;
case kAudioChannelLabel_LeftCenter: return AudioChannelSet::leftCentre;
case kAudioChannelLabel_RightCenter: return AudioChannelSet::rightCentre;
case kAudioChannelLabel_CenterSurround: return AudioChannelSet::surround;
case kAudioChannelLabel_LeftSurroundDirect: return AudioChannelSet::leftSurroundSide;
case kAudioChannelLabel_RightSurroundDirect: return AudioChannelSet::rightSurroundSide;
case kAudioChannelLabel_TopCenterSurround: return AudioChannelSet::topMiddle;
case kAudioChannelLabel_VerticalHeightLeft: return AudioChannelSet::topFrontLeft;
case kAudioChannelLabel_VerticalHeightRight: return AudioChannelSet::topFrontRight;
case kAudioChannelLabel_VerticalHeightCenter: return AudioChannelSet::topFrontCentre;
case kAudioChannelLabel_TopBackLeft: return AudioChannelSet::topRearLeft;
case kAudioChannelLabel_RearSurroundLeft: return AudioChannelSet::leftSurroundRear;
case kAudioChannelLabel_TopBackRight: return AudioChannelSet::topRearRight;
case kAudioChannelLabel_RearSurroundRight: return AudioChannelSet::rightSurroundRear;
case kAudioChannelLabel_TopBackCenter: return AudioChannelSet::topRearCentre;
case kAudioChannelLabel_LFE2: return AudioChannelSet::LFE2;
case kAudioChannelLabel_LeftWide: return AudioChannelSet::wideLeft;
case kAudioChannelLabel_RightWide: return AudioChannelSet::wideRight;
case kAudioChannelLabel_Ambisonic_W: return AudioChannelSet::ambisonicW;
case kAudioChannelLabel_Ambisonic_X: return AudioChannelSet::ambisonicX;
case kAudioChannelLabel_Ambisonic_Y: return AudioChannelSet::ambisonicY;
case kAudioChannelLabel_Ambisonic_Z: return AudioChannelSet::ambisonicZ;
default: return AudioChannelSet::unknown;
}
}
};
#endif
} // namespace juce

View File

@@ -0,0 +1,80 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
// This file will be included directly by macOS/iOS-specific .cpps
#pragma once
#if ! DOXYGEN
#include <mach/mach_time.h>
namespace juce
{
struct CoreAudioTimeConversions
{
public:
CoreAudioTimeConversions()
{
mach_timebase_info_data_t info{};
mach_timebase_info (&info);
numerator = info.numer;
denominator = info.denom;
}
uint64_t hostTimeToNanos (uint64_t hostTime) const
{
return multiplyByRatio (hostTime, numerator, denominator);
}
uint64_t nanosToHostTime (uint64_t nanos) const
{
return multiplyByRatio (nanos, denominator, numerator);
}
private:
// Adapted from CAHostTimeBase.h in the Core Audio Utility Classes
static uint64_t multiplyByRatio (uint64_t toMultiply, uint64_t numerator, uint64_t denominator)
{
#if defined (__SIZEOF_INT128__)
unsigned __int128
#else
long double
#endif
result = toMultiply;
if (numerator != denominator)
{
result *= numerator;
result /= denominator;
}
return (uint64_t) result;
}
uint64_t numerator = 0, denominator = 0;
};
} // namespace juce
#endif

View File

@@ -1,179 +1,179 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Used by AudioSource::getNextAudioBlock().
@tags{Audio}
*/
struct JUCE_API AudioSourceChannelInfo
{
/** Creates an uninitialised AudioSourceChannelInfo. */
AudioSourceChannelInfo() = default;
/** Creates an AudioSourceChannelInfo. */
AudioSourceChannelInfo (AudioBuffer<float>* bufferToUse,
int startSampleOffset, int numSamplesToUse) noexcept
: buffer (bufferToUse),
startSample (startSampleOffset),
numSamples (numSamplesToUse)
{
}
/** Creates an AudioSourceChannelInfo that uses the whole of a buffer.
Note that the buffer provided must not be deleted while the
AudioSourceChannelInfo is still using it.
*/
explicit AudioSourceChannelInfo (AudioBuffer<float>& bufferToUse) noexcept
: buffer (&bufferToUse),
startSample (0),
numSamples (bufferToUse.getNumSamples())
{
}
/** The destination buffer to fill with audio data.
When the AudioSource::getNextAudioBlock() method is called, the active section
of this buffer should be filled with whatever output the source produces.
Only the samples specified by the startSample and numSamples members of this structure
should be affected by the call.
The contents of the buffer when it is passed to the AudioSource::getNextAudioBlock()
method can be treated as the input if the source is performing some kind of filter operation,
but should be cleared if this is not the case - the clearActiveBufferRegion() is
a handy way of doing this.
The number of channels in the buffer could be anything, so the AudioSource
must cope with this in whatever way is appropriate for its function.
*/
AudioBuffer<float>* buffer;
/** The first sample in the buffer from which the callback is expected
to write data. */
int startSample;
/** The number of samples in the buffer which the callback is expected to
fill with data. */
int numSamples;
/** Convenient method to clear the buffer if the source is not producing any data. */
void clearActiveBufferRegion() const
{
if (buffer != nullptr)
buffer->clear (startSample, numSamples);
}
};
//==============================================================================
/**
Base class for objects that can produce a continuous stream of audio.
An AudioSource has two states: 'prepared' and 'unprepared'.
When a source needs to be played, it is first put into a 'prepared' state by a call to
prepareToPlay(), and then repeated calls will be made to its getNextAudioBlock() method to
process the audio data.
Once playback has finished, the releaseResources() method is called to put the stream
back into an 'unprepared' state.
@see AudioFormatReaderSource, ResamplingAudioSource
@tags{Audio}
*/
class JUCE_API AudioSource
{
protected:
//==============================================================================
/** Creates an AudioSource. */
AudioSource() = default;
public:
/** Destructor. */
virtual ~AudioSource() = default;
//==============================================================================
/** Tells the source to prepare for playing.
An AudioSource has two states: prepared and unprepared.
The prepareToPlay() method is guaranteed to be called at least once on an 'unprepared'
source to put it into a 'prepared' state before any calls will be made to getNextAudioBlock().
This callback allows the source to initialise any resources it might need when playing.
Once playback has finished, the releaseResources() method is called to put the stream
back into an 'unprepared' state.
Note that this method could be called more than once in succession without
a matching call to releaseResources(), so make sure your code is robust and
can handle that kind of situation.
@param samplesPerBlockExpected the number of samples that the source
will be expected to supply each time its
getNextAudioBlock() method is called. This
number may vary slightly, because it will be dependent
on audio hardware callbacks, and these aren't
guaranteed to always use a constant block size, so
the source should be able to cope with small variations.
@param sampleRate the sample rate that the output will be used at - this
is needed by sources such as tone generators.
@see releaseResources, getNextAudioBlock
*/
virtual void prepareToPlay (int samplesPerBlockExpected,
double sampleRate) = 0;
/** Allows the source to release anything it no longer needs after playback has stopped.
This will be called when the source is no longer going to have its getNextAudioBlock()
method called, so it should release any spare memory, etc. that it might have
allocated during the prepareToPlay() call.
Note that there's no guarantee that prepareToPlay() will actually have been called before
releaseResources(), and it may be called more than once in succession, so make sure your
code is robust and doesn't make any assumptions about when it will be called.
@see prepareToPlay, getNextAudioBlock
*/
virtual void releaseResources() = 0;
/** Called repeatedly to fetch subsequent blocks of audio data.
After calling the prepareToPlay() method, this callback will be made each
time the audio playback hardware (or whatever other destination the audio
data is going to) needs another block of data.
It will generally be called on a high-priority system thread, or possibly even
an interrupt, so be careful not to do too much work here, as that will cause
audio glitches!
@see AudioSourceChannelInfo, prepareToPlay, releaseResources
*/
virtual void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) = 0;
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
Used by AudioSource::getNextAudioBlock().
@tags{Audio}
*/
struct JUCE_API AudioSourceChannelInfo
{
/** Creates an uninitialised AudioSourceChannelInfo. */
AudioSourceChannelInfo() = default;
/** Creates an AudioSourceChannelInfo. */
AudioSourceChannelInfo (AudioBuffer<float>* bufferToUse,
int startSampleOffset, int numSamplesToUse) noexcept
: buffer (bufferToUse),
startSample (startSampleOffset),
numSamples (numSamplesToUse)
{
}
/** Creates an AudioSourceChannelInfo that uses the whole of a buffer.
Note that the buffer provided must not be deleted while the
AudioSourceChannelInfo is still using it.
*/
explicit AudioSourceChannelInfo (AudioBuffer<float>& bufferToUse) noexcept
: buffer (&bufferToUse),
startSample (0),
numSamples (bufferToUse.getNumSamples())
{
}
/** The destination buffer to fill with audio data.
When the AudioSource::getNextAudioBlock() method is called, the active section
of this buffer should be filled with whatever output the source produces.
Only the samples specified by the startSample and numSamples members of this structure
should be affected by the call.
The contents of the buffer when it is passed to the AudioSource::getNextAudioBlock()
method can be treated as the input if the source is performing some kind of filter operation,
but should be cleared if this is not the case - the clearActiveBufferRegion() is
a handy way of doing this.
The number of channels in the buffer could be anything, so the AudioSource
must cope with this in whatever way is appropriate for its function.
*/
AudioBuffer<float>* buffer;
/** The first sample in the buffer from which the callback is expected
to write data. */
int startSample;
/** The number of samples in the buffer which the callback is expected to
fill with data. */
int numSamples;
/** Convenient method to clear the buffer if the source is not producing any data. */
void clearActiveBufferRegion() const
{
if (buffer != nullptr)
buffer->clear (startSample, numSamples);
}
};
//==============================================================================
/**
Base class for objects that can produce a continuous stream of audio.
An AudioSource has two states: 'prepared' and 'unprepared'.
When a source needs to be played, it is first put into a 'prepared' state by a call to
prepareToPlay(), and then repeated calls will be made to its getNextAudioBlock() method to
process the audio data.
Once playback has finished, the releaseResources() method is called to put the stream
back into an 'unprepared' state.
@see AudioFormatReaderSource, ResamplingAudioSource
@tags{Audio}
*/
class JUCE_API AudioSource
{
protected:
//==============================================================================
/** Creates an AudioSource. */
AudioSource() = default;
public:
/** Destructor. */
virtual ~AudioSource() = default;
//==============================================================================
/** Tells the source to prepare for playing.
An AudioSource has two states: prepared and unprepared.
The prepareToPlay() method is guaranteed to be called at least once on an 'unprepared'
source to put it into a 'prepared' state before any calls will be made to getNextAudioBlock().
This callback allows the source to initialise any resources it might need when playing.
Once playback has finished, the releaseResources() method is called to put the stream
back into an 'unprepared' state.
Note that this method could be called more than once in succession without
a matching call to releaseResources(), so make sure your code is robust and
can handle that kind of situation.
@param samplesPerBlockExpected the number of samples that the source
will be expected to supply each time its
getNextAudioBlock() method is called. This
number may vary slightly, because it will be dependent
on audio hardware callbacks, and these aren't
guaranteed to always use a constant block size, so
the source should be able to cope with small variations.
@param sampleRate the sample rate that the output will be used at - this
is needed by sources such as tone generators.
@see releaseResources, getNextAudioBlock
*/
virtual void prepareToPlay (int samplesPerBlockExpected,
double sampleRate) = 0;
/** Allows the source to release anything it no longer needs after playback has stopped.
This will be called when the source is no longer going to have its getNextAudioBlock()
method called, so it should release any spare memory, etc. that it might have
allocated during the prepareToPlay() call.
Note that there's no guarantee that prepareToPlay() will actually have been called before
releaseResources(), and it may be called more than once in succession, so make sure your
code is robust and doesn't make any assumptions about when it will be called.
@see prepareToPlay, getNextAudioBlock
*/
virtual void releaseResources() = 0;
/** Called repeatedly to fetch subsequent blocks of audio data.
After calling the prepareToPlay() method, this callback will be made each
time the audio playback hardware (or whatever other destination the audio
data is going to) needs another block of data.
It will generally be called on a high-priority system thread, or possibly even
an interrupt, so be careful not to do too much work here, as that will cause
audio glitches!
@see AudioSourceChannelInfo, prepareToPlay, releaseResources
*/
virtual void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) = 0;
};
} // namespace juce

View File

@@ -1,342 +1,329 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
BufferingAudioSource::BufferingAudioSource (PositionableAudioSource* s,
TimeSliceThread& thread,
bool deleteSourceWhenDeleted,
int bufferSizeSamples,
int numChannels,
bool prefillBufferOnPrepareToPlay)
: source (s, deleteSourceWhenDeleted),
backgroundThread (thread),
numberOfSamplesToBuffer (jmax (1024, bufferSizeSamples)),
numberOfChannels (numChannels),
prefillBuffer (prefillBufferOnPrepareToPlay)
{
jassert (source != nullptr);
jassert (numberOfSamplesToBuffer > 1024); // not much point using this class if you're
// not using a larger buffer..
}
BufferingAudioSource::~BufferingAudioSource()
{
releaseResources();
}
//==============================================================================
void BufferingAudioSource::prepareToPlay (int samplesPerBlockExpected, double newSampleRate)
{
auto bufferSizeNeeded = jmax (samplesPerBlockExpected * 2, numberOfSamplesToBuffer);
if (newSampleRate != sampleRate
|| bufferSizeNeeded != buffer.getNumSamples()
|| ! isPrepared)
{
backgroundThread.removeTimeSliceClient (this);
isPrepared = true;
sampleRate = newSampleRate;
source->prepareToPlay (samplesPerBlockExpected, newSampleRate);
buffer.setSize (numberOfChannels, bufferSizeNeeded);
buffer.clear();
const ScopedLock sl (bufferRangeLock);
bufferValidStart = 0;
bufferValidEnd = 0;
backgroundThread.addTimeSliceClient (this);
do
{
const ScopedUnlock ul (bufferRangeLock);
backgroundThread.moveToFrontOfQueue (this);
Thread::sleep (5);
}
while (prefillBuffer
&& (bufferValidEnd - bufferValidStart < jmin (((int) newSampleRate) / 4, buffer.getNumSamples() / 2)));
}
}
void BufferingAudioSource::releaseResources()
{
isPrepared = false;
backgroundThread.removeTimeSliceClient (this);
buffer.setSize (numberOfChannels, 0);
// MSVC2015 seems to need this if statement to not generate a warning during linking.
// As source is set in the constructor, there is no way that source could
// ever equal this, but it seems to make MSVC2015 happy.
if (source != this)
source->releaseResources();
}
void BufferingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
const auto bufferRange = getValidBufferRange (info.numSamples);
if (bufferRange.isEmpty())
{
// total cache miss
info.clearActiveBufferRegion();
return;
}
const auto validStart = bufferRange.getStart();
const auto validEnd = bufferRange.getEnd();
const ScopedLock sl (callbackLock);
if (validStart > 0)
info.buffer->clear (info.startSample, validStart); // partial cache miss at start
if (validEnd < info.numSamples)
info.buffer->clear (info.startSample + validEnd,
info.numSamples - validEnd); // partial cache miss at end
if (validStart < validEnd)
{
for (int chan = jmin (numberOfChannels, info.buffer->getNumChannels()); --chan >= 0;)
{
jassert (buffer.getNumSamples() > 0);
const auto startBufferIndex = (int) ((validStart + nextPlayPos) % buffer.getNumSamples());
const auto endBufferIndex = (int) ((validEnd + nextPlayPos) % buffer.getNumSamples());
if (startBufferIndex < endBufferIndex)
{
info.buffer->copyFrom (chan, info.startSample + validStart,
buffer,
chan, startBufferIndex,
validEnd - validStart);
}
else
{
const auto initialSize = buffer.getNumSamples() - startBufferIndex;
info.buffer->copyFrom (chan, info.startSample + validStart,
buffer,
chan, startBufferIndex,
initialSize);
info.buffer->copyFrom (chan, info.startSample + validStart + initialSize,
buffer,
chan, 0,
(validEnd - validStart) - initialSize);
}
}
}
nextPlayPos += info.numSamples;
}
bool BufferingAudioSource::waitForNextAudioBlockReady (const AudioSourceChannelInfo& info, uint32 timeout)
{
if (source == nullptr || source->getTotalLength() <= 0)
return false;
if ((nextPlayPos + info.numSamples < 0)
|| (! isLooping() && nextPlayPos > getTotalLength()))
return true;
const auto startTime = Time::getMillisecondCounter();
auto now = startTime;
auto elapsed = (now >= startTime ? now - startTime
: (std::numeric_limits<uint32>::max() - startTime) + now);
while (elapsed <= timeout)
{
const auto bufferRange = getValidBufferRange (info.numSamples);
const auto validStart = bufferRange.getStart();
const auto validEnd = bufferRange.getEnd();
if (validStart <= 0
&& validStart < validEnd
&& validEnd >= info.numSamples)
{
return true;
}
if (elapsed < timeout
&& ! bufferReadyEvent.wait (static_cast<int> (timeout - elapsed)))
{
return false;
}
now = Time::getMillisecondCounter();
elapsed = (now >= startTime ? now - startTime
: (std::numeric_limits<uint32>::max() - startTime) + now);
}
return false;
}
void BufferingAudioSource::setLoopRange (int64 loopStart, int64 loopLength)
{
source->setLoopRange(loopStart, loopLength);
loopRangeChanged = true;
backgroundThread.moveToFrontOfQueue (this);
}
int64 BufferingAudioSource::getNextReadPosition() const
{
jassert (source->getTotalLength() > 0);
const auto pos = nextPlayPos.load();
if (source->isLooping()) {
int64 loopstart, looplen;
source->getLoopRange(loopstart, looplen);
return ((pos - loopstart) % looplen) + loopstart ;
}
else return pos;
}
void BufferingAudioSource::setNextReadPosition (int64 newPosition)
{
const ScopedLock sl (bufferRangeLock);
nextPlayPos = newPosition;
backgroundThread.moveToFrontOfQueue (this);
}
Range<int> BufferingAudioSource::getValidBufferRange (int numSamples) const
{
const ScopedLock sl (bufferRangeLock);
const auto pos = nextPlayPos.load();
return { (int) (jlimit (bufferValidStart, bufferValidEnd, pos) - pos),
(int) (jlimit (bufferValidStart, bufferValidEnd, pos + numSamples) - pos) };
}
bool BufferingAudioSource::readNextBufferChunk()
{
int64 newBVS, newBVE, sectionToReadStart, sectionToReadEnd;
{
const ScopedLock sl (bufferRangeLock);
if (wasSourceLooping != isLooping() || loopRangeChanged)
{
wasSourceLooping = isLooping();
bufferValidStart = 0;
bufferValidEnd = 0;
loopRangeChanged = false;
}
newBVS = jmax ((int64) 0, nextPlayPos.load());
newBVE = newBVS + buffer.getNumSamples() - 4;
sectionToReadStart = 0;
sectionToReadEnd = 0;
constexpr int maxChunkSize = 2048;
if (newBVS < bufferValidStart || newBVS >= bufferValidEnd)
{
newBVE = jmin (newBVE, newBVS + maxChunkSize);
sectionToReadStart = newBVS;
sectionToReadEnd = newBVE;
bufferValidStart = 0;
bufferValidEnd = 0;
}
else if (std::abs ((int) (newBVS - bufferValidStart)) > 512
|| std::abs ((int) (newBVE - bufferValidEnd)) > 512)
{
newBVE = jmin (newBVE, bufferValidEnd + maxChunkSize);
sectionToReadStart = bufferValidEnd;
sectionToReadEnd = newBVE;
bufferValidStart = newBVS;
bufferValidEnd = jmin (bufferValidEnd, newBVE);
}
}
if (sectionToReadStart == sectionToReadEnd)
return false;
jassert (buffer.getNumSamples() > 0);
const auto bufferIndexStart = (int) (sectionToReadStart % buffer.getNumSamples());
const auto bufferIndexEnd = (int) (sectionToReadEnd % buffer.getNumSamples());
if (bufferIndexStart < bufferIndexEnd)
{
readBufferSection (sectionToReadStart,
(int) (sectionToReadEnd - sectionToReadStart),
bufferIndexStart);
}
else
{
const auto initialSize = buffer.getNumSamples() - bufferIndexStart;
readBufferSection (sectionToReadStart,
initialSize,
bufferIndexStart);
readBufferSection (sectionToReadStart + initialSize,
(int) (sectionToReadEnd - sectionToReadStart) - initialSize,
0);
}
{
const ScopedLock sl2 (bufferRangeLock);
bufferValidStart = newBVS;
bufferValidEnd = newBVE;
}
bufferReadyEvent.signal();
return true;
}
void BufferingAudioSource::readBufferSection (int64 start, int length, int bufferOffset)
{
if (source->getNextReadPosition() != start)
source->setNextReadPosition (start);
AudioSourceChannelInfo info (&buffer, bufferOffset, length);
const ScopedLock sl (callbackLock);
source->getNextAudioBlock (info);
}
int BufferingAudioSource::useTimeSlice()
{
return readNextBufferChunk() ? 1 : 100;
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
BufferingAudioSource::BufferingAudioSource (PositionableAudioSource* s,
TimeSliceThread& thread,
bool deleteSourceWhenDeleted,
int bufferSizeSamples,
int numChannels,
bool prefillBufferOnPrepareToPlay)
: source (s, deleteSourceWhenDeleted),
backgroundThread (thread),
numberOfSamplesToBuffer (jmax (1024, bufferSizeSamples)),
numberOfChannels (numChannels),
prefillBuffer (prefillBufferOnPrepareToPlay)
{
jassert (source != nullptr);
jassert (numberOfSamplesToBuffer > 1024); // not much point using this class if you're
// not using a larger buffer..
}
BufferingAudioSource::~BufferingAudioSource()
{
releaseResources();
}
//==============================================================================
void BufferingAudioSource::prepareToPlay (int samplesPerBlockExpected, double newSampleRate)
{
auto bufferSizeNeeded = jmax (samplesPerBlockExpected * 2, numberOfSamplesToBuffer);
if (newSampleRate != sampleRate
|| bufferSizeNeeded != buffer.getNumSamples()
|| ! isPrepared)
{
backgroundThread.removeTimeSliceClient (this);
isPrepared = true;
sampleRate = newSampleRate;
source->prepareToPlay (samplesPerBlockExpected, newSampleRate);
buffer.setSize (numberOfChannels, bufferSizeNeeded);
buffer.clear();
const ScopedLock sl (bufferRangeLock);
bufferValidStart = 0;
bufferValidEnd = 0;
backgroundThread.addTimeSliceClient (this);
do
{
const ScopedUnlock ul (bufferRangeLock);
backgroundThread.moveToFrontOfQueue (this);
Thread::sleep (5);
}
while (prefillBuffer
&& (bufferValidEnd - bufferValidStart < jmin (((int) newSampleRate) / 4, buffer.getNumSamples() / 2)));
}
}
void BufferingAudioSource::releaseResources()
{
isPrepared = false;
backgroundThread.removeTimeSliceClient (this);
buffer.setSize (numberOfChannels, 0);
// MSVC2017 seems to need this if statement to not generate a warning during linking.
// As source is set in the constructor, there is no way that source could
// ever equal this, but it seems to make MSVC2017 happy.
if (source != this)
source->releaseResources();
}
void BufferingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
const auto bufferRange = getValidBufferRange (info.numSamples);
if (bufferRange.isEmpty())
{
// total cache miss
info.clearActiveBufferRegion();
return;
}
const auto validStart = bufferRange.getStart();
const auto validEnd = bufferRange.getEnd();
const ScopedLock sl (callbackLock);
if (validStart > 0)
info.buffer->clear (info.startSample, validStart); // partial cache miss at start
if (validEnd < info.numSamples)
info.buffer->clear (info.startSample + validEnd,
info.numSamples - validEnd); // partial cache miss at end
if (validStart < validEnd)
{
for (int chan = jmin (numberOfChannels, info.buffer->getNumChannels()); --chan >= 0;)
{
jassert (buffer.getNumSamples() > 0);
const auto startBufferIndex = (int) ((validStart + nextPlayPos) % buffer.getNumSamples());
const auto endBufferIndex = (int) ((validEnd + nextPlayPos) % buffer.getNumSamples());
if (startBufferIndex < endBufferIndex)
{
info.buffer->copyFrom (chan, info.startSample + validStart,
buffer,
chan, startBufferIndex,
validEnd - validStart);
}
else
{
const auto initialSize = buffer.getNumSamples() - startBufferIndex;
info.buffer->copyFrom (chan, info.startSample + validStart,
buffer,
chan, startBufferIndex,
initialSize);
info.buffer->copyFrom (chan, info.startSample + validStart + initialSize,
buffer,
chan, 0,
(validEnd - validStart) - initialSize);
}
}
}
nextPlayPos += info.numSamples;
}
bool BufferingAudioSource::waitForNextAudioBlockReady (const AudioSourceChannelInfo& info, uint32 timeout)
{
if (source == nullptr || source->getTotalLength() <= 0)
return false;
if ((nextPlayPos + info.numSamples < 0)
|| (! isLooping() && nextPlayPos > getTotalLength()))
return true;
const auto startTime = Time::getMillisecondCounter();
auto now = startTime;
auto elapsed = (now >= startTime ? now - startTime
: (std::numeric_limits<uint32>::max() - startTime) + now);
while (elapsed <= timeout)
{
const auto bufferRange = getValidBufferRange (info.numSamples);
const auto validStart = bufferRange.getStart();
const auto validEnd = bufferRange.getEnd();
if (validStart <= 0
&& validStart < validEnd
&& validEnd >= info.numSamples)
{
return true;
}
if (elapsed < timeout
&& ! bufferReadyEvent.wait (static_cast<int> (timeout - elapsed)))
{
return false;
}
now = Time::getMillisecondCounter();
elapsed = (now >= startTime ? now - startTime
: (std::numeric_limits<uint32>::max() - startTime) + now);
}
return false;
}
int64 BufferingAudioSource::getNextReadPosition() const
{
jassert (source->getTotalLength() > 0);
const auto pos = nextPlayPos.load();
return (source->isLooping() && nextPlayPos > 0)
? pos % source->getTotalLength()
: pos;
}
void BufferingAudioSource::setNextReadPosition (int64 newPosition)
{
const ScopedLock sl (bufferRangeLock);
nextPlayPos = newPosition;
backgroundThread.moveToFrontOfQueue (this);
}
Range<int> BufferingAudioSource::getValidBufferRange (int numSamples) const
{
const ScopedLock sl (bufferRangeLock);
const auto pos = nextPlayPos.load();
return { (int) (jlimit (bufferValidStart, bufferValidEnd, pos) - pos),
(int) (jlimit (bufferValidStart, bufferValidEnd, pos + numSamples) - pos) };
}
bool BufferingAudioSource::readNextBufferChunk()
{
int64 newBVS, newBVE, sectionToReadStart, sectionToReadEnd;
{
const ScopedLock sl (bufferRangeLock);
if (wasSourceLooping != isLooping())
{
wasSourceLooping = isLooping();
bufferValidStart = 0;
bufferValidEnd = 0;
}
newBVS = jmax ((int64) 0, nextPlayPos.load());
newBVE = newBVS + buffer.getNumSamples() - 4;
sectionToReadStart = 0;
sectionToReadEnd = 0;
constexpr int maxChunkSize = 2048;
if (newBVS < bufferValidStart || newBVS >= bufferValidEnd)
{
newBVE = jmin (newBVE, newBVS + maxChunkSize);
sectionToReadStart = newBVS;
sectionToReadEnd = newBVE;
bufferValidStart = 0;
bufferValidEnd = 0;
}
else if (std::abs ((int) (newBVS - bufferValidStart)) > 512
|| std::abs ((int) (newBVE - bufferValidEnd)) > 512)
{
newBVE = jmin (newBVE, bufferValidEnd + maxChunkSize);
sectionToReadStart = bufferValidEnd;
sectionToReadEnd = newBVE;
bufferValidStart = newBVS;
bufferValidEnd = jmin (bufferValidEnd, newBVE);
}
}
if (sectionToReadStart == sectionToReadEnd)
return false;
jassert (buffer.getNumSamples() > 0);
const auto bufferIndexStart = (int) (sectionToReadStart % buffer.getNumSamples());
const auto bufferIndexEnd = (int) (sectionToReadEnd % buffer.getNumSamples());
if (bufferIndexStart < bufferIndexEnd)
{
readBufferSection (sectionToReadStart,
(int) (sectionToReadEnd - sectionToReadStart),
bufferIndexStart);
}
else
{
const auto initialSize = buffer.getNumSamples() - bufferIndexStart;
readBufferSection (sectionToReadStart,
initialSize,
bufferIndexStart);
readBufferSection (sectionToReadStart + initialSize,
(int) (sectionToReadEnd - sectionToReadStart) - initialSize,
0);
}
{
const ScopedLock sl2 (bufferRangeLock);
bufferValidStart = newBVS;
bufferValidEnd = newBVE;
}
bufferReadyEvent.signal();
return true;
}
void BufferingAudioSource::readBufferSection (int64 start, int length, int bufferOffset)
{
if (source->getNextReadPosition() != start)
source->setNextReadPosition (start);
AudioSourceChannelInfo info (&buffer, bufferOffset, length);
const ScopedLock sl (callbackLock);
source->getNextAudioBlock (info);
}
int BufferingAudioSource::useTimeSlice()
{
return readNextBufferChunk() ? 1 : 100;
}
} // namespace juce

View File

@@ -1,134 +1,124 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource which takes another source as input, and buffers it using a thread.
Create this as a wrapper around another thread, and it will read-ahead with
a background thread to smooth out playback. You can either create one of these
directly, or use it indirectly using an AudioTransportSource.
@see PositionableAudioSource, AudioTransportSource
@tags{Audio}
*/
class JUCE_API BufferingAudioSource : public PositionableAudioSource,
private TimeSliceClient
{
public:
//==============================================================================
/** Creates a BufferingAudioSource.
@param source the input source to read from
@param backgroundThread a background thread that will be used for the
background read-ahead. This object must not be deleted
until after any BufferingAudioSources that are using it
have been deleted!
@param deleteSourceWhenDeleted if true, then the input source object will
be deleted when this object is deleted
@param numberOfSamplesToBuffer the size of buffer to use for reading ahead
@param numberOfChannels the number of channels that will be played
@param prefillBufferOnPrepareToPlay if true, then calling prepareToPlay on this object will
block until the buffer has been filled
*/
BufferingAudioSource (PositionableAudioSource* source,
TimeSliceThread& backgroundThread,
bool deleteSourceWhenDeleted,
int numberOfSamplesToBuffer,
int numberOfChannels = 2,
bool prefillBufferOnPrepareToPlay = true);
/** Destructor.
The input source may be deleted depending on whether the deleteSourceWhenDeleted
flag was set in the constructor.
*/
~BufferingAudioSource() override;
//==============================================================================
/** Implementation of the AudioSource method. */
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method. */
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
//==============================================================================
/** Implements the PositionableAudioSource method. */
void setNextReadPosition (int64 newPosition) override;
/** Implements the PositionableAudioSource method. */
int64 getNextReadPosition() const override;
/** Implements the PositionableAudioSource method. */
int64 getTotalLength() const override { return source->getTotalLength(); }
/** Implements the PositionableAudioSource method. */
bool isLooping() const override { return source->isLooping(); }
/** Implements the PositionableAudioSource method. */
void setLooping(bool shouldLoop) override { return source->setLooping(shouldLoop); }
/** Implements the PositionableAudioSource method. */
void setLoopRange (int64 loopStart, int64 loopLength) override;
/** Implements the PositionableAudioSource method. */
void getLoopRange (int64 & loopStart, int64 & loopLength) const override { return source->getLoopRange(loopStart, loopLength); }
/** A useful function to block until the next the buffer info can be filled.
This is useful for offline rendering.
*/
bool waitForNextAudioBlockReady (const AudioSourceChannelInfo& info, const uint32 timeout);
private:
//==============================================================================
Range<int> getValidBufferRange (int numSamples) const;
bool readNextBufferChunk();
void readBufferSection (int64 start, int length, int bufferOffset);
int useTimeSlice() override;
//==============================================================================
OptionalScopedPointer<PositionableAudioSource> source;
TimeSliceThread& backgroundThread;
int numberOfSamplesToBuffer, numberOfChannels;
AudioBuffer<float> buffer;
CriticalSection callbackLock, bufferRangeLock;
WaitableEvent bufferReadyEvent;
int64 bufferValidStart = 0, bufferValidEnd = 0;
std::atomic<int64> nextPlayPos { 0 };
double sampleRate = 0;
bool wasSourceLooping = false, isPrepared = false;
bool loopRangeChanged = false;
const bool prefillBuffer;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BufferingAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource which takes another source as input, and buffers it using a thread.
Create this as a wrapper around another thread, and it will read-ahead with
a background thread to smooth out playback. You can either create one of these
directly, or use it indirectly using an AudioTransportSource.
@see PositionableAudioSource, AudioTransportSource
@tags{Audio}
*/
class JUCE_API BufferingAudioSource : public PositionableAudioSource,
private TimeSliceClient
{
public:
//==============================================================================
/** Creates a BufferingAudioSource.
@param source the input source to read from
@param backgroundThread a background thread that will be used for the
background read-ahead. This object must not be deleted
until after any BufferingAudioSources that are using it
have been deleted!
@param deleteSourceWhenDeleted if true, then the input source object will
be deleted when this object is deleted
@param numberOfSamplesToBuffer the size of buffer to use for reading ahead
@param numberOfChannels the number of channels that will be played
@param prefillBufferOnPrepareToPlay if true, then calling prepareToPlay on this object will
block until the buffer has been filled
*/
BufferingAudioSource (PositionableAudioSource* source,
TimeSliceThread& backgroundThread,
bool deleteSourceWhenDeleted,
int numberOfSamplesToBuffer,
int numberOfChannels = 2,
bool prefillBufferOnPrepareToPlay = true);
/** Destructor.
The input source may be deleted depending on whether the deleteSourceWhenDeleted
flag was set in the constructor.
*/
~BufferingAudioSource() override;
//==============================================================================
/** Implementation of the AudioSource method. */
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method. */
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
//==============================================================================
/** Implements the PositionableAudioSource method. */
void setNextReadPosition (int64 newPosition) override;
/** Implements the PositionableAudioSource method. */
int64 getNextReadPosition() const override;
/** Implements the PositionableAudioSource method. */
int64 getTotalLength() const override { return source->getTotalLength(); }
/** Implements the PositionableAudioSource method. */
bool isLooping() const override { return source->isLooping(); }
/** A useful function to block until the next the buffer info can be filled.
This is useful for offline rendering.
*/
bool waitForNextAudioBlockReady (const AudioSourceChannelInfo& info, const uint32 timeout);
private:
//==============================================================================
Range<int> getValidBufferRange (int numSamples) const;
bool readNextBufferChunk();
void readBufferSection (int64 start, int length, int bufferOffset);
int useTimeSlice() override;
//==============================================================================
OptionalScopedPointer<PositionableAudioSource> source;
TimeSliceThread& backgroundThread;
int numberOfSamplesToBuffer, numberOfChannels;
AudioBuffer<float> buffer;
CriticalSection callbackLock, bufferRangeLock;
WaitableEvent bufferReadyEvent;
int64 bufferValidStart = 0, bufferValidEnd = 0;
std::atomic<int64> nextPlayPos { 0 };
double sampleRate = 0;
bool wasSourceLooping = false, isPrepared = false;
const bool prefillBuffer;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (BufferingAudioSource)
};
} // namespace juce

View File

@@ -1,187 +1,187 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ChannelRemappingAudioSource::ChannelRemappingAudioSource (AudioSource* const source_,
const bool deleteSourceWhenDeleted)
: source (source_, deleteSourceWhenDeleted),
requiredNumberOfChannels (2)
{
remappedInfo.buffer = &buffer;
remappedInfo.startSample = 0;
}
ChannelRemappingAudioSource::~ChannelRemappingAudioSource() {}
//==============================================================================
void ChannelRemappingAudioSource::setNumberOfChannelsToProduce (const int requiredNumberOfChannels_)
{
const ScopedLock sl (lock);
requiredNumberOfChannels = requiredNumberOfChannels_;
}
void ChannelRemappingAudioSource::clearAllMappings()
{
const ScopedLock sl (lock);
remappedInputs.clear();
remappedOutputs.clear();
}
void ChannelRemappingAudioSource::setInputChannelMapping (const int destIndex, const int sourceIndex)
{
const ScopedLock sl (lock);
while (remappedInputs.size() < destIndex)
remappedInputs.add (-1);
remappedInputs.set (destIndex, sourceIndex);
}
void ChannelRemappingAudioSource::setOutputChannelMapping (const int sourceIndex, const int destIndex)
{
const ScopedLock sl (lock);
while (remappedOutputs.size() < sourceIndex)
remappedOutputs.add (-1);
remappedOutputs.set (sourceIndex, destIndex);
}
int ChannelRemappingAudioSource::getRemappedInputChannel (const int inputChannelIndex) const
{
const ScopedLock sl (lock);
if (inputChannelIndex >= 0 && inputChannelIndex < remappedInputs.size())
return remappedInputs.getUnchecked (inputChannelIndex);
return -1;
}
int ChannelRemappingAudioSource::getRemappedOutputChannel (const int outputChannelIndex) const
{
const ScopedLock sl (lock);
if (outputChannelIndex >= 0 && outputChannelIndex < remappedOutputs.size())
return remappedOutputs .getUnchecked (outputChannelIndex);
return -1;
}
//==============================================================================
void ChannelRemappingAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
source->prepareToPlay (samplesPerBlockExpected, sampleRate);
}
void ChannelRemappingAudioSource::releaseResources()
{
source->releaseResources();
}
void ChannelRemappingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
const ScopedLock sl (lock);
buffer.setSize (requiredNumberOfChannels, bufferToFill.numSamples, false, false, true);
const int numChans = bufferToFill.buffer->getNumChannels();
for (int i = 0; i < buffer.getNumChannels(); ++i)
{
const int remappedChan = getRemappedInputChannel (i);
if (remappedChan >= 0 && remappedChan < numChans)
{
buffer.copyFrom (i, 0, *bufferToFill.buffer,
remappedChan,
bufferToFill.startSample,
bufferToFill.numSamples);
}
else
{
buffer.clear (i, 0, bufferToFill.numSamples);
}
}
remappedInfo.numSamples = bufferToFill.numSamples;
source->getNextAudioBlock (remappedInfo);
bufferToFill.clearActiveBufferRegion();
for (int i = 0; i < requiredNumberOfChannels; ++i)
{
const int remappedChan = getRemappedOutputChannel (i);
if (remappedChan >= 0 && remappedChan < numChans)
{
bufferToFill.buffer->addFrom (remappedChan, bufferToFill.startSample,
buffer, i, 0, bufferToFill.numSamples);
}
}
}
//==============================================================================
std::unique_ptr<XmlElement> ChannelRemappingAudioSource::createXml() const
{
auto e = std::make_unique<XmlElement> ("MAPPINGS");
String ins, outs;
const ScopedLock sl (lock);
for (int i = 0; i < remappedInputs.size(); ++i)
ins << remappedInputs.getUnchecked(i) << ' ';
for (int i = 0; i < remappedOutputs.size(); ++i)
outs << remappedOutputs.getUnchecked(i) << ' ';
e->setAttribute ("inputs", ins.trimEnd());
e->setAttribute ("outputs", outs.trimEnd());
return e;
}
void ChannelRemappingAudioSource::restoreFromXml (const XmlElement& e)
{
if (e.hasTagName ("MAPPINGS"))
{
const ScopedLock sl (lock);
clearAllMappings();
StringArray ins, outs;
ins.addTokens (e.getStringAttribute ("inputs"), false);
outs.addTokens (e.getStringAttribute ("outputs"), false);
for (int i = 0; i < ins.size(); ++i)
remappedInputs.add (ins[i].getIntValue());
for (int i = 0; i < outs.size(); ++i)
remappedOutputs.add (outs[i].getIntValue());
}
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ChannelRemappingAudioSource::ChannelRemappingAudioSource (AudioSource* const source_,
const bool deleteSourceWhenDeleted)
: source (source_, deleteSourceWhenDeleted),
requiredNumberOfChannels (2)
{
remappedInfo.buffer = &buffer;
remappedInfo.startSample = 0;
}
ChannelRemappingAudioSource::~ChannelRemappingAudioSource() {}
//==============================================================================
void ChannelRemappingAudioSource::setNumberOfChannelsToProduce (const int requiredNumberOfChannels_)
{
const ScopedLock sl (lock);
requiredNumberOfChannels = requiredNumberOfChannels_;
}
void ChannelRemappingAudioSource::clearAllMappings()
{
const ScopedLock sl (lock);
remappedInputs.clear();
remappedOutputs.clear();
}
void ChannelRemappingAudioSource::setInputChannelMapping (const int destIndex, const int sourceIndex)
{
const ScopedLock sl (lock);
while (remappedInputs.size() < destIndex)
remappedInputs.add (-1);
remappedInputs.set (destIndex, sourceIndex);
}
void ChannelRemappingAudioSource::setOutputChannelMapping (const int sourceIndex, const int destIndex)
{
const ScopedLock sl (lock);
while (remappedOutputs.size() < sourceIndex)
remappedOutputs.add (-1);
remappedOutputs.set (sourceIndex, destIndex);
}
int ChannelRemappingAudioSource::getRemappedInputChannel (const int inputChannelIndex) const
{
const ScopedLock sl (lock);
if (inputChannelIndex >= 0 && inputChannelIndex < remappedInputs.size())
return remappedInputs.getUnchecked (inputChannelIndex);
return -1;
}
int ChannelRemappingAudioSource::getRemappedOutputChannel (const int outputChannelIndex) const
{
const ScopedLock sl (lock);
if (outputChannelIndex >= 0 && outputChannelIndex < remappedOutputs.size())
return remappedOutputs .getUnchecked (outputChannelIndex);
return -1;
}
//==============================================================================
void ChannelRemappingAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
source->prepareToPlay (samplesPerBlockExpected, sampleRate);
}
void ChannelRemappingAudioSource::releaseResources()
{
source->releaseResources();
}
void ChannelRemappingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
const ScopedLock sl (lock);
buffer.setSize (requiredNumberOfChannels, bufferToFill.numSamples, false, false, true);
const int numChans = bufferToFill.buffer->getNumChannels();
for (int i = 0; i < buffer.getNumChannels(); ++i)
{
const int remappedChan = getRemappedInputChannel (i);
if (remappedChan >= 0 && remappedChan < numChans)
{
buffer.copyFrom (i, 0, *bufferToFill.buffer,
remappedChan,
bufferToFill.startSample,
bufferToFill.numSamples);
}
else
{
buffer.clear (i, 0, bufferToFill.numSamples);
}
}
remappedInfo.numSamples = bufferToFill.numSamples;
source->getNextAudioBlock (remappedInfo);
bufferToFill.clearActiveBufferRegion();
for (int i = 0; i < requiredNumberOfChannels; ++i)
{
const int remappedChan = getRemappedOutputChannel (i);
if (remappedChan >= 0 && remappedChan < numChans)
{
bufferToFill.buffer->addFrom (remappedChan, bufferToFill.startSample,
buffer, i, 0, bufferToFill.numSamples);
}
}
}
//==============================================================================
std::unique_ptr<XmlElement> ChannelRemappingAudioSource::createXml() const
{
auto e = std::make_unique<XmlElement> ("MAPPINGS");
String ins, outs;
const ScopedLock sl (lock);
for (int i = 0; i < remappedInputs.size(); ++i)
ins << remappedInputs.getUnchecked(i) << ' ';
for (int i = 0; i < remappedOutputs.size(); ++i)
outs << remappedOutputs.getUnchecked(i) << ' ';
e->setAttribute ("inputs", ins.trimEnd());
e->setAttribute ("outputs", outs.trimEnd());
return e;
}
void ChannelRemappingAudioSource::restoreFromXml (const XmlElement& e)
{
if (e.hasTagName ("MAPPINGS"))
{
const ScopedLock sl (lock);
clearAllMappings();
StringArray ins, outs;
ins.addTokens (e.getStringAttribute ("inputs"), false);
outs.addTokens (e.getStringAttribute ("outputs"), false);
for (int i = 0; i < ins.size(); ++i)
remappedInputs.add (ins[i].getIntValue());
for (int i = 0; i < outs.size(); ++i)
remappedOutputs.add (outs[i].getIntValue());
}
}
} // namespace juce

View File

@@ -1,141 +1,141 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that takes the audio from another source, and re-maps its
input and output channels to a different arrangement.
You can use this to increase or decrease the number of channels that an
audio source uses, or to re-order those channels.
Call the reset() method before using it to set up a default mapping, and then
the setInputChannelMapping() and setOutputChannelMapping() methods to
create an appropriate mapping, otherwise no channels will be connected and
it'll produce silence.
@see AudioSource
@tags{Audio}
*/
class ChannelRemappingAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a remapping source that will pass on audio from the given input.
@param source the input source to use. Make sure that this doesn't
get deleted before the ChannelRemappingAudioSource object
@param deleteSourceWhenDeleted if true, the input source will be deleted
when this object is deleted, if false, the caller is
responsible for its deletion
*/
ChannelRemappingAudioSource (AudioSource* source,
bool deleteSourceWhenDeleted);
/** Destructor. */
~ChannelRemappingAudioSource() override;
//==============================================================================
/** Specifies a number of channels that this audio source must produce from its
getNextAudioBlock() callback.
*/
void setNumberOfChannelsToProduce (int requiredNumberOfChannels);
/** Clears any mapped channels.
After this, no channels are mapped, so this object will produce silence. Create
some mappings with setInputChannelMapping() and setOutputChannelMapping().
*/
void clearAllMappings();
/** Creates an input channel mapping.
When the getNextAudioBlock() method is called, the data in channel sourceChannelIndex of the incoming
data will be sent to destChannelIndex of our input source.
@param destChannelIndex the index of an input channel in our input audio source (i.e. the
source specified when this object was created).
@param sourceChannelIndex the index of the input channel in the incoming audio data buffer
during our getNextAudioBlock() callback
*/
void setInputChannelMapping (int destChannelIndex,
int sourceChannelIndex);
/** Creates an output channel mapping.
When the getNextAudioBlock() method is called, the data returned in channel sourceChannelIndex by
our input audio source will be copied to channel destChannelIndex of the final buffer.
@param sourceChannelIndex the index of an output channel coming from our input audio source
(i.e. the source specified when this object was created).
@param destChannelIndex the index of the output channel in the incoming audio data buffer
during our getNextAudioBlock() callback
*/
void setOutputChannelMapping (int sourceChannelIndex,
int destChannelIndex);
/** Returns the channel from our input that will be sent to channel inputChannelIndex of
our input audio source.
*/
int getRemappedInputChannel (int inputChannelIndex) const;
/** Returns the output channel to which channel outputChannelIndex of our input audio
source will be sent to.
*/
int getRemappedOutputChannel (int outputChannelIndex) const;
//==============================================================================
/** Returns an XML object to encapsulate the state of the mappings.
@see restoreFromXml
*/
std::unique_ptr<XmlElement> createXml() const;
/** Restores the mappings from an XML object created by createXML().
@see createXml
*/
void restoreFromXml (const XmlElement&);
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
OptionalScopedPointer<AudioSource> source;
Array<int> remappedInputs, remappedOutputs;
int requiredNumberOfChannels;
AudioBuffer<float> buffer;
AudioSourceChannelInfo remappedInfo;
CriticalSection lock;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChannelRemappingAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that takes the audio from another source, and re-maps its
input and output channels to a different arrangement.
You can use this to increase or decrease the number of channels that an
audio source uses, or to re-order those channels.
Call the reset() method before using it to set up a default mapping, and then
the setInputChannelMapping() and setOutputChannelMapping() methods to
create an appropriate mapping, otherwise no channels will be connected and
it'll produce silence.
@see AudioSource
@tags{Audio}
*/
class ChannelRemappingAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a remapping source that will pass on audio from the given input.
@param source the input source to use. Make sure that this doesn't
get deleted before the ChannelRemappingAudioSource object
@param deleteSourceWhenDeleted if true, the input source will be deleted
when this object is deleted, if false, the caller is
responsible for its deletion
*/
ChannelRemappingAudioSource (AudioSource* source,
bool deleteSourceWhenDeleted);
/** Destructor. */
~ChannelRemappingAudioSource() override;
//==============================================================================
/** Specifies a number of channels that this audio source must produce from its
getNextAudioBlock() callback.
*/
void setNumberOfChannelsToProduce (int requiredNumberOfChannels);
/** Clears any mapped channels.
After this, no channels are mapped, so this object will produce silence. Create
some mappings with setInputChannelMapping() and setOutputChannelMapping().
*/
void clearAllMappings();
/** Creates an input channel mapping.
When the getNextAudioBlock() method is called, the data in channel sourceChannelIndex of the incoming
data will be sent to destChannelIndex of our input source.
@param destChannelIndex the index of an input channel in our input audio source (i.e. the
source specified when this object was created).
@param sourceChannelIndex the index of the input channel in the incoming audio data buffer
during our getNextAudioBlock() callback
*/
void setInputChannelMapping (int destChannelIndex,
int sourceChannelIndex);
/** Creates an output channel mapping.
When the getNextAudioBlock() method is called, the data returned in channel sourceChannelIndex by
our input audio source will be copied to channel destChannelIndex of the final buffer.
@param sourceChannelIndex the index of an output channel coming from our input audio source
(i.e. the source specified when this object was created).
@param destChannelIndex the index of the output channel in the incoming audio data buffer
during our getNextAudioBlock() callback
*/
void setOutputChannelMapping (int sourceChannelIndex,
int destChannelIndex);
/** Returns the channel from our input that will be sent to channel inputChannelIndex of
our input audio source.
*/
int getRemappedInputChannel (int inputChannelIndex) const;
/** Returns the output channel to which channel outputChannelIndex of our input audio
source will be sent to.
*/
int getRemappedOutputChannel (int outputChannelIndex) const;
//==============================================================================
/** Returns an XML object to encapsulate the state of the mappings.
@see restoreFromXml
*/
std::unique_ptr<XmlElement> createXml() const;
/** Restores the mappings from an XML object created by createXML().
@see createXml
*/
void restoreFromXml (const XmlElement&);
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
OptionalScopedPointer<AudioSource> source;
Array<int> remappedInputs, remappedOutputs;
int requiredNumberOfChannels;
AudioBuffer<float> buffer;
AudioSourceChannelInfo remappedInfo;
CriticalSection lock;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ChannelRemappingAudioSource)
};
} // namespace juce

View File

@@ -1,80 +1,80 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
IIRFilterAudioSource::IIRFilterAudioSource (AudioSource* const inputSource,
const bool deleteInputWhenDeleted)
: input (inputSource, deleteInputWhenDeleted)
{
jassert (inputSource != nullptr);
for (int i = 2; --i >= 0;)
iirFilters.add (new IIRFilter());
}
IIRFilterAudioSource::~IIRFilterAudioSource() {}
//==============================================================================
void IIRFilterAudioSource::setCoefficients (const IIRCoefficients& newCoefficients)
{
for (int i = iirFilters.size(); --i >= 0;)
iirFilters.getUnchecked(i)->setCoefficients (newCoefficients);
}
void IIRFilterAudioSource::makeInactive()
{
for (int i = iirFilters.size(); --i >= 0;)
iirFilters.getUnchecked(i)->makeInactive();
}
//==============================================================================
void IIRFilterAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
input->prepareToPlay (samplesPerBlockExpected, sampleRate);
for (int i = iirFilters.size(); --i >= 0;)
iirFilters.getUnchecked(i)->reset();
}
void IIRFilterAudioSource::releaseResources()
{
input->releaseResources();
}
void IIRFilterAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
input->getNextAudioBlock (bufferToFill);
const int numChannels = bufferToFill.buffer->getNumChannels();
while (numChannels > iirFilters.size())
iirFilters.add (new IIRFilter (*iirFilters.getUnchecked (0)));
for (int i = 0; i < numChannels; ++i)
iirFilters.getUnchecked(i)
->processSamples (bufferToFill.buffer->getWritePointer (i, bufferToFill.startSample),
bufferToFill.numSamples);
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
IIRFilterAudioSource::IIRFilterAudioSource (AudioSource* const inputSource,
const bool deleteInputWhenDeleted)
: input (inputSource, deleteInputWhenDeleted)
{
jassert (inputSource != nullptr);
for (int i = 2; --i >= 0;)
iirFilters.add (new IIRFilter());
}
IIRFilterAudioSource::~IIRFilterAudioSource() {}
//==============================================================================
void IIRFilterAudioSource::setCoefficients (const IIRCoefficients& newCoefficients)
{
for (int i = iirFilters.size(); --i >= 0;)
iirFilters.getUnchecked(i)->setCoefficients (newCoefficients);
}
void IIRFilterAudioSource::makeInactive()
{
for (int i = iirFilters.size(); --i >= 0;)
iirFilters.getUnchecked(i)->makeInactive();
}
//==============================================================================
void IIRFilterAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
input->prepareToPlay (samplesPerBlockExpected, sampleRate);
for (int i = iirFilters.size(); --i >= 0;)
iirFilters.getUnchecked(i)->reset();
}
void IIRFilterAudioSource::releaseResources()
{
input->releaseResources();
}
void IIRFilterAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
input->getNextAudioBlock (bufferToFill);
const int numChannels = bufferToFill.buffer->getNumChannels();
while (numChannels > iirFilters.size())
iirFilters.add (new IIRFilter (*iirFilters.getUnchecked (0)));
for (int i = 0; i < numChannels; ++i)
iirFilters.getUnchecked(i)
->processSamples (bufferToFill.buffer->getWritePointer (i, bufferToFill.startSample),
bufferToFill.numSamples);
}
} // namespace juce

View File

@@ -1,68 +1,68 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that performs an IIR filter on another source.
@tags{Audio}
*/
class JUCE_API IIRFilterAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a IIRFilterAudioSource for a given input source.
@param inputSource the input source to read from - this must not be null
@param deleteInputWhenDeleted if true, the input source will be deleted when
this object is deleted
*/
IIRFilterAudioSource (AudioSource* inputSource,
bool deleteInputWhenDeleted);
/** Destructor. */
~IIRFilterAudioSource() override;
//==============================================================================
/** Changes the filter to use the same parameters as the one being passed in. */
void setCoefficients (const IIRCoefficients& newCoefficients);
/** Calls IIRFilter::makeInactive() on all the filters being used internally. */
void makeInactive();
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
OptionalScopedPointer<AudioSource> input;
OwnedArray<IIRFilter> iirFilters;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (IIRFilterAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that performs an IIR filter on another source.
@tags{Audio}
*/
class JUCE_API IIRFilterAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a IIRFilterAudioSource for a given input source.
@param inputSource the input source to read from - this must not be null
@param deleteInputWhenDeleted if true, the input source will be deleted when
this object is deleted
*/
IIRFilterAudioSource (AudioSource* inputSource,
bool deleteInputWhenDeleted);
/** Destructor. */
~IIRFilterAudioSource() override;
//==============================================================================
/** Changes the filter to use the same parameters as the one being passed in. */
void setCoefficients (const IIRCoefficients& newCoefficients);
/** Calls IIRFilter::makeInactive() on all the filters being used internally. */
void makeInactive();
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
OptionalScopedPointer<AudioSource> input;
OwnedArray<IIRFilter> iirFilters;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (IIRFilterAudioSource)
};
} // namespace juce

View File

@@ -1,273 +1,261 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MemoryAudioSource::MemoryAudioSource (AudioBuffer<float>& bufferToUse, bool copyMemory, bool shouldLoop)
: isCurrentlyLooping (shouldLoop), loopStartPos(0), loopLen(bufferToUse.getNumSamples())
{
if (copyMemory)
buffer.makeCopyOf (bufferToUse);
else
buffer.setDataToReferTo (bufferToUse.getArrayOfWritePointers(),
bufferToUse.getNumChannels(),
bufferToUse.getNumSamples());
}
//==============================================================================
void MemoryAudioSource::prepareToPlay (int /*samplesPerBlockExpected*/, double /*sampleRate*/)
{
position = 0;
}
void MemoryAudioSource::releaseResources() {}
void MemoryAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
if (buffer.getNumSamples() == 0)
{
bufferToFill.clearActiveBufferRegion();
return;
}
auto& dst = *bufferToFill.buffer;
auto channels = jmin (dst.getNumChannels(), buffer.getNumChannels());
int max = 0, pos = 0;
auto n = buffer.getNumSamples();
auto m = bufferToFill.numSamples;
int i = position;
for (; (i < n || isCurrentlyLooping) && (pos < m); i += max)
{
max = jmin (m - pos, n - (i % n));
int ch = 0;
for (; ch < channels; ++ch)
dst.copyFrom (ch, bufferToFill.startSample + pos, buffer, ch, i % n, max);
for (; ch < dst.getNumChannels(); ++ch)
dst.clear (ch, bufferToFill.startSample + pos, max);
pos += max;
}
if (pos < m)
dst.clear (bufferToFill.startSample + pos, m - pos);
position = i;
}
//==============================================================================
void MemoryAudioSource::setNextReadPosition (int64 newPosition)
{
position = (int) newPosition;
}
int64 MemoryAudioSource::getNextReadPosition() const
{
return position;
}
int64 MemoryAudioSource::getTotalLength() const
{
return buffer.getNumSamples();
}
//==============================================================================
bool MemoryAudioSource::isLooping() const
{
return isCurrentlyLooping;
}
void MemoryAudioSource::setLooping (bool shouldLoop)
{
isCurrentlyLooping = shouldLoop;
}
void MemoryAudioSource::getLoopRange (int64 & loopStart, int64 & loopLength) const
{
loopStart = loopStartPos;
loopLength = loopLen;
}
void MemoryAudioSource::setLoopRange (int64 loopStart, int64 loopLength)
{
loopStartPos = jlimit((int64)0, (int64) (buffer.getNumSamples()-1) , loopStart);
loopLen = jlimit((int64)1, (int64) (buffer.getNumSamples() - loopStartPos), loopLength);
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
static bool operator== (const AudioBuffer<float>& a, const AudioBuffer<float>& b)
{
if (a.getNumChannels() != b.getNumChannels())
return false;
for (int channel = 0; channel < a.getNumChannels(); ++channel)
{
auto* aPtr = a.getReadPointer (channel);
auto* bPtr = b.getReadPointer (channel);
if (std::vector<float> (aPtr, aPtr + a.getNumSamples())
!= std::vector<float> (bPtr, bPtr + b.getNumSamples()))
{
return false;
}
}
return true;
}
struct MemoryAudioSourceTests : public UnitTest
{
MemoryAudioSourceTests() : UnitTest ("MemoryAudioSource", UnitTestCategories::audio) {}
void runTest() override
{
constexpr int blockSize = 512;
AudioBuffer<float> bufferToFill { 2, blockSize };
AudioSourceChannelInfo channelInfo { bufferToFill };
beginTest ("A zero-length buffer produces silence, whether or not looping is enabled");
{
for (const bool enableLooping : { false, true })
{
AudioBuffer<float> buffer;
MemoryAudioSource source { buffer, true, false };
source.setLooping (enableLooping);
source.prepareToPlay (blockSize, 44100.0);
for (int i = 0; i < 2; ++i)
{
play (source, channelInfo);
expect (isSilent (bufferToFill));
}
}
}
beginTest ("A short buffer without looping is played once and followed by silence");
{
auto buffer = getShortBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (false);
source.prepareToPlay (blockSize, 44100.0);
play (source, channelInfo);
auto copy = buffer;
copy.setSize (buffer.getNumChannels(), blockSize, true, true, false);
expect (bufferToFill == copy);
play (source, channelInfo);
expect (isSilent (bufferToFill));
}
beginTest ("A short buffer with looping is played multiple times");
{
auto buffer = getShortBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (true);
source.prepareToPlay (blockSize, 44100.0);
play (source, channelInfo);
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
expect (bufferToFill.getSample (0, sample + buffer.getNumSamples()) == buffer.getSample (0, sample));
expect (! isSilent (bufferToFill));
}
beginTest ("A long buffer without looping is played once");
{
auto buffer = getLongBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (false);
source.prepareToPlay (blockSize, 44100.0);
play (source, channelInfo);
auto copy = buffer;
copy.setSize (buffer.getNumChannels(), blockSize, true, true, false);
expect (bufferToFill == copy);
for (int i = 0; i < 10; ++i)
play (source, channelInfo);
expect (isSilent (bufferToFill));
}
beginTest ("A long buffer with looping is played multiple times");
{
auto buffer = getLongBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (true);
source.prepareToPlay (blockSize, 44100.0);
for (int i = 0; i < 100; ++i)
{
play (source, channelInfo);
expect (bufferToFill.getSample (0, 0) == buffer.getSample (0, (i * blockSize) % buffer.getNumSamples()));
}
}
}
static AudioBuffer<float> getTestBuffer (int length)
{
AudioBuffer<float> buffer { 2, length };
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
buffer.setSample (channel, sample, jmap ((float) sample, 0.0f, (float) length, -1.0f, 1.0f));
return buffer;
}
static AudioBuffer<float> getShortBuffer() { return getTestBuffer (5); }
static AudioBuffer<float> getLongBuffer() { return getTestBuffer (1000); }
static void play (MemoryAudioSource& source, AudioSourceChannelInfo& info)
{
info.clearActiveBufferRegion();
source.getNextAudioBlock (info);
}
static bool isSilent (const AudioBuffer<float>& b)
{
for (int channel = 0; channel < b.getNumChannels(); ++channel)
if (b.findMinMax (channel, 0, b.getNumSamples()) != Range<float>{})
return false;
return true;
}
};
static MemoryAudioSourceTests memoryAudioSourceTests;
#endif
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MemoryAudioSource::MemoryAudioSource (AudioBuffer<float>& bufferToUse, bool copyMemory, bool shouldLoop)
: isCurrentlyLooping (shouldLoop)
{
if (copyMemory)
buffer.makeCopyOf (bufferToUse);
else
buffer.setDataToReferTo (bufferToUse.getArrayOfWritePointers(),
bufferToUse.getNumChannels(),
bufferToUse.getNumSamples());
}
//==============================================================================
void MemoryAudioSource::prepareToPlay (int /*samplesPerBlockExpected*/, double /*sampleRate*/)
{
position = 0;
}
void MemoryAudioSource::releaseResources() {}
void MemoryAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
if (buffer.getNumSamples() == 0)
{
bufferToFill.clearActiveBufferRegion();
return;
}
auto& dst = *bufferToFill.buffer;
auto channels = jmin (dst.getNumChannels(), buffer.getNumChannels());
int max = 0, pos = 0;
auto n = buffer.getNumSamples();
auto m = bufferToFill.numSamples;
int i = position;
for (; (i < n || isCurrentlyLooping) && (pos < m); i += max)
{
max = jmin (m - pos, n - (i % n));
int ch = 0;
for (; ch < channels; ++ch)
dst.copyFrom (ch, bufferToFill.startSample + pos, buffer, ch, i % n, max);
for (; ch < dst.getNumChannels(); ++ch)
dst.clear (ch, bufferToFill.startSample + pos, max);
pos += max;
}
if (pos < m)
dst.clear (bufferToFill.startSample + pos, m - pos);
position = i;
}
//==============================================================================
void MemoryAudioSource::setNextReadPosition (int64 newPosition)
{
position = (int) newPosition;
}
int64 MemoryAudioSource::getNextReadPosition() const
{
return position;
}
int64 MemoryAudioSource::getTotalLength() const
{
return buffer.getNumSamples();
}
//==============================================================================
bool MemoryAudioSource::isLooping() const
{
return isCurrentlyLooping;
}
void MemoryAudioSource::setLooping (bool shouldLoop)
{
isCurrentlyLooping = shouldLoop;
}
//==============================================================================
//==============================================================================
#if JUCE_UNIT_TESTS
static bool operator== (const AudioBuffer<float>& a, const AudioBuffer<float>& b)
{
if (a.getNumChannels() != b.getNumChannels())
return false;
for (int channel = 0; channel < a.getNumChannels(); ++channel)
{
auto* aPtr = a.getReadPointer (channel);
auto* bPtr = b.getReadPointer (channel);
if (std::vector<float> (aPtr, aPtr + a.getNumSamples())
!= std::vector<float> (bPtr, bPtr + b.getNumSamples()))
{
return false;
}
}
return true;
}
struct MemoryAudioSourceTests : public UnitTest
{
MemoryAudioSourceTests() : UnitTest ("MemoryAudioSource", UnitTestCategories::audio) {}
void runTest() override
{
constexpr int blockSize = 512;
AudioBuffer<float> bufferToFill { 2, blockSize };
AudioSourceChannelInfo channelInfo { bufferToFill };
beginTest ("A zero-length buffer produces silence, whether or not looping is enabled");
{
for (const bool enableLooping : { false, true })
{
AudioBuffer<float> buffer;
MemoryAudioSource source { buffer, true, false };
source.setLooping (enableLooping);
source.prepareToPlay (blockSize, 44100.0);
for (int i = 0; i < 2; ++i)
{
play (source, channelInfo);
expect (isSilent (bufferToFill));
}
}
}
beginTest ("A short buffer without looping is played once and followed by silence");
{
auto buffer = getShortBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (false);
source.prepareToPlay (blockSize, 44100.0);
play (source, channelInfo);
auto copy = buffer;
copy.setSize (buffer.getNumChannels(), blockSize, true, true, false);
expect (bufferToFill == copy);
play (source, channelInfo);
expect (isSilent (bufferToFill));
}
beginTest ("A short buffer with looping is played multiple times");
{
auto buffer = getShortBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (true);
source.prepareToPlay (blockSize, 44100.0);
play (source, channelInfo);
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
expect (bufferToFill.getSample (0, sample + buffer.getNumSamples()) == buffer.getSample (0, sample));
expect (! isSilent (bufferToFill));
}
beginTest ("A long buffer without looping is played once");
{
auto buffer = getLongBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (false);
source.prepareToPlay (blockSize, 44100.0);
play (source, channelInfo);
auto copy = buffer;
copy.setSize (buffer.getNumChannels(), blockSize, true, true, false);
expect (bufferToFill == copy);
for (int i = 0; i < 10; ++i)
play (source, channelInfo);
expect (isSilent (bufferToFill));
}
beginTest ("A long buffer with looping is played multiple times");
{
auto buffer = getLongBuffer();
MemoryAudioSource source { buffer, true, false };
source.setLooping (true);
source.prepareToPlay (blockSize, 44100.0);
for (int i = 0; i < 100; ++i)
{
play (source, channelInfo);
expect (bufferToFill.getSample (0, 0) == buffer.getSample (0, (i * blockSize) % buffer.getNumSamples()));
}
}
}
static AudioBuffer<float> getTestBuffer (int length)
{
AudioBuffer<float> buffer { 2, length };
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
buffer.setSample (channel, sample, jmap ((float) sample, 0.0f, (float) length, -1.0f, 1.0f));
return buffer;
}
static AudioBuffer<float> getShortBuffer() { return getTestBuffer (5); }
static AudioBuffer<float> getLongBuffer() { return getTestBuffer (1000); }
static void play (MemoryAudioSource& source, AudioSourceChannelInfo& info)
{
info.clearActiveBufferRegion();
source.getNextAudioBlock (info);
}
static bool isSilent (const AudioBuffer<float>& b)
{
for (int channel = 0; channel < b.getNumChannels(); ++channel)
if (b.findMinMax (channel, 0, b.getNumSamples()) != Range<float>{})
return false;
return true;
}
};
static MemoryAudioSourceTests memoryAudioSourceTests;
#endif
} // namespace juce

View File

@@ -1,90 +1,82 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource which takes some float audio data as an input.
@tags{Audio}
*/
class JUCE_API MemoryAudioSource : public PositionableAudioSource
{
public:
//==============================================================================
/** Creates a MemoryAudioSource by providing an audio buffer.
If copyMemory is true then the buffer will be copied into an internal
buffer which will be owned by the MemoryAudioSource. If copyMemory is
false, then you must ensure that the lifetime of the audio buffer is
at least as long as the MemoryAudioSource.
*/
MemoryAudioSource (AudioBuffer<float>& audioBuffer, bool copyMemory, bool shouldLoop = false);
//==============================================================================
/** Implementation of the AudioSource method. */
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method. */
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override;
//==============================================================================
/** Implementation of the PositionableAudioSource method. */
void setNextReadPosition (int64 newPosition) override;
/** Implementation of the PositionableAudioSource method. */
int64 getNextReadPosition() const override;
/** Implementation of the PositionableAudioSource method. */
int64 getTotalLength() const override;
//==============================================================================
/** Implementation of the PositionableAudioSource method. */
bool isLooping() const override;
/** Implementation of the PositionableAudioSource method. */
void setLooping (bool shouldLoop) override;
/** Sets the start position of the looping in samples. */
void setLoopRange (int64 loopStart, int64 loopLength) override;
/** Returns the position where the loop playback starts. */
void getLoopRange(int64 & loopStart, int64 & loopLength) const override;
private:
//==============================================================================
AudioBuffer<float> buffer;
int position = 0;
bool isCurrentlyLooping;
int64 loopStartPos = 0;
int64 loopLen = 0;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MemoryAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource which takes some float audio data as an input.
@tags{Audio}
*/
class JUCE_API MemoryAudioSource : public PositionableAudioSource
{
public:
//==============================================================================
/** Creates a MemoryAudioSource by providing an audio buffer.
If copyMemory is true then the buffer will be copied into an internal
buffer which will be owned by the MemoryAudioSource. If copyMemory is
false, then you must ensure that the lifetime of the audio buffer is
at least as long as the MemoryAudioSource.
*/
MemoryAudioSource (AudioBuffer<float>& audioBuffer, bool copyMemory, bool shouldLoop = false);
//==============================================================================
/** Implementation of the AudioSource method. */
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method. */
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill) override;
//==============================================================================
/** Implementation of the PositionableAudioSource method. */
void setNextReadPosition (int64 newPosition) override;
/** Implementation of the PositionableAudioSource method. */
int64 getNextReadPosition() const override;
/** Implementation of the PositionableAudioSource method. */
int64 getTotalLength() const override;
//==============================================================================
/** Implementation of the PositionableAudioSource method. */
bool isLooping() const override;
/** Implementation of the PositionableAudioSource method. */
void setLooping (bool shouldLoop) override;
private:
//==============================================================================
AudioBuffer<float> buffer;
int position = 0;
bool isCurrentlyLooping;
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MemoryAudioSource)
};
} // namespace juce

View File

@@ -1,158 +1,158 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MixerAudioSource::MixerAudioSource()
: currentSampleRate (0.0), bufferSizeExpected (0)
{
}
MixerAudioSource::~MixerAudioSource()
{
removeAllInputs();
}
//==============================================================================
void MixerAudioSource::addInputSource (AudioSource* input, const bool deleteWhenRemoved)
{
if (input != nullptr && ! inputs.contains (input))
{
double localRate;
int localBufferSize;
{
const ScopedLock sl (lock);
localRate = currentSampleRate;
localBufferSize = bufferSizeExpected;
}
if (localRate > 0.0)
input->prepareToPlay (localBufferSize, localRate);
const ScopedLock sl (lock);
inputsToDelete.setBit (inputs.size(), deleteWhenRemoved);
inputs.add (input);
}
}
void MixerAudioSource::removeInputSource (AudioSource* const input)
{
if (input != nullptr)
{
std::unique_ptr<AudioSource> toDelete;
{
const ScopedLock sl (lock);
const int index = inputs.indexOf (input);
if (index < 0)
return;
if (inputsToDelete [index])
toDelete.reset (input);
inputsToDelete.shiftBits (-1, index);
inputs.remove (index);
}
input->releaseResources();
}
}
void MixerAudioSource::removeAllInputs()
{
OwnedArray<AudioSource> toDelete;
{
const ScopedLock sl (lock);
for (int i = inputs.size(); --i >= 0;)
if (inputsToDelete[i])
toDelete.add (inputs.getUnchecked(i));
inputs.clear();
}
for (int i = toDelete.size(); --i >= 0;)
toDelete.getUnchecked(i)->releaseResources();
}
void MixerAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
tempBuffer.setSize (2, samplesPerBlockExpected);
const ScopedLock sl (lock);
currentSampleRate = sampleRate;
bufferSizeExpected = samplesPerBlockExpected;
for (int i = inputs.size(); --i >= 0;)
inputs.getUnchecked(i)->prepareToPlay (samplesPerBlockExpected, sampleRate);
}
void MixerAudioSource::releaseResources()
{
const ScopedLock sl (lock);
for (int i = inputs.size(); --i >= 0;)
inputs.getUnchecked(i)->releaseResources();
tempBuffer.setSize (2, 0);
currentSampleRate = 0;
bufferSizeExpected = 0;
}
void MixerAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
const ScopedLock sl (lock);
if (inputs.size() > 0)
{
inputs.getUnchecked(0)->getNextAudioBlock (info);
if (inputs.size() > 1)
{
tempBuffer.setSize (jmax (1, info.buffer->getNumChannels()),
info.buffer->getNumSamples());
AudioSourceChannelInfo info2 (&tempBuffer, 0, info.numSamples);
for (int i = 1; i < inputs.size(); ++i)
{
inputs.getUnchecked(i)->getNextAudioBlock (info2);
for (int chan = 0; chan < info.buffer->getNumChannels(); ++chan)
info.buffer->addFrom (chan, info.startSample, tempBuffer, chan, 0, info.numSamples);
}
}
}
else
{
info.clearActiveBufferRegion();
}
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
MixerAudioSource::MixerAudioSource()
: currentSampleRate (0.0), bufferSizeExpected (0)
{
}
MixerAudioSource::~MixerAudioSource()
{
removeAllInputs();
}
//==============================================================================
void MixerAudioSource::addInputSource (AudioSource* input, const bool deleteWhenRemoved)
{
if (input != nullptr && ! inputs.contains (input))
{
double localRate;
int localBufferSize;
{
const ScopedLock sl (lock);
localRate = currentSampleRate;
localBufferSize = bufferSizeExpected;
}
if (localRate > 0.0)
input->prepareToPlay (localBufferSize, localRate);
const ScopedLock sl (lock);
inputsToDelete.setBit (inputs.size(), deleteWhenRemoved);
inputs.add (input);
}
}
void MixerAudioSource::removeInputSource (AudioSource* const input)
{
if (input != nullptr)
{
std::unique_ptr<AudioSource> toDelete;
{
const ScopedLock sl (lock);
const int index = inputs.indexOf (input);
if (index < 0)
return;
if (inputsToDelete [index])
toDelete.reset (input);
inputsToDelete.shiftBits (-1, index);
inputs.remove (index);
}
input->releaseResources();
}
}
void MixerAudioSource::removeAllInputs()
{
OwnedArray<AudioSource> toDelete;
{
const ScopedLock sl (lock);
for (int i = inputs.size(); --i >= 0;)
if (inputsToDelete[i])
toDelete.add (inputs.getUnchecked(i));
inputs.clear();
}
for (int i = toDelete.size(); --i >= 0;)
toDelete.getUnchecked(i)->releaseResources();
}
void MixerAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
tempBuffer.setSize (2, samplesPerBlockExpected);
const ScopedLock sl (lock);
currentSampleRate = sampleRate;
bufferSizeExpected = samplesPerBlockExpected;
for (int i = inputs.size(); --i >= 0;)
inputs.getUnchecked(i)->prepareToPlay (samplesPerBlockExpected, sampleRate);
}
void MixerAudioSource::releaseResources()
{
const ScopedLock sl (lock);
for (int i = inputs.size(); --i >= 0;)
inputs.getUnchecked(i)->releaseResources();
tempBuffer.setSize (2, 0);
currentSampleRate = 0;
bufferSizeExpected = 0;
}
void MixerAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
const ScopedLock sl (lock);
if (inputs.size() > 0)
{
inputs.getUnchecked(0)->getNextAudioBlock (info);
if (inputs.size() > 1)
{
tempBuffer.setSize (jmax (1, info.buffer->getNumChannels()),
info.buffer->getNumSamples());
AudioSourceChannelInfo info2 (&tempBuffer, 0, info.numSamples);
for (int i = 1; i < inputs.size(); ++i)
{
inputs.getUnchecked(i)->getNextAudioBlock (info2);
for (int chan = 0; chan < info.buffer->getNumChannels(); ++chan)
info.buffer->addFrom (chan, info.startSample, tempBuffer, chan, 0, info.numSamples);
}
}
}
else
{
info.clearActiveBufferRegion();
}
}
} // namespace juce

View File

@@ -1,99 +1,99 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that mixes together the output of a set of other AudioSources.
Input sources can be added and removed while the mixer is running as long as their
prepareToPlay() and releaseResources() methods are called before and after adding
them to the mixer.
@tags{Audio}
*/
class JUCE_API MixerAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a MixerAudioSource. */
MixerAudioSource();
/** Destructor. */
~MixerAudioSource() override;
//==============================================================================
/** Adds an input source to the mixer.
If the mixer is running you'll need to make sure that the input source
is ready to play by calling its prepareToPlay() method before adding it.
If the mixer is stopped, then its input sources will be automatically
prepared when the mixer's prepareToPlay() method is called.
@param newInput the source to add to the mixer
@param deleteWhenRemoved if true, then this source will be deleted when
no longer needed by the mixer.
*/
void addInputSource (AudioSource* newInput, bool deleteWhenRemoved);
/** Removes an input source.
If the source was added by calling addInputSource() with the deleteWhenRemoved
flag set, it will be deleted by this method.
*/
void removeInputSource (AudioSource* input);
/** Removes all the input sources.
Any sources which were added by calling addInputSource() with the deleteWhenRemoved
flag set will be deleted by this method.
*/
void removeAllInputs();
//==============================================================================
/** Implementation of the AudioSource method.
This will call prepareToPlay() on all its input sources.
*/
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method.
This will call releaseResources() on all its input sources.
*/
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
Array<AudioSource*> inputs;
BigInteger inputsToDelete;
CriticalSection lock;
AudioBuffer<float> tempBuffer;
double currentSampleRate;
int bufferSizeExpected;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MixerAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that mixes together the output of a set of other AudioSources.
Input sources can be added and removed while the mixer is running as long as their
prepareToPlay() and releaseResources() methods are called before and after adding
them to the mixer.
@tags{Audio}
*/
class JUCE_API MixerAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a MixerAudioSource. */
MixerAudioSource();
/** Destructor. */
~MixerAudioSource() override;
//==============================================================================
/** Adds an input source to the mixer.
If the mixer is running you'll need to make sure that the input source
is ready to play by calling its prepareToPlay() method before adding it.
If the mixer is stopped, then its input sources will be automatically
prepared when the mixer's prepareToPlay() method is called.
@param newInput the source to add to the mixer
@param deleteWhenRemoved if true, then this source will be deleted when
no longer needed by the mixer.
*/
void addInputSource (AudioSource* newInput, bool deleteWhenRemoved);
/** Removes an input source.
If the source was added by calling addInputSource() with the deleteWhenRemoved
flag set, it will be deleted by this method.
*/
void removeInputSource (AudioSource* input);
/** Removes all the input sources.
Any sources which were added by calling addInputSource() with the deleteWhenRemoved
flag set will be deleted by this method.
*/
void removeAllInputs();
//==============================================================================
/** Implementation of the AudioSource method.
This will call prepareToPlay() on all its input sources.
*/
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method.
This will call releaseResources() on all its input sources.
*/
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
Array<AudioSource*> inputs;
BigInteger inputsToDelete;
CriticalSection lock;
AudioBuffer<float> tempBuffer;
double currentSampleRate;
int bufferSizeExpected;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MixerAudioSource)
};
} // namespace juce

View File

@@ -1,83 +1,76 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A type of AudioSource which can be repositioned.
The basic AudioSource just streams continuously with no idea of a current
time or length, so the PositionableAudioSource is used for a finite stream
that has a current read position.
@see AudioSource, AudioTransportSource
@tags{Audio}
*/
class JUCE_API PositionableAudioSource : public AudioSource
{
protected:
//==============================================================================
/** Creates the PositionableAudioSource. */
PositionableAudioSource() = default;
public:
/** Destructor */
~PositionableAudioSource() override = default;
//==============================================================================
/** Tells the stream to move to a new position.
Calling this indicates that the next call to AudioSource::getNextAudioBlock()
should return samples from this position.
Note that this may be called on a different thread to getNextAudioBlock(),
so the subclass should make sure it's synchronised.
*/
virtual void setNextReadPosition (int64 newPosition) = 0;
/** Returns the position from which the next block will be returned.
@see setNextReadPosition
*/
virtual int64 getNextReadPosition() const = 0;
/** Returns the total length of the stream (in samples). */
virtual int64 getTotalLength() const = 0;
/** Returns true if this source is actually playing in a loop. */
virtual bool isLooping() const = 0;
/** Tells the source whether you'd like it to play in a loop. */
virtual void setLooping (bool shouldLoop) { ignoreUnused (shouldLoop); }
/** Sets the start position of the looping in samples. */
virtual void setLoopRange (int64 loopStart, int64 loopLength) { ignoreUnused(loopStart); ignoreUnused(loopLength); }
/** Returns the position where the loop playback starts. */
virtual void getLoopRange(int64 & loopStart, int64 & loopLength) const { };
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A type of AudioSource which can be repositioned.
The basic AudioSource just streams continuously with no idea of a current
time or length, so the PositionableAudioSource is used for a finite stream
that has a current read position.
@see AudioSource, AudioTransportSource
@tags{Audio}
*/
class JUCE_API PositionableAudioSource : public AudioSource
{
protected:
//==============================================================================
/** Creates the PositionableAudioSource. */
PositionableAudioSource() = default;
public:
/** Destructor */
~PositionableAudioSource() override = default;
//==============================================================================
/** Tells the stream to move to a new position.
Calling this indicates that the next call to AudioSource::getNextAudioBlock()
should return samples from this position.
Note that this may be called on a different thread to getNextAudioBlock(),
so the subclass should make sure it's synchronised.
*/
virtual void setNextReadPosition (int64 newPosition) = 0;
/** Returns the position from which the next block will be returned.
@see setNextReadPosition
*/
virtual int64 getNextReadPosition() const = 0;
/** Returns the total length of the stream (in samples). */
virtual int64 getTotalLength() const = 0;
/** Returns true if this source is actually playing in a loop. */
virtual bool isLooping() const = 0;
/** Tells the source whether you'd like it to play in a loop. */
virtual void setLooping (bool shouldLoop) { ignoreUnused (shouldLoop); }
};
} // namespace juce

View File

@@ -1,265 +1,265 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ResamplingAudioSource::ResamplingAudioSource (AudioSource* const inputSource,
const bool deleteInputWhenDeleted,
const int channels)
: input (inputSource, deleteInputWhenDeleted),
numChannels (channels)
{
jassert (input != nullptr);
zeromem (coefficients, sizeof (coefficients));
}
ResamplingAudioSource::~ResamplingAudioSource() {}
void ResamplingAudioSource::setResamplingRatio (const double samplesInPerOutputSample)
{
jassert (samplesInPerOutputSample > 0);
const SpinLock::ScopedLockType sl (ratioLock);
ratio = jmax (0.0, samplesInPerOutputSample);
}
void ResamplingAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
const SpinLock::ScopedLockType sl (ratioLock);
auto scaledBlockSize = roundToInt (samplesPerBlockExpected * ratio);
input->prepareToPlay (scaledBlockSize, sampleRate * ratio);
buffer.setSize (numChannels, scaledBlockSize + 32);
filterStates.calloc (numChannels);
srcBuffers.calloc (numChannels);
destBuffers.calloc (numChannels);
createLowPass (ratio);
flushBuffers();
}
void ResamplingAudioSource::flushBuffers()
{
const ScopedLock sl (callbackLock);
buffer.clear();
bufferPos = 0;
sampsInBuffer = 0;
subSampleOffset = 0.0;
resetFilters();
}
void ResamplingAudioSource::releaseResources()
{
input->releaseResources();
buffer.setSize (numChannels, 0);
}
void ResamplingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
const ScopedLock sl (callbackLock);
double localRatio;
{
const SpinLock::ScopedLockType ratioSl (ratioLock);
localRatio = ratio;
}
if (lastRatio != localRatio)
{
createLowPass (localRatio);
lastRatio = localRatio;
}
const int sampsNeeded = roundToInt (info.numSamples * localRatio) + 3;
int bufferSize = buffer.getNumSamples();
if (bufferSize < sampsNeeded + 8)
{
bufferPos %= bufferSize;
bufferSize = sampsNeeded + 32;
buffer.setSize (buffer.getNumChannels(), bufferSize, true, true);
}
bufferPos %= bufferSize;
int endOfBufferPos = bufferPos + sampsInBuffer;
const int channelsToProcess = jmin (numChannels, info.buffer->getNumChannels());
while (sampsNeeded > sampsInBuffer)
{
endOfBufferPos %= bufferSize;
int numToDo = jmin (sampsNeeded - sampsInBuffer,
bufferSize - endOfBufferPos);
AudioSourceChannelInfo readInfo (&buffer, endOfBufferPos, numToDo);
input->getNextAudioBlock (readInfo);
if (localRatio > 1.0001)
{
// for down-sampling, pre-apply the filter..
for (int i = channelsToProcess; --i >= 0;)
applyFilter (buffer.getWritePointer (i, endOfBufferPos), numToDo, filterStates[i]);
}
sampsInBuffer += numToDo;
endOfBufferPos += numToDo;
}
for (int channel = 0; channel < channelsToProcess; ++channel)
{
destBuffers[channel] = info.buffer->getWritePointer (channel, info.startSample);
srcBuffers[channel] = buffer.getReadPointer (channel);
}
int nextPos = (bufferPos + 1) % bufferSize;
for (int m = info.numSamples; --m >= 0;)
{
jassert (sampsInBuffer > 0 && nextPos != endOfBufferPos);
const float alpha = (float) subSampleOffset;
for (int channel = 0; channel < channelsToProcess; ++channel)
*destBuffers[channel]++ = srcBuffers[channel][bufferPos]
+ alpha * (srcBuffers[channel][nextPos] - srcBuffers[channel][bufferPos]);
subSampleOffset += localRatio;
while (subSampleOffset >= 1.0)
{
if (++bufferPos >= bufferSize)
bufferPos = 0;
--sampsInBuffer;
nextPos = (bufferPos + 1) % bufferSize;
subSampleOffset -= 1.0;
}
}
if (localRatio < 0.9999)
{
// for up-sampling, apply the filter after transposing..
for (int i = channelsToProcess; --i >= 0;)
applyFilter (info.buffer->getWritePointer (i, info.startSample), info.numSamples, filterStates[i]);
}
else if (localRatio <= 1.0001 && info.numSamples > 0)
{
// if the filter's not currently being applied, keep it stoked with the last couple of samples to avoid discontinuities
for (int i = channelsToProcess; --i >= 0;)
{
const float* const endOfBuffer = info.buffer->getReadPointer (i, info.startSample + info.numSamples - 1);
FilterState& fs = filterStates[i];
if (info.numSamples > 1)
{
fs.y2 = fs.x2 = *(endOfBuffer - 1);
}
else
{
fs.y2 = fs.y1;
fs.x2 = fs.x1;
}
fs.y1 = fs.x1 = *endOfBuffer;
}
}
jassert (sampsInBuffer >= 0);
}
void ResamplingAudioSource::createLowPass (const double frequencyRatio)
{
const double proportionalRate = (frequencyRatio > 1.0) ? 0.5 / frequencyRatio
: 0.5 * frequencyRatio;
const double n = 1.0 / std::tan (MathConstants<double>::pi * jmax (0.001, proportionalRate));
const double nSquared = n * n;
const double c1 = 1.0 / (1.0 + MathConstants<double>::sqrt2 * n + nSquared);
setFilterCoefficients (c1,
c1 * 2.0f,
c1,
1.0,
c1 * 2.0 * (1.0 - nSquared),
c1 * (1.0 - MathConstants<double>::sqrt2 * n + nSquared));
}
void ResamplingAudioSource::setFilterCoefficients (double c1, double c2, double c3, double c4, double c5, double c6)
{
const double a = 1.0 / c4;
c1 *= a;
c2 *= a;
c3 *= a;
c5 *= a;
c6 *= a;
coefficients[0] = c1;
coefficients[1] = c2;
coefficients[2] = c3;
coefficients[3] = c4;
coefficients[4] = c5;
coefficients[5] = c6;
}
void ResamplingAudioSource::resetFilters()
{
if (filterStates != nullptr)
filterStates.clear ((size_t) numChannels);
}
void ResamplingAudioSource::applyFilter (float* samples, int num, FilterState& fs)
{
while (--num >= 0)
{
const double in = *samples;
double out = coefficients[0] * in
+ coefficients[1] * fs.x1
+ coefficients[2] * fs.x2
- coefficients[4] * fs.y1
- coefficients[5] * fs.y2;
#if JUCE_INTEL
if (! (out < -1.0e-8 || out > 1.0e-8))
out = 0;
#endif
fs.x2 = fs.x1;
fs.x1 = in;
fs.y2 = fs.y1;
fs.y1 = out;
*samples++ = (float) out;
}
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ResamplingAudioSource::ResamplingAudioSource (AudioSource* const inputSource,
const bool deleteInputWhenDeleted,
const int channels)
: input (inputSource, deleteInputWhenDeleted),
numChannels (channels)
{
jassert (input != nullptr);
zeromem (coefficients, sizeof (coefficients));
}
ResamplingAudioSource::~ResamplingAudioSource() {}
void ResamplingAudioSource::setResamplingRatio (const double samplesInPerOutputSample)
{
jassert (samplesInPerOutputSample > 0);
const SpinLock::ScopedLockType sl (ratioLock);
ratio = jmax (0.0, samplesInPerOutputSample);
}
void ResamplingAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
const SpinLock::ScopedLockType sl (ratioLock);
auto scaledBlockSize = roundToInt (samplesPerBlockExpected * ratio);
input->prepareToPlay (scaledBlockSize, sampleRate * ratio);
buffer.setSize (numChannels, scaledBlockSize + 32);
filterStates.calloc (numChannels);
srcBuffers.calloc (numChannels);
destBuffers.calloc (numChannels);
createLowPass (ratio);
flushBuffers();
}
void ResamplingAudioSource::flushBuffers()
{
const ScopedLock sl (callbackLock);
buffer.clear();
bufferPos = 0;
sampsInBuffer = 0;
subSampleOffset = 0.0;
resetFilters();
}
void ResamplingAudioSource::releaseResources()
{
input->releaseResources();
buffer.setSize (numChannels, 0);
}
void ResamplingAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
const ScopedLock sl (callbackLock);
double localRatio;
{
const SpinLock::ScopedLockType ratioSl (ratioLock);
localRatio = ratio;
}
if (lastRatio != localRatio)
{
createLowPass (localRatio);
lastRatio = localRatio;
}
const int sampsNeeded = roundToInt (info.numSamples * localRatio) + 3;
int bufferSize = buffer.getNumSamples();
if (bufferSize < sampsNeeded + 8)
{
bufferPos %= bufferSize;
bufferSize = sampsNeeded + 32;
buffer.setSize (buffer.getNumChannels(), bufferSize, true, true);
}
bufferPos %= bufferSize;
int endOfBufferPos = bufferPos + sampsInBuffer;
const int channelsToProcess = jmin (numChannels, info.buffer->getNumChannels());
while (sampsNeeded > sampsInBuffer)
{
endOfBufferPos %= bufferSize;
int numToDo = jmin (sampsNeeded - sampsInBuffer,
bufferSize - endOfBufferPos);
AudioSourceChannelInfo readInfo (&buffer, endOfBufferPos, numToDo);
input->getNextAudioBlock (readInfo);
if (localRatio > 1.0001)
{
// for down-sampling, pre-apply the filter..
for (int i = channelsToProcess; --i >= 0;)
applyFilter (buffer.getWritePointer (i, endOfBufferPos), numToDo, filterStates[i]);
}
sampsInBuffer += numToDo;
endOfBufferPos += numToDo;
}
for (int channel = 0; channel < channelsToProcess; ++channel)
{
destBuffers[channel] = info.buffer->getWritePointer (channel, info.startSample);
srcBuffers[channel] = buffer.getReadPointer (channel);
}
int nextPos = (bufferPos + 1) % bufferSize;
for (int m = info.numSamples; --m >= 0;)
{
jassert (sampsInBuffer > 0 && nextPos != endOfBufferPos);
const float alpha = (float) subSampleOffset;
for (int channel = 0; channel < channelsToProcess; ++channel)
*destBuffers[channel]++ = srcBuffers[channel][bufferPos]
+ alpha * (srcBuffers[channel][nextPos] - srcBuffers[channel][bufferPos]);
subSampleOffset += localRatio;
while (subSampleOffset >= 1.0)
{
if (++bufferPos >= bufferSize)
bufferPos = 0;
--sampsInBuffer;
nextPos = (bufferPos + 1) % bufferSize;
subSampleOffset -= 1.0;
}
}
if (localRatio < 0.9999)
{
// for up-sampling, apply the filter after transposing..
for (int i = channelsToProcess; --i >= 0;)
applyFilter (info.buffer->getWritePointer (i, info.startSample), info.numSamples, filterStates[i]);
}
else if (localRatio <= 1.0001 && info.numSamples > 0)
{
// if the filter's not currently being applied, keep it stoked with the last couple of samples to avoid discontinuities
for (int i = channelsToProcess; --i >= 0;)
{
const float* const endOfBuffer = info.buffer->getReadPointer (i, info.startSample + info.numSamples - 1);
FilterState& fs = filterStates[i];
if (info.numSamples > 1)
{
fs.y2 = fs.x2 = *(endOfBuffer - 1);
}
else
{
fs.y2 = fs.y1;
fs.x2 = fs.x1;
}
fs.y1 = fs.x1 = *endOfBuffer;
}
}
jassert (sampsInBuffer >= 0);
}
void ResamplingAudioSource::createLowPass (const double frequencyRatio)
{
const double proportionalRate = (frequencyRatio > 1.0) ? 0.5 / frequencyRatio
: 0.5 * frequencyRatio;
const double n = 1.0 / std::tan (MathConstants<double>::pi * jmax (0.001, proportionalRate));
const double nSquared = n * n;
const double c1 = 1.0 / (1.0 + MathConstants<double>::sqrt2 * n + nSquared);
setFilterCoefficients (c1,
c1 * 2.0f,
c1,
1.0,
c1 * 2.0 * (1.0 - nSquared),
c1 * (1.0 - MathConstants<double>::sqrt2 * n + nSquared));
}
void ResamplingAudioSource::setFilterCoefficients (double c1, double c2, double c3, double c4, double c5, double c6)
{
const double a = 1.0 / c4;
c1 *= a;
c2 *= a;
c3 *= a;
c5 *= a;
c6 *= a;
coefficients[0] = c1;
coefficients[1] = c2;
coefficients[2] = c3;
coefficients[3] = c4;
coefficients[4] = c5;
coefficients[5] = c6;
}
void ResamplingAudioSource::resetFilters()
{
if (filterStates != nullptr)
filterStates.clear ((size_t) numChannels);
}
void ResamplingAudioSource::applyFilter (float* samples, int num, FilterState& fs)
{
while (--num >= 0)
{
const double in = *samples;
double out = coefficients[0] * in
+ coefficients[1] * fs.x1
+ coefficients[2] * fs.x2
- coefficients[4] * fs.y1
- coefficients[5] * fs.y2;
#if JUCE_INTEL
if (! (out < -1.0e-8 || out > 1.0e-8))
out = 0;
#endif
fs.x2 = fs.x1;
fs.x1 = in;
fs.y2 = fs.y1;
fs.y1 = out;
*samples++ = (float) out;
}
}
} // namespace juce

View File

@@ -1,106 +1,106 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A type of AudioSource that takes an input source and changes its sample rate.
@see AudioSource, LagrangeInterpolator, CatmullRomInterpolator
@tags{Audio}
*/
class JUCE_API ResamplingAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a ResamplingAudioSource for a given input source.
@param inputSource the input source to read from
@param deleteInputWhenDeleted if true, the input source will be deleted when
this object is deleted
@param numChannels the number of channels to process
*/
ResamplingAudioSource (AudioSource* inputSource,
bool deleteInputWhenDeleted,
int numChannels = 2);
/** Destructor. */
~ResamplingAudioSource() override;
/** Changes the resampling ratio.
(This value can be changed at any time, even while the source is running).
@param samplesInPerOutputSample if set to 1.0, the input is passed through; higher
values will speed it up; lower values will slow it
down. The ratio must be greater than 0
*/
void setResamplingRatio (double samplesInPerOutputSample);
/** Returns the current resampling ratio.
This is the value that was set by setResamplingRatio().
*/
double getResamplingRatio() const noexcept { return ratio; }
/** Clears any buffers and filters that the resampler is using. */
void flushBuffers();
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
OptionalScopedPointer<AudioSource> input;
double ratio = 1.0, lastRatio = 1.0;
AudioBuffer<float> buffer;
int bufferPos = 0, sampsInBuffer = 0;
double subSampleOffset = 0.0;
double coefficients[6];
SpinLock ratioLock;
CriticalSection callbackLock;
const int numChannels;
HeapBlock<float*> destBuffers;
HeapBlock<const float*> srcBuffers;
void setFilterCoefficients (double c1, double c2, double c3, double c4, double c5, double c6);
void createLowPass (double proportionalRate);
struct FilterState
{
double x1, x2, y1, y2;
};
HeapBlock<FilterState> filterStates;
void resetFilters();
void applyFilter (float* samples, int num, FilterState& fs);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ResamplingAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A type of AudioSource that takes an input source and changes its sample rate.
@see AudioSource, LagrangeInterpolator, CatmullRomInterpolator
@tags{Audio}
*/
class JUCE_API ResamplingAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a ResamplingAudioSource for a given input source.
@param inputSource the input source to read from
@param deleteInputWhenDeleted if true, the input source will be deleted when
this object is deleted
@param numChannels the number of channels to process
*/
ResamplingAudioSource (AudioSource* inputSource,
bool deleteInputWhenDeleted,
int numChannels = 2);
/** Destructor. */
~ResamplingAudioSource() override;
/** Changes the resampling ratio.
(This value can be changed at any time, even while the source is running).
@param samplesInPerOutputSample if set to 1.0, the input is passed through; higher
values will speed it up; lower values will slow it
down. The ratio must be greater than 0
*/
void setResamplingRatio (double samplesInPerOutputSample);
/** Returns the current resampling ratio.
This is the value that was set by setResamplingRatio().
*/
double getResamplingRatio() const noexcept { return ratio; }
/** Clears any buffers and filters that the resampler is using. */
void flushBuffers();
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
OptionalScopedPointer<AudioSource> input;
double ratio = 1.0, lastRatio = 1.0;
AudioBuffer<float> buffer;
int bufferPos = 0, sampsInBuffer = 0;
double subSampleOffset = 0.0;
double coefficients[6];
SpinLock ratioLock;
CriticalSection callbackLock;
const int numChannels;
HeapBlock<float*> destBuffers;
HeapBlock<const float*> srcBuffers;
void setFilterCoefficients (double c1, double c2, double c3, double c4, double c5, double c6);
void createLowPass (double proportionalRate);
struct FilterState
{
double x1, x2, y1, y2;
};
HeapBlock<FilterState> filterStates;
void resetFilters();
void applyFilter (float* samples, int num, FilterState& fs);
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ResamplingAudioSource)
};
} // namespace juce

View File

@@ -1,83 +1,83 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ReverbAudioSource::ReverbAudioSource (AudioSource* const inputSource, const bool deleteInputWhenDeleted)
: input (inputSource, deleteInputWhenDeleted),
bypass (false)
{
jassert (inputSource != nullptr);
}
ReverbAudioSource::~ReverbAudioSource() {}
void ReverbAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
const ScopedLock sl (lock);
input->prepareToPlay (samplesPerBlockExpected, sampleRate);
reverb.setSampleRate (sampleRate);
}
void ReverbAudioSource::releaseResources() {}
void ReverbAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
const ScopedLock sl (lock);
input->getNextAudioBlock (bufferToFill);
if (! bypass)
{
float* const firstChannel = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
if (bufferToFill.buffer->getNumChannels() > 1)
{
reverb.processStereo (firstChannel,
bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample),
bufferToFill.numSamples);
}
else
{
reverb.processMono (firstChannel, bufferToFill.numSamples);
}
}
}
void ReverbAudioSource::setParameters (const Reverb::Parameters& newParams)
{
const ScopedLock sl (lock);
reverb.setParameters (newParams);
}
void ReverbAudioSource::setBypassed (bool b) noexcept
{
if (bypass != b)
{
const ScopedLock sl (lock);
bypass = b;
reverb.reset();
}
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ReverbAudioSource::ReverbAudioSource (AudioSource* const inputSource, const bool deleteInputWhenDeleted)
: input (inputSource, deleteInputWhenDeleted),
bypass (false)
{
jassert (inputSource != nullptr);
}
ReverbAudioSource::~ReverbAudioSource() {}
void ReverbAudioSource::prepareToPlay (int samplesPerBlockExpected, double sampleRate)
{
const ScopedLock sl (lock);
input->prepareToPlay (samplesPerBlockExpected, sampleRate);
reverb.setSampleRate (sampleRate);
}
void ReverbAudioSource::releaseResources() {}
void ReverbAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& bufferToFill)
{
const ScopedLock sl (lock);
input->getNextAudioBlock (bufferToFill);
if (! bypass)
{
float* const firstChannel = bufferToFill.buffer->getWritePointer (0, bufferToFill.startSample);
if (bufferToFill.buffer->getNumChannels() > 1)
{
reverb.processStereo (firstChannel,
bufferToFill.buffer->getWritePointer (1, bufferToFill.startSample),
bufferToFill.numSamples);
}
else
{
reverb.processMono (firstChannel, bufferToFill.numSamples);
}
}
}
void ReverbAudioSource::setParameters (const Reverb::Parameters& newParams)
{
const ScopedLock sl (lock);
reverb.setParameters (newParams);
}
void ReverbAudioSource::setBypassed (bool b) noexcept
{
if (bypass != b)
{
const ScopedLock sl (lock);
bypass = b;
reverb.reset();
}
}
} // namespace juce

View File

@@ -1,74 +1,74 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that uses the Reverb class to apply a reverb to another AudioSource.
@see Reverb
@tags{Audio}
*/
class JUCE_API ReverbAudioSource : public AudioSource
{
public:
/** Creates a ReverbAudioSource to process a given input source.
@param inputSource the input source to read from - this must not be null
@param deleteInputWhenDeleted if true, the input source will be deleted when
this object is deleted
*/
ReverbAudioSource (AudioSource* inputSource,
bool deleteInputWhenDeleted);
/** Destructor. */
~ReverbAudioSource() override;
//==============================================================================
/** Returns the parameters from the reverb. */
const Reverb::Parameters& getParameters() const noexcept { return reverb.getParameters(); }
/** Changes the reverb's parameters. */
void setParameters (const Reverb::Parameters& newParams);
void setBypassed (bool isBypassed) noexcept;
bool isBypassed() const noexcept { return bypass; }
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
CriticalSection lock;
OptionalScopedPointer<AudioSource> input;
Reverb reverb;
std::atomic<bool> bypass;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ReverbAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
An AudioSource that uses the Reverb class to apply a reverb to another AudioSource.
@see Reverb
@tags{Audio}
*/
class JUCE_API ReverbAudioSource : public AudioSource
{
public:
/** Creates a ReverbAudioSource to process a given input source.
@param inputSource the input source to read from - this must not be null
@param deleteInputWhenDeleted if true, the input source will be deleted when
this object is deleted
*/
ReverbAudioSource (AudioSource* inputSource,
bool deleteInputWhenDeleted);
/** Destructor. */
~ReverbAudioSource() override;
//==============================================================================
/** Returns the parameters from the reverb. */
const Reverb::Parameters& getParameters() const noexcept { return reverb.getParameters(); }
/** Changes the reverb's parameters. */
void setParameters (const Reverb::Parameters& newParams);
void setBypassed (bool isBypassed) noexcept;
bool isBypassed() const noexcept { return bypass; }
//==============================================================================
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
void releaseResources() override;
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
CriticalSection lock;
OptionalScopedPointer<AudioSource> input;
Reverb reverb;
std::atomic<bool> bypass;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ReverbAudioSource)
};
} // namespace juce

View File

@@ -1,78 +1,78 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ToneGeneratorAudioSource::ToneGeneratorAudioSource()
: frequency (1000.0),
sampleRate (44100.0),
currentPhase (0.0),
phasePerSample (0.0),
amplitude (0.5f)
{
}
ToneGeneratorAudioSource::~ToneGeneratorAudioSource()
{
}
//==============================================================================
void ToneGeneratorAudioSource::setAmplitude (const float newAmplitude)
{
amplitude = newAmplitude;
}
void ToneGeneratorAudioSource::setFrequency (const double newFrequencyHz)
{
frequency = newFrequencyHz;
phasePerSample = 0.0;
}
//==============================================================================
void ToneGeneratorAudioSource::prepareToPlay (int /*samplesPerBlockExpected*/, double rate)
{
currentPhase = 0.0;
phasePerSample = 0.0;
sampleRate = rate;
}
void ToneGeneratorAudioSource::releaseResources()
{
}
void ToneGeneratorAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
if (phasePerSample == 0.0)
phasePerSample = MathConstants<double>::twoPi / (sampleRate / frequency);
for (int i = 0; i < info.numSamples; ++i)
{
const float sample = amplitude * (float) std::sin (currentPhase);
currentPhase += phasePerSample;
for (int j = info.buffer->getNumChannels(); --j >= 0;)
info.buffer->setSample (j, info.startSample + i, sample);
}
}
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
ToneGeneratorAudioSource::ToneGeneratorAudioSource()
: frequency (1000.0),
sampleRate (44100.0),
currentPhase (0.0),
phasePerSample (0.0),
amplitude (0.5f)
{
}
ToneGeneratorAudioSource::~ToneGeneratorAudioSource()
{
}
//==============================================================================
void ToneGeneratorAudioSource::setAmplitude (const float newAmplitude)
{
amplitude = newAmplitude;
}
void ToneGeneratorAudioSource::setFrequency (const double newFrequencyHz)
{
frequency = newFrequencyHz;
phasePerSample = 0.0;
}
//==============================================================================
void ToneGeneratorAudioSource::prepareToPlay (int /*samplesPerBlockExpected*/, double rate)
{
currentPhase = 0.0;
phasePerSample = 0.0;
sampleRate = rate;
}
void ToneGeneratorAudioSource::releaseResources()
{
}
void ToneGeneratorAudioSource::getNextAudioBlock (const AudioSourceChannelInfo& info)
{
if (phasePerSample == 0.0)
phasePerSample = MathConstants<double>::twoPi / (sampleRate / frequency);
for (int i = 0; i < info.numSamples; ++i)
{
const float sample = amplitude * (float) std::sin (currentPhase);
currentPhase += phasePerSample;
for (int j = info.buffer->getNumChannels(); --j >= 0;)
info.buffer->setSample (j, info.startSample + i, sample);
}
}
} // namespace juce

View File

@@ -1,71 +1,71 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A simple AudioSource that generates a sine wave.
@tags{Audio}
*/
class JUCE_API ToneGeneratorAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a ToneGeneratorAudioSource. */
ToneGeneratorAudioSource();
/** Destructor. */
~ToneGeneratorAudioSource() override;
//==============================================================================
/** Sets the signal's amplitude. */
void setAmplitude (float newAmplitude);
/** Sets the signal's frequency. */
void setFrequency (double newFrequencyHz);
//==============================================================================
/** Implementation of the AudioSource method. */
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method. */
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
double frequency, sampleRate;
double currentPhase, phasePerSample;
float amplitude;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ToneGeneratorAudioSource)
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A simple AudioSource that generates a sine wave.
@tags{Audio}
*/
class JUCE_API ToneGeneratorAudioSource : public AudioSource
{
public:
//==============================================================================
/** Creates a ToneGeneratorAudioSource. */
ToneGeneratorAudioSource();
/** Destructor. */
~ToneGeneratorAudioSource() override;
//==============================================================================
/** Sets the signal's amplitude. */
void setAmplitude (float newAmplitude);
/** Sets the signal's frequency. */
void setFrequency (double newFrequencyHz);
//==============================================================================
/** Implementation of the AudioSource method. */
void prepareToPlay (int samplesPerBlockExpected, double sampleRate) override;
/** Implementation of the AudioSource method. */
void releaseResources() override;
/** Implementation of the AudioSource method. */
void getNextAudioBlock (const AudioSourceChannelInfo&) override;
private:
//==============================================================================
double frequency, sampleRate;
double currentPhase, phasePerSample;
float amplitude;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ToneGeneratorAudioSource)
};
} // namespace juce

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,269 +1,297 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A very simple ADSR envelope class.
To use it, call setSampleRate() with the current sample rate and give it some parameters
with setParameters() then call getNextSample() to get the envelope value to be applied
to each audio sample or applyEnvelopeToBuffer() to apply the envelope to a whole buffer.
@tags{Audio}
*/
class JUCE_API ADSR
{
public:
//==============================================================================
ADSR()
{
recalculateRates();
}
//==============================================================================
/**
Holds the parameters being used by an ADSR object.
@tags{Audio}
*/
struct JUCE_API Parameters
{
Parameters() = default;
Parameters (float attackTimeSeconds,
float decayTimeSeconds,
float sustainLevel,
float releaseTimeSeconds)
: attack (attackTimeSeconds),
decay (decayTimeSeconds),
sustain (sustainLevel),
release (releaseTimeSeconds)
{
}
float attack = 0.1f, decay = 0.1f, sustain = 1.0f, release = 0.1f;
};
/** Sets the parameters that will be used by an ADSR object.
You must have called setSampleRate() with the correct sample rate before
this otherwise the values may be incorrect!
@see getParameters
*/
void setParameters (const Parameters& newParameters)
{
// need to call setSampleRate() first!
jassert (sampleRate > 0.0);
parameters = newParameters;
recalculateRates();
}
/** Returns the parameters currently being used by an ADSR object.
@see setParameters
*/
const Parameters& getParameters() const noexcept { return parameters; }
/** Returns true if the envelope is in its attack, decay, sustain or release stage. */
bool isActive() const noexcept { return state != State::idle; }
//==============================================================================
/** Sets the sample rate that will be used for the envelope.
This must be called before the getNextSample() or setParameters() methods.
*/
void setSampleRate (double newSampleRate) noexcept
{
jassert (newSampleRate > 0.0);
sampleRate = newSampleRate;
}
//==============================================================================
/** Resets the envelope to an idle state. */
void reset() noexcept
{
envelopeVal = 0.0f;
state = State::idle;
}
/** Starts the attack phase of the envelope. */
void noteOn() noexcept
{
if (attackRate > 0.0f)
{
state = State::attack;
}
else if (decayRate > 0.0f)
{
envelopeVal = 1.0f;
state = State::decay;
}
else
{
state = State::sustain;
}
}
/** Starts the release phase of the envelope. */
void noteOff() noexcept
{
if (state != State::idle)
{
if (parameters.release > 0.0f)
{
releaseRate = (float) (envelopeVal / (parameters.release * sampleRate));
state = State::release;
}
else
{
reset();
}
}
}
//==============================================================================
/** Returns the next sample value for an ADSR object.
@see applyEnvelopeToBuffer
*/
float getNextSample() noexcept
{
if (state == State::idle)
return 0.0f;
if (state == State::attack)
{
envelopeVal += attackRate;
if (envelopeVal >= 1.0f)
{
envelopeVal = 1.0f;
goToNextState();
}
}
else if (state == State::decay)
{
envelopeVal -= decayRate;
if (envelopeVal <= parameters.sustain)
{
envelopeVal = parameters.sustain;
goToNextState();
}
}
else if (state == State::sustain)
{
envelopeVal = parameters.sustain;
}
else if (state == State::release)
{
envelopeVal -= releaseRate;
if (envelopeVal <= 0.0f)
goToNextState();
}
return envelopeVal;
}
/** This method will conveniently apply the next numSamples number of envelope values
to an AudioBuffer.
@see getNextSample
*/
template <typename FloatType>
void applyEnvelopeToBuffer (AudioBuffer<FloatType>& buffer, int startSample, int numSamples)
{
jassert (startSample + numSamples <= buffer.getNumSamples());
if (state == State::idle)
{
buffer.clear (startSample, numSamples);
return;
}
if (state == State::sustain)
{
buffer.applyGain (startSample, numSamples, parameters.sustain);
return;
}
auto numChannels = buffer.getNumChannels();
while (--numSamples >= 0)
{
auto env = getNextSample();
for (int i = 0; i < numChannels; ++i)
buffer.getWritePointer (i)[startSample] *= env;
++startSample;
}
}
private:
//==============================================================================
void recalculateRates() noexcept
{
auto getRate = [] (float distance, float timeInSeconds, double sr)
{
return timeInSeconds > 0.0f ? (float) (distance / (timeInSeconds * sr)) : -1.0f;
};
attackRate = getRate (1.0f, parameters.attack, sampleRate);
decayRate = getRate (1.0f - parameters.sustain, parameters.decay, sampleRate);
releaseRate = getRate (parameters.sustain, parameters.release, sampleRate);
if ((state == State::attack && attackRate <= 0.0f)
|| (state == State::decay && (decayRate <= 0.0f || envelopeVal <= parameters.sustain))
|| (state == State::release && releaseRate <= 0.0f))
{
goToNextState();
}
}
void goToNextState() noexcept
{
if (state == State::attack)
state = (decayRate > 0.0f ? State::decay : State::sustain);
else if (state == State::decay)
state = State::sustain;
else if (state == State::release)
reset();
}
//==============================================================================
enum class State { idle, attack, decay, sustain, release };
State state = State::idle;
Parameters parameters;
double sampleRate = 44100.0;
float envelopeVal = 0.0f, attackRate = 0.0f, decayRate = 0.0f, releaseRate = 0.0f;
};
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
//==============================================================================
/**
A very simple ADSR envelope class.
To use it, call setSampleRate() with the current sample rate and give it some parameters
with setParameters() then call getNextSample() to get the envelope value to be applied
to each audio sample or applyEnvelopeToBuffer() to apply the envelope to a whole buffer.
Do not change the parameters during playback. If you change the parameters before the
release stage has completed then you must call reset() before the next call to
noteOn().
@tags{Audio}
*/
class JUCE_API ADSR
{
public:
//==============================================================================
ADSR()
{
recalculateRates();
}
//==============================================================================
/**
Holds the parameters being used by an ADSR object.
@tags{Audio}
*/
struct JUCE_API Parameters
{
Parameters() = default;
Parameters (float attackTimeSeconds,
float decayTimeSeconds,
float sustainLevel,
float releaseTimeSeconds)
: attack (attackTimeSeconds),
decay (decayTimeSeconds),
sustain (sustainLevel),
release (releaseTimeSeconds)
{
}
float attack = 0.1f, decay = 0.1f, sustain = 1.0f, release = 0.1f;
};
/** Sets the parameters that will be used by an ADSR object.
You must have called setSampleRate() with the correct sample rate before
this otherwise the values may be incorrect!
@see getParameters
*/
void setParameters (const Parameters& newParameters)
{
// need to call setSampleRate() first!
jassert (sampleRate > 0.0);
parameters = newParameters;
recalculateRates();
}
/** Returns the parameters currently being used by an ADSR object.
@see setParameters
*/
const Parameters& getParameters() const noexcept { return parameters; }
/** Returns true if the envelope is in its attack, decay, sustain or release stage. */
bool isActive() const noexcept { return state != State::idle; }
//==============================================================================
/** Sets the sample rate that will be used for the envelope.
This must be called before the getNextSample() or setParameters() methods.
*/
void setSampleRate (double newSampleRate) noexcept
{
jassert (newSampleRate > 0.0);
sampleRate = newSampleRate;
}
//==============================================================================
/** Resets the envelope to an idle state. */
void reset() noexcept
{
envelopeVal = 0.0f;
state = State::idle;
}
/** Starts the attack phase of the envelope. */
void noteOn() noexcept
{
if (attackRate > 0.0f)
{
state = State::attack;
}
else if (decayRate > 0.0f)
{
envelopeVal = 1.0f;
state = State::decay;
}
else
{
envelopeVal = parameters.sustain;
state = State::sustain;
}
}
/** Starts the release phase of the envelope. */
void noteOff() noexcept
{
if (state != State::idle)
{
if (parameters.release > 0.0f)
{
releaseRate = (float) (envelopeVal / (parameters.release * sampleRate));
state = State::release;
}
else
{
reset();
}
}
}
//==============================================================================
/** Returns the next sample value for an ADSR object.
@see applyEnvelopeToBuffer
*/
float getNextSample() noexcept
{
switch (state)
{
case State::idle:
{
return 0.0f;
}
case State::attack:
{
envelopeVal += attackRate;
if (envelopeVal >= 1.0f)
{
envelopeVal = 1.0f;
goToNextState();
}
break;
}
case State::decay:
{
envelopeVal -= decayRate;
if (envelopeVal <= parameters.sustain)
{
envelopeVal = parameters.sustain;
goToNextState();
}
break;
}
case State::sustain:
{
envelopeVal = parameters.sustain;
break;
}
case State::release:
{
envelopeVal -= releaseRate;
if (envelopeVal <= 0.0f)
goToNextState();
break;
}
}
return envelopeVal;
}
/** This method will conveniently apply the next numSamples number of envelope values
to an AudioBuffer.
@see getNextSample
*/
template <typename FloatType>
void applyEnvelopeToBuffer (AudioBuffer<FloatType>& buffer, int startSample, int numSamples)
{
jassert (startSample + numSamples <= buffer.getNumSamples());
if (state == State::idle)
{
buffer.clear (startSample, numSamples);
return;
}
if (state == State::sustain)
{
buffer.applyGain (startSample, numSamples, parameters.sustain);
return;
}
auto numChannels = buffer.getNumChannels();
while (--numSamples >= 0)
{
auto env = getNextSample();
for (int i = 0; i < numChannels; ++i)
buffer.getWritePointer (i)[startSample] *= env;
++startSample;
}
}
private:
//==============================================================================
void recalculateRates() noexcept
{
auto getRate = [] (float distance, float timeInSeconds, double sr)
{
return timeInSeconds > 0.0f ? (float) (distance / (timeInSeconds * sr)) : -1.0f;
};
attackRate = getRate (1.0f, parameters.attack, sampleRate);
decayRate = getRate (1.0f - parameters.sustain, parameters.decay, sampleRate);
releaseRate = getRate (parameters.sustain, parameters.release, sampleRate);
if ((state == State::attack && attackRate <= 0.0f)
|| (state == State::decay && (decayRate <= 0.0f || envelopeVal <= parameters.sustain))
|| (state == State::release && releaseRate <= 0.0f))
{
goToNextState();
}
}
void goToNextState() noexcept
{
if (state == State::attack)
{
state = (decayRate > 0.0f ? State::decay : State::sustain);
return;
}
if (state == State::decay)
{
state = State::sustain;
return;
}
if (state == State::release)
reset();
}
//==============================================================================
enum class State { idle, attack, decay, sustain, release };
State state = State::idle;
Parameters parameters;
double sampleRate = 44100.0;
float envelopeVal = 0.0f, attackRate = 0.0f, decayRate = 0.0f, releaseRate = 0.0f;
};
} // namespace juce

View File

@@ -1,243 +1,257 @@
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2020 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
struct ADSRTests : public UnitTest
{
ADSRTests() : UnitTest ("ADSR", UnitTestCategories::audio) {}
void runTest() override
{
constexpr double sampleRate = 44100.0;
const ADSR::Parameters parameters { 0.1f, 0.1f, 0.5f, 0.1f };
ADSR adsr;
adsr.setSampleRate (sampleRate);
adsr.setParameters (parameters);
beginTest ("Idle");
{
adsr.reset();
expect (! adsr.isActive());
expectEquals (adsr.getNextSample(), 0.0f);
}
beginTest ("Attack");
{
adsr.reset();
adsr.noteOn();
expect (adsr.isActive());
auto buffer = getTestBuffer (sampleRate, parameters.attack);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isIncreasing (buffer));
}
beginTest ("Decay");
{
adsr.reset();
adsr.noteOn();
advanceADSR (adsr, roundToInt (parameters.attack * sampleRate));
auto buffer = getTestBuffer (sampleRate, parameters.decay);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isDecreasing (buffer));
}
beginTest ("Sustain");
{
adsr.reset();
adsr.noteOn();
advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay + 0.01) * sampleRate));
auto random = getRandom();
for (int numTests = 0; numTests < 100; ++numTests)
{
const auto sustainLevel = random.nextFloat();
const auto sustainLength = jmax (0.1f, random.nextFloat());
adsr.setParameters ({ parameters.attack, parameters.decay, sustainLevel, parameters.release });
auto buffer = getTestBuffer (sampleRate, sustainLength);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isSustained (buffer, sustainLevel));
}
}
beginTest ("Release");
{
adsr.reset();
adsr.noteOn();
advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate));
adsr.noteOff();
auto buffer = getTestBuffer (sampleRate, parameters.release);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isDecreasing (buffer));
}
beginTest ("Zero-length attack jumps to decay");
{
adsr.reset();
adsr.setParameters ({ 0.0f, parameters.decay, parameters.sustain, parameters.release });
adsr.noteOn();
auto buffer = getTestBuffer (sampleRate, parameters.decay);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isDecreasing (buffer));
}
beginTest ("Zero-length decay jumps to sustain");
{
adsr.reset();
adsr.setParameters ({ parameters.attack, 0.0f, parameters.sustain, parameters.release });
adsr.noteOn();
advanceADSR (adsr, roundToInt (parameters.attack * sampleRate));
adsr.getNextSample();
expectEquals (adsr.getNextSample(), parameters.sustain);
auto buffer = getTestBuffer (sampleRate, 1);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isSustained (buffer, parameters.sustain));
}
beginTest ("Zero-length attack and decay jumps to sustain");
{
adsr.reset();
adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release });
adsr.noteOn();
expectEquals (adsr.getNextSample(), parameters.sustain);
auto buffer = getTestBuffer (sampleRate, 1);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isSustained (buffer, parameters.sustain));
}
beginTest ("Zero-length release resets to idle");
{
adsr.reset();
adsr.setParameters ({ parameters.attack, parameters.decay, parameters.sustain, 0.0f });
adsr.noteOn();
advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate));
adsr.noteOff();
expect (! adsr.isActive());
}
}
static void advanceADSR (ADSR& adsr, int numSamplesToAdvance)
{
while (--numSamplesToAdvance >= 0)
adsr.getNextSample();
}
static AudioBuffer<float> getTestBuffer (double sampleRate, float lengthInSeconds)
{
AudioBuffer<float> buffer { 2, roundToInt (lengthInSeconds * sampleRate) };
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
buffer.setSample (channel, sample, 1.0f);
return buffer;
}
static bool isIncreasing (const AudioBuffer<float>& b)
{
jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0);
for (int channel = 0; channel < b.getNumChannels(); ++channel)
{
float previousSample = -1.0f;
for (int sample = 0; sample < b.getNumSamples(); ++sample)
{
const auto currentSample = b.getSample (channel, sample);
if (currentSample <= previousSample)
return false;
previousSample = currentSample;
}
}
return true;
}
static bool isDecreasing (const AudioBuffer<float>& b)
{
jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0);
for (int channel = 0; channel < b.getNumChannels(); ++channel)
{
float previousSample = std::numeric_limits<float>::max();
for (int sample = 0; sample < b.getNumSamples(); ++sample)
{
const auto currentSample = b.getSample (channel, sample);
if (currentSample >= previousSample)
return false;
previousSample = currentSample;
}
}
return true;
}
static bool isSustained (const AudioBuffer<float>& b, float sustainLevel)
{
jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0);
for (int channel = 0; channel < b.getNumChannels(); ++channel)
if (b.findMinMax (channel, 0, b.getNumSamples()) != Range<float> { sustainLevel, sustainLevel })
return false;
return true;
}
};
static ADSRTests adsrTests;
} // namespace juce
/*
==============================================================================
This file is part of the JUCE library.
Copyright (c) 2022 - Raw Material Software Limited
JUCE is an open source library subject to commercial or open-source
licensing.
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.
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
DISCLAIMED.
==============================================================================
*/
namespace juce
{
struct ADSRTests : public UnitTest
{
ADSRTests() : UnitTest ("ADSR", UnitTestCategories::audio) {}
void runTest() override
{
constexpr double sampleRate = 44100.0;
const ADSR::Parameters parameters { 0.1f, 0.1f, 0.5f, 0.1f };
ADSR adsr;
adsr.setSampleRate (sampleRate);
adsr.setParameters (parameters);
beginTest ("Idle");
{
adsr.reset();
expect (! adsr.isActive());
expectEquals (adsr.getNextSample(), 0.0f);
}
beginTest ("Attack");
{
adsr.reset();
adsr.noteOn();
expect (adsr.isActive());
auto buffer = getTestBuffer (sampleRate, parameters.attack);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isIncreasing (buffer));
}
beginTest ("Decay");
{
adsr.reset();
adsr.noteOn();
advanceADSR (adsr, roundToInt (parameters.attack * sampleRate));
auto buffer = getTestBuffer (sampleRate, parameters.decay);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isDecreasing (buffer));
}
beginTest ("Sustain");
{
adsr.reset();
adsr.noteOn();
advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay + 0.01) * sampleRate));
auto random = getRandom();
for (int numTests = 0; numTests < 100; ++numTests)
{
const auto sustainLevel = random.nextFloat();
const auto sustainLength = jmax (0.1f, random.nextFloat());
adsr.setParameters ({ parameters.attack, parameters.decay, sustainLevel, parameters.release });
auto buffer = getTestBuffer (sampleRate, sustainLength);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isSustained (buffer, sustainLevel));
}
}
beginTest ("Release");
{
adsr.reset();
adsr.noteOn();
advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate));
adsr.noteOff();
auto buffer = getTestBuffer (sampleRate, parameters.release);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isDecreasing (buffer));
}
beginTest ("Zero-length attack jumps to decay");
{
adsr.reset();
adsr.setParameters ({ 0.0f, parameters.decay, parameters.sustain, parameters.release });
adsr.noteOn();
auto buffer = getTestBuffer (sampleRate, parameters.decay);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isDecreasing (buffer));
}
beginTest ("Zero-length decay jumps to sustain");
{
adsr.reset();
adsr.setParameters ({ parameters.attack, 0.0f, parameters.sustain, parameters.release });
adsr.noteOn();
advanceADSR (adsr, roundToInt (parameters.attack * sampleRate));
adsr.getNextSample();
expectEquals (adsr.getNextSample(), parameters.sustain);
auto buffer = getTestBuffer (sampleRate, 1);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isSustained (buffer, parameters.sustain));
}
beginTest ("Zero-length attack and decay jumps to sustain");
{
adsr.reset();
adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release });
adsr.noteOn();
expectEquals (adsr.getNextSample(), parameters.sustain);
auto buffer = getTestBuffer (sampleRate, 1);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isSustained (buffer, parameters.sustain));
}
beginTest ("Zero-length attack and decay releases correctly");
{
adsr.reset();
adsr.setParameters ({ 0.0f, 0.0f, parameters.sustain, parameters.release });
adsr.noteOn();
adsr.noteOff();
auto buffer = getTestBuffer (sampleRate, parameters.release);
adsr.applyEnvelopeToBuffer (buffer, 0, buffer.getNumSamples());
expect (isDecreasing (buffer));
}
beginTest ("Zero-length release resets to idle");
{
adsr.reset();
adsr.setParameters ({ parameters.attack, parameters.decay, parameters.sustain, 0.0f });
adsr.noteOn();
advanceADSR (adsr, roundToInt ((parameters.attack + parameters.decay) * sampleRate));
adsr.noteOff();
expect (! adsr.isActive());
}
}
static void advanceADSR (ADSR& adsr, int numSamplesToAdvance)
{
while (--numSamplesToAdvance >= 0)
adsr.getNextSample();
}
static AudioBuffer<float> getTestBuffer (double sampleRate, float lengthInSeconds)
{
AudioBuffer<float> buffer { 2, roundToInt (lengthInSeconds * sampleRate) };
for (int channel = 0; channel < buffer.getNumChannels(); ++channel)
for (int sample = 0; sample < buffer.getNumSamples(); ++sample)
buffer.setSample (channel, sample, 1.0f);
return buffer;
}
static bool isIncreasing (const AudioBuffer<float>& b)
{
jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0);
for (int channel = 0; channel < b.getNumChannels(); ++channel)
{
float previousSample = -1.0f;
for (int sample = 0; sample < b.getNumSamples(); ++sample)
{
const auto currentSample = b.getSample (channel, sample);
if (currentSample <= previousSample)
return false;
previousSample = currentSample;
}
}
return true;
}
static bool isDecreasing (const AudioBuffer<float>& b)
{
jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0);
for (int channel = 0; channel < b.getNumChannels(); ++channel)
{
float previousSample = std::numeric_limits<float>::max();
for (int sample = 0; sample < b.getNumSamples(); ++sample)
{
const auto currentSample = b.getSample (channel, sample);
if (currentSample >= previousSample)
return false;
previousSample = currentSample;
}
}
return true;
}
static bool isSustained (const AudioBuffer<float>& b, float sustainLevel)
{
jassert (b.getNumChannels() > 0 && b.getNumSamples() > 0);
for (int channel = 0; channel < b.getNumChannels(); ++channel)
if (b.findMinMax (channel, 0, b.getNumSamples()) != Range<float> { sustainLevel, sustainLevel })
return false;
return true;
}
};
static ADSRTests adsrTests;
} // namespace juce

Some files were not shown because too many files have changed in this diff Show More