mirror of
https://github.com/AxioDL/amuse.git
synced 2025-07-01 10:53:36 +00:00
Submix class and initial effects
This commit is contained in:
parent
708662c23e
commit
3cce975977
@ -10,7 +10,13 @@ set(SOURCES
|
|||||||
lib/Listener.cpp
|
lib/Listener.cpp
|
||||||
lib/Sequencer.cpp
|
lib/Sequencer.cpp
|
||||||
lib/SoundMacroState.cpp
|
lib/SoundMacroState.cpp
|
||||||
lib/Voice.cpp)
|
lib/Voice.cpp
|
||||||
|
lib/Submix.cpp
|
||||||
|
lib/EffectBase.cpp
|
||||||
|
lib/EffectReverbHi.cpp
|
||||||
|
lib/EffectReverbStd.cpp
|
||||||
|
lib/EffectChorus.cpp
|
||||||
|
lib/EffectDelay.cpp)
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
include/amuse/AudioGroup.hpp
|
include/amuse/AudioGroup.hpp
|
||||||
@ -26,8 +32,15 @@ set(HEADERS
|
|||||||
include/amuse/Sequencer.hpp
|
include/amuse/Sequencer.hpp
|
||||||
include/amuse/SoundMacroState.hpp
|
include/amuse/SoundMacroState.hpp
|
||||||
include/amuse/Voice.hpp
|
include/amuse/Voice.hpp
|
||||||
|
include/amuse/Submix.hpp
|
||||||
|
include/amuse/IBackendSubmix.hpp
|
||||||
include/amuse/IBackendVoice.hpp
|
include/amuse/IBackendVoice.hpp
|
||||||
include/amuse/IBackendVoiceAllocator.hpp
|
include/amuse/IBackendVoiceAllocator.hpp
|
||||||
|
include/amuse/EffectBase.hpp
|
||||||
|
include/amuse/EffectReverbHi.hpp
|
||||||
|
include/amuse/EffectReverbStd.hpp
|
||||||
|
include/amuse/EffectChorus.hpp
|
||||||
|
include/amuse/EffectDelay.hpp
|
||||||
include/amuse/Common.hpp
|
include/amuse/Common.hpp
|
||||||
include/amuse/amuse.hpp)
|
include/amuse/amuse.hpp)
|
||||||
|
|
||||||
|
@ -2,7 +2,9 @@
|
|||||||
#define __AMUSE_BOO_BACKEND_HPP__
|
#define __AMUSE_BOO_BACKEND_HPP__
|
||||||
|
|
||||||
#include <boo/audiodev/IAudioVoiceEngine.hpp>
|
#include <boo/audiodev/IAudioVoiceEngine.hpp>
|
||||||
|
#include <boo/audiodev/IAudioSubmix.hpp>
|
||||||
#include "IBackendVoice.hpp"
|
#include "IBackendVoice.hpp"
|
||||||
|
#include "IBackendSubmix.hpp"
|
||||||
#include "IBackendVoiceAllocator.hpp"
|
#include "IBackendVoiceAllocator.hpp"
|
||||||
|
|
||||||
namespace amuse
|
namespace amuse
|
||||||
@ -23,12 +25,36 @@ class BooBackendVoice : public IBackendVoice
|
|||||||
public:
|
public:
|
||||||
BooBackendVoice(boo::IAudioVoiceEngine& engine, Voice& clientVox,
|
BooBackendVoice(boo::IAudioVoiceEngine& engine, Voice& clientVox,
|
||||||
double sampleRate, bool dynamicPitch);
|
double sampleRate, bool dynamicPitch);
|
||||||
|
BooBackendVoice(boo::IAudioSubmix& submix, Voice& clientVox,
|
||||||
|
double sampleRate, bool dynamicPitch);
|
||||||
void setMatrixCoefficients(const float coefs[8]);
|
void setMatrixCoefficients(const float coefs[8]);
|
||||||
void setPitchRatio(double ratio);
|
void setPitchRatio(double ratio);
|
||||||
void start();
|
void start();
|
||||||
void stop();
|
void stop();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Backend submix implementation for boo mixer */
|
||||||
|
class BooBackendSubmix : public IBackendSubmix
|
||||||
|
{
|
||||||
|
friend class BooBackendVoiceAllocator;
|
||||||
|
Submix& m_clientSmx;
|
||||||
|
struct SubmixCallback : boo::IAudioSubmixCallback
|
||||||
|
{
|
||||||
|
BooBackendSubmix& m_parent;
|
||||||
|
bool canApplyEffect() const;
|
||||||
|
void applyEffect(int16_t* audio, const boo::ChannelMap& chanMap, double sampleRate) const;
|
||||||
|
void applyEffect(int32_t* audio, const boo::ChannelMap& chanMap, double sampleRate) const;
|
||||||
|
void applyEffect(float* audio, const boo::ChannelMap& chanMap, double sampleRate) const;
|
||||||
|
SubmixCallback(BooBackendSubmix& parent) : m_parent(parent) {}
|
||||||
|
} m_cb;
|
||||||
|
std::unique_ptr<boo::IAudioSubmix> m_booSubmix;
|
||||||
|
public:
|
||||||
|
BooBackendSubmix(boo::IAudioVoiceEngine& engine, Submix& clientSmx);
|
||||||
|
BooBackendSubmix(boo::IAudioSubmix& parent, Submix& clientSmx);
|
||||||
|
void setChannelGains(const float gains[8]);
|
||||||
|
std::unique_ptr<IBackendVoice> allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch);
|
||||||
|
};
|
||||||
|
|
||||||
/** Backend voice allocator implementation for boo mixer */
|
/** Backend voice allocator implementation for boo mixer */
|
||||||
class BooBackendVoiceAllocator : public IBackendVoiceAllocator
|
class BooBackendVoiceAllocator : public IBackendVoiceAllocator
|
||||||
{
|
{
|
||||||
@ -36,6 +62,7 @@ class BooBackendVoiceAllocator : public IBackendVoiceAllocator
|
|||||||
public:
|
public:
|
||||||
BooBackendVoiceAllocator(boo::IAudioVoiceEngine& booEngine);
|
BooBackendVoiceAllocator(boo::IAudioVoiceEngine& booEngine);
|
||||||
std::unique_ptr<IBackendVoice> allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch);
|
std::unique_ptr<IBackendVoice> allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch);
|
||||||
|
std::unique_ptr<IBackendSubmix> allocateSubmix(Submix& clientSmx);
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
13
include/amuse/EffectBase.hpp
Normal file
13
include/amuse/EffectBase.hpp
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#ifndef __AMUSE_EFFECTBASE_HPP__
|
||||||
|
#define __AMUSE_EFFECTBASE_HPP__
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
|
||||||
|
class EffectBase
|
||||||
|
{
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __AMUSE_EFFECTBASE_HPP__
|
20
include/amuse/EffectChorus.hpp
Normal file
20
include/amuse/EffectChorus.hpp
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#ifndef __AMUSE_EFFECTCHORUS_HPP__
|
||||||
|
#define __AMUSE_EFFECTCHORUS_HPP__
|
||||||
|
|
||||||
|
#include "EffectBase.hpp"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
|
||||||
|
/** Mixes the audio back into itself after continuously-varying delay */
|
||||||
|
class EffectChorus : public EffectBase
|
||||||
|
{
|
||||||
|
uint32_t m_baseDelay; /**< [5, 15] minimum value (in ms) for computed delay */
|
||||||
|
uint32_t m_variation; /**< [0, 5] time error (in ms) to set delay within */
|
||||||
|
uint32_t m_period; /**< [500, 10000] time (in ms) of one delay-shift cycle */
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __AMUSE_EFFECTCHORUS_HPP__
|
20
include/amuse/EffectDelay.hpp
Normal file
20
include/amuse/EffectDelay.hpp
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#ifndef __AMUSE_EFFECTDELAY_HPP__
|
||||||
|
#define __AMUSE_EFFECTDELAY_HPP__
|
||||||
|
|
||||||
|
#include "EffectBase.hpp"
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
|
||||||
|
/** Mixes the audio back into itself after specified delay */
|
||||||
|
class EffectDelay : public EffectBase
|
||||||
|
{
|
||||||
|
uint32_t m_delay[8]; /**< [10, 5000] time in ms of each channel's delay */
|
||||||
|
uint32_t m_feedback[8]; /**< [0, 100] percent to mix delayed signal with input signal */
|
||||||
|
uint32_t m_output[8]; /**< [0, 100] total output percent */
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __AMUSE_EFFECTDELAY_HPP__
|
22
include/amuse/EffectReverbHi.hpp
Normal file
22
include/amuse/EffectReverbHi.hpp
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
#ifndef __AMUSE_EFFECTREVERBHI_HPP__
|
||||||
|
#define __AMUSE_EFFECTREVERBHI_HPP__
|
||||||
|
|
||||||
|
#include "EffectBase.hpp"
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
|
||||||
|
/** Reverb effect with configurable reflection filtering and channel-crosstalk */
|
||||||
|
class EffectReverbHi : public EffectBase
|
||||||
|
{
|
||||||
|
float m_coloration; /**< [0.0, 1.0] influences filter coefficients to define surface characteristics of a room */
|
||||||
|
float m_mix; /**< [0.0, 1.0] dry/wet mix factor of reverb effect */
|
||||||
|
float m_time; /**< [0.01, 10.0] time in seconds for reflection decay */
|
||||||
|
float m_damping; /**< [0.0, 1.0] damping factor influencing low-pass filter of reflections */
|
||||||
|
float m_preDelay; /**< [0.0, 0.1] time in seconds before initial reflection heard */
|
||||||
|
float m_crosstalk; /**< [0.0, 100.0] factor defining how much reflections are allowed to bleed to other channels */
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __AMUSE_EFFECTREVERBHI_HPP__
|
21
include/amuse/EffectReverbStd.hpp
Normal file
21
include/amuse/EffectReverbStd.hpp
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#ifndef __AMUSE_EFFECTREVERBSTD_HPP__
|
||||||
|
#define __AMUSE_EFFECTREVERBSTD_HPP__
|
||||||
|
|
||||||
|
#include "EffectBase.hpp"
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
|
||||||
|
/** Reverb effect with configurable reflection filtering */
|
||||||
|
class EffectReverbStd : public EffectBase
|
||||||
|
{
|
||||||
|
float m_coloration; /**< [0.0, 1.0] influences filter coefficients to define surface characteristics of a room */
|
||||||
|
float m_mix; /**< [0.0, 1.0] dry/wet mix factor of reverb effect */
|
||||||
|
float m_time; /**< [0.01, 10.0] time in seconds for reflection decay */
|
||||||
|
float m_damping; /**< [0.0, 1.0] damping factor influencing low-pass filter of reflections */
|
||||||
|
float m_preDelay; /**< [0.0, 0.1] time in seconds before initial reflection heard */
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __AMUSE_EFFECTREVERBSTD_HPP__
|
@ -13,6 +13,7 @@ namespace amuse
|
|||||||
{
|
{
|
||||||
class IBackendVoiceAllocator;
|
class IBackendVoiceAllocator;
|
||||||
class Voice;
|
class Voice;
|
||||||
|
class Submix;
|
||||||
class Emitter;
|
class Emitter;
|
||||||
class Sequencer;
|
class Sequencer;
|
||||||
class AudioGroup;
|
class AudioGroup;
|
||||||
@ -26,10 +27,14 @@ class Engine
|
|||||||
std::list<Voice> m_activeVoices;
|
std::list<Voice> m_activeVoices;
|
||||||
std::list<Emitter> m_activeEmitters;
|
std::list<Emitter> m_activeEmitters;
|
||||||
std::list<Sequencer> m_activeSequencers;
|
std::list<Sequencer> m_activeSequencers;
|
||||||
|
std::list<Submix> m_activeSubmixes;
|
||||||
std::linear_congruential_engine<uint32_t, 0x41c64e6d, 0x3039, UINT32_MAX> m_random;
|
std::linear_congruential_engine<uint32_t, 0x41c64e6d, 0x3039, UINT32_MAX> m_random;
|
||||||
int m_nextVid = 0;
|
int m_nextVid = 0;
|
||||||
Voice* _allocateVoice(const AudioGroup& group, double sampleRate, bool dynamicPitch, bool emitter);
|
Voice* _allocateVoice(const AudioGroup& group, double sampleRate,
|
||||||
|
bool dynamicPitch, bool emitter, Submix* smx);
|
||||||
|
Submix* _allocateSubmix(Submix* smx);
|
||||||
std::list<Voice>::iterator _destroyVoice(Voice* voice);
|
std::list<Voice>::iterator _destroyVoice(Voice* voice);
|
||||||
|
std::list<Submix>::iterator _destroySubmix(Submix* smx);
|
||||||
AudioGroup* _findGroupFromSfxId(int sfxId, const AudioGroupSampleDirectory::Entry*& entOut) const;
|
AudioGroup* _findGroupFromSfxId(int sfxId, const AudioGroupSampleDirectory::Entry*& entOut) const;
|
||||||
AudioGroup* _findGroupFromSongId(int songId) const;
|
AudioGroup* _findGroupFromSongId(int songId) const;
|
||||||
public:
|
public:
|
||||||
@ -39,17 +44,24 @@ public:
|
|||||||
void pumpEngine();
|
void pumpEngine();
|
||||||
|
|
||||||
/** Add audio group data pointers to engine; must remain resident! */
|
/** Add audio group data pointers to engine; must remain resident! */
|
||||||
bool addAudioGroup(int groupId, const AudioGroupData& data);
|
const AudioGroup* addAudioGroup(int groupId, const AudioGroupData& data);
|
||||||
|
|
||||||
/** Remove audio group from engine */
|
/** Remove audio group from engine */
|
||||||
void removeAudioGroup(int groupId);
|
void removeAudioGroup(int groupId);
|
||||||
|
|
||||||
|
/** Create new Submix (a.k.a 'Studio') within root mix engine */
|
||||||
|
Submix* addSubmix(Submix* parent=nullptr);
|
||||||
|
|
||||||
|
/** Remove Submix and deallocate */
|
||||||
|
void removeSubmix(Submix* smx);
|
||||||
|
|
||||||
/** Start soundFX playing from loaded audio groups */
|
/** Start soundFX playing from loaded audio groups */
|
||||||
Voice* fxStart(int sfxId, float vol, float pan);
|
Voice* fxStart(int sfxId, float vol, float pan, Submix* smx=nullptr);
|
||||||
|
|
||||||
/** Start soundFX playing from loaded audio groups, attach to positional emitter */
|
/** Start soundFX playing from loaded audio groups, attach to positional emitter */
|
||||||
Emitter* addEmitter(const Vector3f& pos, const Vector3f& dir, float maxDist,
|
Emitter* addEmitter(const Vector3f& pos, const Vector3f& dir, float maxDist,
|
||||||
float falloff, int sfxId, float minVol, float maxVol);
|
float falloff, int sfxId, float minVol, float maxVol,
|
||||||
|
Submix* smx=nullptr);
|
||||||
|
|
||||||
/** Start song playing from loaded audio groups */
|
/** Start song playing from loaded audio groups */
|
||||||
Sequencer* seqPlay(int songId, const unsigned char* arrData);
|
Sequencer* seqPlay(int songId, const unsigned char* arrData);
|
||||||
|
28
include/amuse/IBackendSubmix.hpp
Normal file
28
include/amuse/IBackendSubmix.hpp
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
#ifndef __AMUSE_IBACKENDSUBMIX_HPP__
|
||||||
|
#define __AMUSE_IBACKENDSUBMIX_HPP__
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
class IBackendVoice;
|
||||||
|
class Voice;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Client-implemented submix instance
|
||||||
|
*/
|
||||||
|
class IBackendSubmix
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IBackendSubmix() = default;
|
||||||
|
|
||||||
|
/** Set channel-gains for submix (AudioChannel enum for array index) */
|
||||||
|
virtual void setChannelGains(const float gains[8])=0;
|
||||||
|
|
||||||
|
/** Amuse obtains a new voice from the platform outputting to this submix */
|
||||||
|
virtual std::unique_ptr<IBackendVoice> allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch)=0;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __AMUSE_IBACKENDSUBMIX_HPP__
|
@ -18,6 +18,13 @@ enum class AudioChannel
|
|||||||
Unknown = 0xff
|
Unknown = 0xff
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Same structure from boo, used to represent interleaved speaker layout */
|
||||||
|
struct ChannelMap
|
||||||
|
{
|
||||||
|
unsigned m_channelCount = 0;
|
||||||
|
AudioChannel m_channels[8] = {};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Client-implemented voice instance
|
* @brief Client-implemented voice instance
|
||||||
*/
|
*/
|
||||||
|
@ -6,7 +6,9 @@
|
|||||||
namespace amuse
|
namespace amuse
|
||||||
{
|
{
|
||||||
class IBackendVoice;
|
class IBackendVoice;
|
||||||
|
class IBackendSubmix;
|
||||||
class Voice;
|
class Voice;
|
||||||
|
class Submix;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Client-implemented voice allocator
|
* @brief Client-implemented voice allocator
|
||||||
@ -18,6 +20,9 @@ public:
|
|||||||
|
|
||||||
/** Amuse obtains a new voice from the platform this way */
|
/** Amuse obtains a new voice from the platform this way */
|
||||||
virtual std::unique_ptr<IBackendVoice> allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch)=0;
|
virtual std::unique_ptr<IBackendVoice> allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch)=0;
|
||||||
|
|
||||||
|
/** Amuse obtains a new submix from the platform this way */
|
||||||
|
virtual std::unique_ptr<IBackendSubmix> allocateSubmix(Submix& clientSmx)=0;
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
53
include/amuse/Submix.hpp
Normal file
53
include/amuse/Submix.hpp
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
#ifndef __AMUSE_SUBMIX_HPP__
|
||||||
|
#define __AMUSE_SUBMIX_HPP__
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include "SoundMacroState.hpp"
|
||||||
|
#include "IBackendSubmix.hpp"
|
||||||
|
#include "IBackendVoice.hpp"
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
class IBackendSubmix;
|
||||||
|
|
||||||
|
/** Intermediate mix of voices for applying auxiliary effects */
|
||||||
|
class Submix
|
||||||
|
{
|
||||||
|
friend class Engine;
|
||||||
|
friend class Voice;
|
||||||
|
Engine& m_root;
|
||||||
|
Submix* m_submix = nullptr; /**< Parent submix of this submix (or NULL if mixing to main output) */
|
||||||
|
std::list<Submix>::iterator m_engineIt; /**< Iterator to self within Engine's list for quick deletion */
|
||||||
|
std::unique_ptr<IBackendSubmix> m_backendSubmix; /**< Handle to client-implemented backend submix */
|
||||||
|
std::unordered_set<Voice*> m_activeVoices; /**< Secondary index of Voices within Submix */
|
||||||
|
std::unordered_set<Submix*> m_activeSubmixes; /**< Secondary index of Submixes within Submix */
|
||||||
|
bool m_destroyed = false;
|
||||||
|
void _destroy();
|
||||||
|
public:
|
||||||
|
Submix(Engine& engine, Submix* smx);
|
||||||
|
~Submix()
|
||||||
|
{
|
||||||
|
#ifndef NDEBUG
|
||||||
|
/* Ensure proper destruction procedure followed */
|
||||||
|
assert(m_destroyed);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Obtain pointer to Submix's parent Submix */
|
||||||
|
Submix* getParentSubmix() {return m_submix;}
|
||||||
|
|
||||||
|
/** Returns true when an effect callback is bound */
|
||||||
|
bool canApplyEffect() const;
|
||||||
|
|
||||||
|
/** in/out transformation entry for audio effect */
|
||||||
|
void applyEffect(int16_t* audio, const ChannelMap& chanMap, double sampleRate) const;
|
||||||
|
void applyEffect(int32_t* audio, const ChannelMap& chanMap, double sampleRate) const;
|
||||||
|
void applyEffect(float* audio, const ChannelMap& chanMap, double sampleRate) const;
|
||||||
|
|
||||||
|
Engine& getEngine() {return m_root;}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // __AMUSE_SUBMIX_HPP__
|
@ -11,6 +11,7 @@
|
|||||||
namespace amuse
|
namespace amuse
|
||||||
{
|
{
|
||||||
class IBackendVoice;
|
class IBackendVoice;
|
||||||
|
class Submix;
|
||||||
|
|
||||||
/** State of voice over lifetime */
|
/** State of voice over lifetime */
|
||||||
enum class VoiceState
|
enum class VoiceState
|
||||||
@ -24,27 +25,29 @@ enum class VoiceState
|
|||||||
class Voice : public Entity
|
class Voice : public Entity
|
||||||
{
|
{
|
||||||
friend class Engine;
|
friend class Engine;
|
||||||
template <class U, class A>
|
|
||||||
friend class std::list;
|
|
||||||
int m_vid; /**< VoiceID of this voice instance */
|
int m_vid; /**< VoiceID of this voice instance */
|
||||||
bool m_emitter; /**< Voice is part of an Emitter */
|
bool m_emitter; /**< Voice is part of an Emitter */
|
||||||
|
Submix* m_submix = nullptr; /**< Submix this voice outputs to (or NULL for the main output mix) */
|
||||||
std::list<Voice>::iterator m_engineIt; /**< Iterator to self within Engine's list for quick deletion */
|
std::list<Voice>::iterator m_engineIt; /**< Iterator to self within Engine's list for quick deletion */
|
||||||
std::unique_ptr<IBackendVoice> m_backendVoice; /**< Handle to client-implemented backend voice */
|
std::unique_ptr<IBackendVoice> m_backendVoice; /**< Handle to client-implemented backend voice */
|
||||||
SoundMacroState m_state; /**< State container for SoundMacro playback */
|
SoundMacroState m_state; /**< State container for SoundMacro playback */
|
||||||
Voice *m_nextSibling = nullptr, *m_prevSibling = nullptr; /**< Sibling voice links for PLAYMACRO usage */
|
std::list<Voice> m_childVoices; /**< Child voices for PLAYMACRO usage */
|
||||||
uint8_t m_lastNote = 0; /**< Last MIDI semitone played by voice */
|
uint8_t m_lastNote = 0; /**< Last MIDI semitone played by voice */
|
||||||
uint8_t m_keygroup = 0; /**< Keygroup voice is a member of */
|
uint8_t m_keygroup = 0; /**< Keygroup voice is a member of */
|
||||||
|
|
||||||
void _destroy();
|
void _destroy();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Voice(Engine& engine, const AudioGroup& group, int vid, bool emitter);
|
Voice(Engine& engine, const AudioGroup& group, int vid, bool emitter, Submix* smx);
|
||||||
Voice(Engine& engine, const AudioGroup& group, ObjectId oid, int vid, bool emitter);
|
Voice(Engine& engine, const AudioGroup& group, ObjectId oid, int vid, bool emitter, Submix* smx);
|
||||||
|
|
||||||
/** Request specified count of audio frames (samples) from voice,
|
/** Request specified count of audio frames (samples) from voice,
|
||||||
* internally advancing the voice stream */
|
* internally advancing the voice stream */
|
||||||
size_t supplyAudio(size_t frames, int16_t* data);
|
size_t supplyAudio(size_t frames, int16_t* data);
|
||||||
|
|
||||||
|
/** Obtain pointer to Voice's Submix */
|
||||||
|
Submix* getSubmix() {return m_submix;}
|
||||||
|
|
||||||
/** Get current state of voice */
|
/** Get current state of voice */
|
||||||
VoiceState state() const;
|
VoiceState state() const;
|
||||||
|
|
||||||
@ -52,7 +55,7 @@ public:
|
|||||||
int vid() const {return m_vid;}
|
int vid() const {return m_vid;}
|
||||||
|
|
||||||
/** Allocate parallel macro and tie to voice for possible emitter influence */
|
/** Allocate parallel macro and tie to voice for possible emitter influence */
|
||||||
Voice* startSiblingMacro(int8_t addNote, ObjectId macroId, int macroStep);
|
Voice* startChildMacro(int8_t addNote, ObjectId macroId, int macroStep);
|
||||||
|
|
||||||
/** Load specified SoundMacro ID of within group into voice */
|
/** Load specified SoundMacro ID of within group into voice */
|
||||||
bool loadSoundMacro(ObjectId macroId, int macroStep=0, bool pushPc=false);
|
bool loadSoundMacro(ObjectId macroId, int macroStep=0, bool pushPc=false);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#include "amuse/BooBackend.hpp"
|
#include "amuse/BooBackend.hpp"
|
||||||
#include "amuse/Voice.hpp"
|
#include "amuse/Voice.hpp"
|
||||||
|
#include "amuse/Submix.hpp"
|
||||||
|
|
||||||
namespace amuse
|
namespace amuse
|
||||||
{
|
{
|
||||||
@ -16,6 +17,12 @@ BooBackendVoice::BooBackendVoice(boo::IAudioVoiceEngine& engine, Voice& clientVo
|
|||||||
m_booVoice(engine.allocateNewMonoVoice(sampleRate, &m_cb, dynamicPitch))
|
m_booVoice(engine.allocateNewMonoVoice(sampleRate, &m_cb, dynamicPitch))
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
BooBackendVoice::BooBackendVoice(boo::IAudioSubmix& submix, Voice& clientVox,
|
||||||
|
double sampleRate, bool dynamicPitch)
|
||||||
|
: m_clientVox(clientVox), m_cb(*this),
|
||||||
|
m_booVoice(submix.allocateNewMonoVoice(sampleRate, &m_cb, dynamicPitch))
|
||||||
|
{}
|
||||||
|
|
||||||
void BooBackendVoice::setMatrixCoefficients(const float coefs[8])
|
void BooBackendVoice::setMatrixCoefficients(const float coefs[8])
|
||||||
{
|
{
|
||||||
m_booVoice->setMonoMatrixCoefficients(coefs);
|
m_booVoice->setMonoMatrixCoefficients(coefs);
|
||||||
@ -36,6 +43,48 @@ void BooBackendVoice::stop()
|
|||||||
m_booVoice->stop();
|
m_booVoice->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool BooBackendSubmix::SubmixCallback::canApplyEffect() const
|
||||||
|
{
|
||||||
|
return m_parent.m_clientSmx.canApplyEffect();
|
||||||
|
}
|
||||||
|
|
||||||
|
void BooBackendSubmix::SubmixCallback::applyEffect(int16_t* audio, const boo::ChannelMap& chanMap,
|
||||||
|
double sampleRate) const
|
||||||
|
{
|
||||||
|
return m_parent.m_clientSmx.applyEffect(audio, reinterpret_cast<const ChannelMap&>(chanMap), sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BooBackendSubmix::SubmixCallback::applyEffect(int32_t* audio, const boo::ChannelMap& chanMap,
|
||||||
|
double sampleRate) const
|
||||||
|
{
|
||||||
|
return m_parent.m_clientSmx.applyEffect(audio, reinterpret_cast<const ChannelMap&>(chanMap), sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
void BooBackendSubmix::SubmixCallback::applyEffect(float* audio, const boo::ChannelMap& chanMap,
|
||||||
|
double sampleRate) const
|
||||||
|
{
|
||||||
|
return m_parent.m_clientSmx.applyEffect(audio, reinterpret_cast<const ChannelMap&>(chanMap), sampleRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
BooBackendSubmix::BooBackendSubmix(boo::IAudioVoiceEngine& engine, Submix& clientSmx)
|
||||||
|
: m_clientSmx(clientSmx), m_cb(*this), m_booSubmix(engine.allocateNewSubmix(&m_cb))
|
||||||
|
{}
|
||||||
|
|
||||||
|
BooBackendSubmix::BooBackendSubmix(boo::IAudioSubmix& parent, Submix& clientSmx)
|
||||||
|
: m_clientSmx(clientSmx), m_cb(*this), m_booSubmix(parent.allocateNewSubmix(&m_cb))
|
||||||
|
{}
|
||||||
|
|
||||||
|
void BooBackendSubmix::setChannelGains(const float gains[8])
|
||||||
|
{
|
||||||
|
m_booSubmix->setChannelGains(gains);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IBackendVoice>
|
||||||
|
BooBackendSubmix::allocateVoice(Voice& clientVox, double sampleRate, bool dynamicPitch)
|
||||||
|
{
|
||||||
|
return std::make_unique<BooBackendVoice>(*m_booSubmix, clientVox, sampleRate, dynamicPitch);
|
||||||
|
}
|
||||||
|
|
||||||
BooBackendVoiceAllocator::BooBackendVoiceAllocator(boo::IAudioVoiceEngine& booEngine)
|
BooBackendVoiceAllocator::BooBackendVoiceAllocator(boo::IAudioVoiceEngine& booEngine)
|
||||||
: m_booEngine(booEngine)
|
: m_booEngine(booEngine)
|
||||||
{}
|
{}
|
||||||
@ -46,4 +95,9 @@ BooBackendVoiceAllocator::allocateVoice(Voice& clientVox, double sampleRate, boo
|
|||||||
return std::make_unique<BooBackendVoice>(m_booEngine, clientVox, sampleRate, dynamicPitch);
|
return std::make_unique<BooBackendVoice>(m_booEngine, clientVox, sampleRate, dynamicPitch);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IBackendSubmix> BooBackendVoiceAllocator::allocateSubmix(Submix& clientSmx)
|
||||||
|
{
|
||||||
|
return std::make_unique<BooBackendSubmix>(m_booEngine, clientSmx);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
0
lib/EffectBase.cpp
Normal file
0
lib/EffectBase.cpp
Normal file
0
lib/EffectChorus.cpp
Normal file
0
lib/EffectChorus.cpp
Normal file
0
lib/EffectDelay.cpp
Normal file
0
lib/EffectDelay.cpp
Normal file
0
lib/EffectReverbHi.cpp
Normal file
0
lib/EffectReverbHi.cpp
Normal file
0
lib/EffectReverbStd.cpp
Normal file
0
lib/EffectReverbStd.cpp
Normal file
@ -1,5 +1,6 @@
|
|||||||
#include "amuse/Engine.hpp"
|
#include "amuse/Engine.hpp"
|
||||||
#include "amuse/Voice.hpp"
|
#include "amuse/Voice.hpp"
|
||||||
|
#include "amuse/Submix.hpp"
|
||||||
#include "amuse/Sequencer.hpp"
|
#include "amuse/Sequencer.hpp"
|
||||||
#include "amuse/IBackendVoice.hpp"
|
#include "amuse/IBackendVoice.hpp"
|
||||||
#include "amuse/IBackendVoiceAllocator.hpp"
|
#include "amuse/IBackendVoiceAllocator.hpp"
|
||||||
@ -13,15 +14,24 @@ Engine::Engine(IBackendVoiceAllocator& backend)
|
|||||||
: m_backend(backend)
|
: m_backend(backend)
|
||||||
{}
|
{}
|
||||||
|
|
||||||
Voice* Engine::_allocateVoice(const AudioGroup& group, double sampleRate, bool dynamicPitch, bool emitter)
|
Voice* Engine::_allocateVoice(const AudioGroup& group, double sampleRate,
|
||||||
|
bool dynamicPitch, bool emitter, Submix* smx)
|
||||||
{
|
{
|
||||||
auto it = m_activeVoices.emplace(m_activeVoices.end(), *this, group, m_nextVid++, emitter);
|
auto it = m_activeVoices.emplace(m_activeVoices.end(), *this, group, m_nextVid++, emitter, smx);
|
||||||
m_activeVoices.back().m_backendVoice =
|
m_activeVoices.back().m_backendVoice =
|
||||||
m_backend.allocateVoice(m_activeVoices.back(), sampleRate, dynamicPitch);
|
m_backend.allocateVoice(m_activeVoices.back(), sampleRate, dynamicPitch);
|
||||||
m_activeVoices.back().m_engineIt = it;
|
m_activeVoices.back().m_engineIt = it;
|
||||||
return &m_activeVoices.back();
|
return &m_activeVoices.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Submix* Engine::_allocateSubmix(Submix* smx)
|
||||||
|
{
|
||||||
|
auto it = m_activeSubmixes.emplace(m_activeSubmixes.end(), *this, smx);
|
||||||
|
m_activeSubmixes.back().m_backendSubmix = m_backend.allocateSubmix(m_activeSubmixes.back());
|
||||||
|
m_activeSubmixes.back().m_engineIt = it;
|
||||||
|
return &m_activeSubmixes.back();
|
||||||
|
}
|
||||||
|
|
||||||
std::list<Voice>::iterator Engine::_destroyVoice(Voice* voice)
|
std::list<Voice>::iterator Engine::_destroyVoice(Voice* voice)
|
||||||
{
|
{
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
@ -31,6 +41,15 @@ std::list<Voice>::iterator Engine::_destroyVoice(Voice* voice)
|
|||||||
return m_activeVoices.erase(voice->m_engineIt);
|
return m_activeVoices.erase(voice->m_engineIt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::list<Submix>::iterator Engine::_destroySubmix(Submix* smx)
|
||||||
|
{
|
||||||
|
#ifndef NDEBUG
|
||||||
|
assert(this == &smx->getEngine());
|
||||||
|
#endif
|
||||||
|
smx->_destroy();
|
||||||
|
return m_activeSubmixes.erase(smx->m_engineIt);
|
||||||
|
}
|
||||||
|
|
||||||
AudioGroup* Engine::_findGroupFromSfxId(int sfxId, const AudioGroupSampleDirectory::Entry*& entOut) const
|
AudioGroup* Engine::_findGroupFromSfxId(int sfxId, const AudioGroupSampleDirectory::Entry*& entOut) const
|
||||||
{
|
{
|
||||||
for (const auto& grp : m_audioGroups)
|
for (const auto& grp : m_audioGroups)
|
||||||
@ -62,13 +81,14 @@ void Engine::pumpEngine()
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Add audio group data pointers to engine; must remain resident! */
|
/** Add audio group data pointers to engine; must remain resident! */
|
||||||
bool Engine::addAudioGroup(int groupId, const AudioGroupData& data)
|
const AudioGroup* Engine::addAudioGroup(int groupId, const AudioGroupData& data)
|
||||||
{
|
{
|
||||||
std::unique_ptr<AudioGroup> grp = std::make_unique<AudioGroup>(groupId, data);
|
std::unique_ptr<AudioGroup> grp = std::make_unique<AudioGroup>(groupId, data);
|
||||||
if (!grp)
|
if (!grp)
|
||||||
return false;
|
return nullptr;
|
||||||
|
AudioGroup* ret = grp.get();
|
||||||
m_audioGroups.emplace(std::make_pair(groupId, std::move(grp)));
|
m_audioGroups.emplace(std::make_pair(groupId, std::move(grp)));
|
||||||
return true;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove audio group from engine */
|
/** Remove audio group from engine */
|
||||||
@ -78,6 +98,7 @@ void Engine::removeAudioGroup(int groupId)
|
|||||||
{
|
{
|
||||||
if (it->getAudioGroup().groupId() == groupId)
|
if (it->getAudioGroup().groupId() == groupId)
|
||||||
{
|
{
|
||||||
|
it->_destroy();
|
||||||
it = m_activeVoices.erase(it);
|
it = m_activeVoices.erase(it);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -88,6 +109,7 @@ void Engine::removeAudioGroup(int groupId)
|
|||||||
{
|
{
|
||||||
if (it->getAudioGroup().groupId() == groupId)
|
if (it->getAudioGroup().groupId() == groupId)
|
||||||
{
|
{
|
||||||
|
it->_destroy();
|
||||||
it = m_activeEmitters.erase(it);
|
it = m_activeEmitters.erase(it);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -98,6 +120,7 @@ void Engine::removeAudioGroup(int groupId)
|
|||||||
{
|
{
|
||||||
if (it->getAudioGroup().groupId() == groupId)
|
if (it->getAudioGroup().groupId() == groupId)
|
||||||
{
|
{
|
||||||
|
it->_destroy();
|
||||||
it = m_activeSequencers.erase(it);
|
it = m_activeSequencers.erase(it);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -107,15 +130,54 @@ void Engine::removeAudioGroup(int groupId)
|
|||||||
m_audioGroups.erase(groupId);
|
m_audioGroups.erase(groupId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create new Submix (a.k.a 'Studio') within root mix engine */
|
||||||
|
Submix* Engine::addSubmix(Submix* smx)
|
||||||
|
{
|
||||||
|
return _allocateSubmix(smx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove Submix and deallocate */
|
||||||
|
void Engine::removeSubmix(Submix* smx)
|
||||||
|
{
|
||||||
|
/* Delete all voices bound to submix */
|
||||||
|
for (auto it = m_activeVoices.begin() ; it != m_activeVoices.end() ;)
|
||||||
|
{
|
||||||
|
Submix* vsmx = it->getSubmix();
|
||||||
|
if (vsmx && vsmx == smx)
|
||||||
|
{
|
||||||
|
it->_destroy();
|
||||||
|
it = m_activeVoices.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete all submixes bound to submix */
|
||||||
|
for (auto it = m_activeSubmixes.begin() ; it != m_activeSubmixes.end() ;)
|
||||||
|
{
|
||||||
|
Submix* ssmx = it->getParentSubmix();
|
||||||
|
if (ssmx && ssmx == smx)
|
||||||
|
{
|
||||||
|
it->_destroy();
|
||||||
|
it = m_activeSubmixes.erase(it);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Delete submix */
|
||||||
|
_destroySubmix(smx);
|
||||||
|
}
|
||||||
|
|
||||||
/** Start soundFX playing from loaded audio groups */
|
/** Start soundFX playing from loaded audio groups */
|
||||||
Voice* Engine::fxStart(int sfxId, float vol, float pan)
|
Voice* Engine::fxStart(int sfxId, float vol, float pan, Submix* smx)
|
||||||
{
|
{
|
||||||
const AudioGroupSampleDirectory::Entry* entry;
|
const AudioGroupSampleDirectory::Entry* entry;
|
||||||
AudioGroup* grp = _findGroupFromSfxId(sfxId, entry);
|
AudioGroup* grp = _findGroupFromSfxId(sfxId, entry);
|
||||||
if (!grp)
|
if (!grp)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
Voice* ret = _allocateVoice(*grp, entry->m_sampleRate, true, false);
|
Voice* ret = _allocateVoice(*grp, entry->m_sampleRate, true, false, smx);
|
||||||
ret->setVolume(vol);
|
ret->setVolume(vol);
|
||||||
ret->setPanning(pan);
|
ret->setPanning(pan);
|
||||||
return ret;
|
return ret;
|
||||||
@ -123,14 +185,14 @@ Voice* Engine::fxStart(int sfxId, float vol, float pan)
|
|||||||
|
|
||||||
/** Start soundFX playing from loaded audio groups, attach to positional emitter */
|
/** Start soundFX playing from loaded audio groups, attach to positional emitter */
|
||||||
Emitter* Engine::addEmitter(const Vector3f& pos, const Vector3f& dir, float maxDist,
|
Emitter* Engine::addEmitter(const Vector3f& pos, const Vector3f& dir, float maxDist,
|
||||||
float falloff, int sfxId, float minVol, float maxVol)
|
float falloff, int sfxId, float minVol, float maxVol, Submix* smx)
|
||||||
{
|
{
|
||||||
const AudioGroupSampleDirectory::Entry* entry;
|
const AudioGroupSampleDirectory::Entry* entry;
|
||||||
AudioGroup* grp = _findGroupFromSfxId(sfxId, entry);
|
AudioGroup* grp = _findGroupFromSfxId(sfxId, entry);
|
||||||
if (!grp)
|
if (!grp)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
||||||
Voice* vox = _allocateVoice(*grp, entry->m_sampleRate, true, true);
|
Voice* vox = _allocateVoice(*grp, entry->m_sampleRate, true, true, smx);
|
||||||
m_activeEmitters.emplace_back(*this, *grp, *vox);
|
m_activeEmitters.emplace_back(*this, *grp, *vox);
|
||||||
Emitter& ret = m_activeEmitters.back();
|
Emitter& ret = m_activeEmitters.back();
|
||||||
ret.setPos(pos);
|
ret.setPos(pos);
|
||||||
|
@ -438,7 +438,7 @@ bool SoundMacroState::advance(Voice& vox, float dt)
|
|||||||
//int8_t priority = cmd.m_data[5];
|
//int8_t priority = cmd.m_data[5];
|
||||||
//int8_t maxVoices = cmd.m_data[6];
|
//int8_t maxVoices = cmd.m_data[6];
|
||||||
|
|
||||||
Voice* sibVox = vox.startSiblingMacro(addNote, macroId, macroStep);
|
Voice* sibVox = vox.startChildMacro(addNote, macroId, macroStep);
|
||||||
if (sibVox)
|
if (sibVox)
|
||||||
m_lastPlayMacroVid = sibVox->vid();
|
m_lastPlayMacroVid = sibVox->vid();
|
||||||
|
|
||||||
|
20
lib/Submix.cpp
Normal file
20
lib/Submix.cpp
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
#include "amuse/Submix.hpp"
|
||||||
|
|
||||||
|
namespace amuse
|
||||||
|
{
|
||||||
|
|
||||||
|
void Submix::_destroy()
|
||||||
|
{
|
||||||
|
m_destroyed = true;
|
||||||
|
if (m_submix)
|
||||||
|
m_submix->m_activeSubmixes.erase(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Submix::Submix(Engine& engine, Submix* smx)
|
||||||
|
: m_root(engine), m_submix(smx)
|
||||||
|
{
|
||||||
|
if (m_submix)
|
||||||
|
m_submix->m_activeSubmixes.insert(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,31 +1,36 @@
|
|||||||
#include "amuse/Voice.hpp"
|
#include "amuse/Voice.hpp"
|
||||||
|
#include "amuse/Submix.hpp"
|
||||||
#include "amuse/IBackendVoice.hpp"
|
#include "amuse/IBackendVoice.hpp"
|
||||||
|
|
||||||
namespace amuse
|
namespace amuse
|
||||||
{
|
{
|
||||||
|
|
||||||
Voice::Voice(Engine& engine, const AudioGroup& group, int vid, bool emitter)
|
|
||||||
: Entity(engine, group), m_vid(vid), m_emitter(emitter)
|
|
||||||
{}
|
|
||||||
|
|
||||||
Voice::Voice(Engine& engine, const AudioGroup& group, ObjectId oid, int vid, bool emitter)
|
|
||||||
: Entity(engine, group, oid), m_vid(vid), m_emitter(emitter)
|
|
||||||
{}
|
|
||||||
|
|
||||||
void Voice::_destroy()
|
void Voice::_destroy()
|
||||||
{
|
{
|
||||||
Entity::_destroy();
|
Entity::_destroy();
|
||||||
if (m_prevSibling)
|
if (m_submix)
|
||||||
m_prevSibling->m_nextSibling = m_nextSibling;
|
m_submix->m_activeVoices.erase(this);
|
||||||
if (m_nextSibling)
|
}
|
||||||
m_nextSibling->m_prevSibling = m_prevSibling;
|
|
||||||
|
Voice::Voice(Engine& engine, const AudioGroup& group, int vid, bool emitter, Submix* smx)
|
||||||
|
: Entity(engine, group), m_vid(vid), m_emitter(emitter), m_submix(smx)
|
||||||
|
{
|
||||||
|
if (m_submix)
|
||||||
|
m_submix->m_activeVoices.insert(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
Voice::Voice(Engine& engine, const AudioGroup& group, ObjectId oid, int vid, bool emitter, Submix* smx)
|
||||||
|
: Entity(engine, group, oid), m_vid(vid), m_emitter(emitter), m_submix(smx)
|
||||||
|
{
|
||||||
|
if (m_submix)
|
||||||
|
m_submix->m_activeVoices.insert(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t Voice::supplyAudio(size_t frames, int16_t* data)
|
size_t Voice::supplyAudio(size_t frames, int16_t* data)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Voice* Voice::startSiblingMacro(int8_t addNote, ObjectId macroId, int macroStep)
|
Voice* Voice::startChildMacro(int8_t addNote, ObjectId macroId, int macroStep)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user