-
-
Save atsushieno/d47a82739c64595da2f15cd8bc87673a to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "CLAPAudioPluginFormat.h" | |
namespace clap_juce_hosting { | |
// HostedAudioProcessorParameter::setValue() implementation | |
uint32_t clap_juce_host_in_events_set_value_size(const struct clap_input_events *list) { | |
return 1; | |
} | |
const clap_event_header_t* clap_juce_host_in_events_set_value_get(const struct clap_input_events *list, uint32_t index) { | |
auto vp = (CLAPHostedAudioProcessorParameter::HostedParameterNewValue*) list->ctx; | |
auto parameter = vp->parameter; | |
auto value = vp->new_value; | |
delete vp; // should be correct | |
parameter->set_value_event = clap_event_param_value { | |
clap_event_header_t{sizeof(clap_event_param_value) - sizeof(clap_event_header_t), 0, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_VALUE, 0}, | |
parameter->info.id, nullptr, -1, -1, -1, -1, value}; | |
return ¶meter->set_value_event.header; | |
} | |
void CLAPHostedAudioProcessorParameter::setValue(float newValue) { | |
// If the processor is actively running, then we have to put the event to the waiting queue and process them at process(). | |
// If not, then the event queue is never processed, so we have to use param_ext.flush(). | |
// It is somewhat complicated because we have to implement and set up an extra clap_input_events, and enqueue the event for it. | |
if (!ctx->is_processing) { | |
auto vp = new CLAPHostedAudioProcessorParameter::HostedParameterNewValue{this, newValue, 0}; // it is deleted at get(). | |
set_value_input_events = clap_input_events{vp, | |
clap_juce_host_in_events_set_value_size, | |
clap_juce_host_in_events_set_value_get}; | |
ctx->params_ext->flush(ctx->plugin, &set_value_input_events, nullptr); | |
} else { | |
// FIXME: assign appropriate time | |
// FIXME: should we pass CLAP_EVENT_IS_LIVE? It's not obvious from juce::HostedAudioProcessorParameter class. | |
set_value_event = clap_event_param_value { | |
clap_event_header_t{sizeof(clap_event_param_value), 0, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_PARAM_VALUE, 0}, | |
info.id, info.cookie, -1, -1, -1, -1, newValue}; | |
instance->ctx->editor_event_list.push(&set_value_event.header); | |
} | |
} | |
// log support | |
void clap_juce_host_ext_log(const clap_host_t *host, clap_log_severity severity, const char *msg) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
ctx->logger.addEntry(severity, msg); | |
} | |
clap_host_log_t host_ext_log{clap_juce_host_ext_log}; | |
// audio ports support | |
bool clap_juce_host_ext_audio_ports_is_rescan_flag_supported(const clap_host_t *host, uint32_t flag) { | |
return false; // TODO: implement | |
} | |
void clap_juce_host_ext_audio_ports_rescan(const clap_host_t *host, uint32_t flags) { | |
// TODO: implement | |
} | |
clap_host_audio_ports_t host_ext_audio_ports{ | |
clap_juce_host_ext_audio_ports_is_rescan_flag_supported, | |
clap_juce_host_ext_audio_ports_rescan}; | |
// note ports support | |
uint32_t clap_juce_host_ext_note_ports_supported_dialects(const clap_host_t *host) { | |
return CLAP_NOTE_DIALECT_MIDI || CLAP_NOTE_DIALECT_CLAP; | |
} | |
void clap_juce_host_ext_note_ports_rescan(const clap_host_t *host, uint32_t flags) { | |
// TODO: implement | |
} | |
clap_host_note_ports_t host_ext_note_ports{ | |
clap_juce_host_ext_note_ports_supported_dialects, | |
clap_juce_host_ext_note_ports_rescan}; | |
// thread check support | |
bool clap_juce_host_ext_is_main_thread(const clap_host_t *host) { | |
return juce::MessageManager::getInstance()->isThisTheMessageThread(); | |
} | |
bool clap_juce_host_ext_is_audio_thread(const clap_host_t *host) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
return ctx->audio_thread_id == juce::Thread::getCurrentThreadId(); | |
} | |
clap_host_thread_check_t host_ext_thread_check{clap_juce_host_ext_is_main_thread, clap_juce_host_ext_is_audio_thread}; | |
// "thread pool" support | |
bool clap_juce_host_ext_request_exec(const clap_host_t *host, uint32_t num_tasks) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
ctx->realtimeParallelFor(num_tasks); | |
} | |
clap_host_thread_pool_t host_ext_thread_pool{clap_juce_host_ext_request_exec}; | |
// audio ports config support | |
void clap_juce_host_ext_audio_ports_config_rescan(const clap_host_t *host) { | |
// TODO: implement | |
} | |
clap_host_audio_ports_config_t host_ext_audio_ports_config{clap_juce_host_ext_audio_ports_config_rescan}; | |
// Timer support | |
void CLAPHostMultiTimer::timerCallback(int timerID) { | |
if (ctx->plugin) { | |
auto pluginTimer = (clap_plugin_timer_support_t*) ctx->plugin->get_extension(ctx->plugin, CLAP_EXT_TIMER_SUPPORT); | |
if (pluginTimer) | |
pluginTimer->on_timer(ctx->plugin, timerID); | |
} | |
} | |
bool clap_juce_host_ext_register_timer(const clap_host_t *host, uint32_t period_ms, clap_id *timer_id) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
std::mutex mutex; | |
mutex.lock(); | |
*timer_id = ctx->last_timer_id++; | |
ctx->timer.startTimer(*timer_id, period_ms); | |
mutex.unlock(); | |
} | |
bool clap_juce_host_ext_unregister_timer(const clap_host_t *host, clap_id timer_id) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
ctx->timer.stopTimer(timer_id); | |
} | |
clap_host_timer_support_t host_ext_timer_support{clap_juce_host_ext_register_timer, clap_juce_host_ext_unregister_timer}; | |
// FD support | |
bool clap_juce_host_ext_register_fd(const clap_host_t *host, int fd, clap_posix_fd_flags_t flags) { | |
return false; // TODO: implement | |
} | |
bool clap_juce_host_ext_modify_fd(const clap_host_t *host, int fd, clap_posix_fd_flags_t flags) { | |
return false; // TODO: implement | |
} | |
bool clap_juce_host_ext_unregister_fd(const clap_host_t *host, int fd) { | |
return false; // TODO: implement | |
} | |
clap_host_posix_fd_support_t host_ext_posix_fd_support { | |
clap_juce_host_ext_register_fd, | |
clap_juce_host_ext_modify_fd, | |
clap_juce_host_ext_unregister_fd}; | |
// file reference support | |
void clap_juce_host_ext_file_reference_changed(const clap_host_t *host) { | |
// TODO: implement | |
} | |
void clap_juce_host_ext_file_reference_set_dirty(const clap_host_t *host, clap_id resource_id) { | |
// TODO: implement | |
} | |
clap_host_file_reference host_ext_file_reference { | |
clap_juce_host_ext_file_reference_changed, | |
clap_juce_host_ext_file_reference_set_dirty | |
}; | |
// latency support | |
void clap_juce_host_ext_latency_changed(const clap_host_t *host) { | |
// is it correct interpretation? | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
ctx->instance->setLatencySamples(ctx->latency_ext->get(ctx->plugin)); | |
} | |
clap_host_latency_t host_ext_latency { clap_juce_host_ext_latency_changed }; | |
// gui support | |
void clap_juce_host_ext_gui_resize_hints_changed(const clap_host_t *host) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
auto editor = (CLAPHostAudioProcessorEditor*) ctx->instance->getActiveEditor(); | |
editor->updateResizeHints(); | |
} | |
bool clap_juce_host_ext_gui_request_resize(const clap_host_t *host, uint32_t width, uint32_t height) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
auto editor = ctx->instance->getActiveEditor(); | |
editor->setSize(width, height); | |
return true; | |
} | |
bool clap_juce_host_ext_gui_request_show(const clap_host_t *host) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
auto editor = (CLAPHostAudioProcessorEditor*) ctx->instance->getActiveEditor(); | |
return editor->onShowRequested(); | |
} | |
bool clap_juce_host_ext_gui_request_hide(const clap_host_t *host) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
auto editor = (CLAPHostAudioProcessorEditor*) ctx->instance->getActiveEditor(); | |
return editor->onHideRequested(); | |
} | |
void clap_juce_host_ext_gui_closed(const clap_host_t *host, bool was_destroyed) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
auto editor = ctx->instance->getActiveEditor(); | |
// FIXME: implement the actual disposal? | |
} | |
clap_host_gui_t host_ext_gui { | |
clap_juce_host_ext_gui_resize_hints_changed, | |
clap_juce_host_ext_gui_request_resize, | |
clap_juce_host_ext_gui_request_show, | |
clap_juce_host_ext_gui_request_hide, | |
clap_juce_host_ext_gui_closed | |
}; | |
void clap_juce_host_ext_params_rescan(const clap_host_t *host, clap_param_rescan_flags flags) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
ctx->instance->buildParameters(); | |
} | |
void clap_juce_host_ext_params_clear(const clap_host_t *host, clap_id param_id, clap_param_clear_flags flags) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
for (auto p : ctx->instance->getParameters()) | |
if (p->getValue() != p->getDefaultValue()) | |
p->setValue(p->getDefaultValue()); | |
} | |
void clap_juce_host_ext_params_request_flush(const clap_host_t *host) { | |
// TODO: implement | |
} | |
clap_host_params_t host_ext_params { | |
clap_juce_host_ext_params_rescan, | |
clap_juce_host_ext_params_clear, | |
clap_juce_host_ext_params_request_flush | |
}; | |
// state support | |
void clap_juce_host_ext_state_mark_dirty(const clap_host_t *host) { | |
// TODO: implement | |
} | |
clap_host_state_t host_ext_state{ clap_juce_host_ext_state_mark_dirty }; | |
// clap_host_t implementation | |
const void* clap_juce_host_get_extension(const clap_host_t* host, const char* name) { | |
if (strcmp(name, CLAP_EXT_LOG) == 0) | |
return &host_ext_log; | |
if (strcmp(name, CLAP_EXT_AUDIO_PORTS) == 0) | |
return &host_ext_audio_ports; | |
if (strcmp(name, CLAP_EXT_NOTE_PORTS) == 0) | |
return &host_ext_note_ports; | |
if (strcmp(name, CLAP_EXT_THREAD_CHECK) == 0) | |
return &host_ext_thread_check; | |
if (strcmp(name, CLAP_EXT_THREAD_POOL) == 0) | |
return &host_ext_thread_pool; | |
if (strcmp(name, CLAP_EXT_AUDIO_PORTS_CONFIG) == 0) | |
return &host_ext_audio_ports_config; | |
//if (strcmp(name, CLAP_EXT_TIMER_SUPPORT) == 0) | |
// return &host_ext_timer_support; | |
if (strcmp(name, CLAP_EXT_POSIX_FD_SUPPORT) == 0) | |
return nullptr;//&host_ext_posix_fd_support; | |
if (strcmp(name, CLAP_EXT_FILE_REFERENCE) == 0) | |
return nullptr;//&host_ext_file_reference; | |
if (strcmp(name, CLAP_EXT_LATENCY) == 0) | |
return &host_ext_latency; | |
if (strcmp(name, CLAP_EXT_GUI) == 0) | |
return &host_ext_gui; | |
if (strcmp(name, CLAP_EXT_PARAMS) == 0) | |
return &host_ext_params; | |
if (strcmp(name, CLAP_EXT_STATE) == 0) | |
return &host_ext_state; | |
return nullptr; | |
} | |
void clap_juce_host_request_restart(const clap_host_t* host) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
auto playHead = ctx->instance->getPlayHead(); | |
if (playHead) { | |
playHead->transportPlay(false); | |
playHead->transportPlay(true); | |
} | |
} | |
void clap_juce_host_request_process(const clap_host_t* host) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
auto playHead = ctx->instance->getPlayHead(); | |
if (playHead) | |
playHead->transportPlay(true); | |
} | |
void clap_juce_host_request_callback(const clap_host_t* host) { | |
auto ctx = (CLAPFactoryContext*) host->host_data; | |
juce::MessageManager::callAsync([ctx]() { | |
ctx->plugin->on_main_thread(ctx->plugin); | |
}); | |
} | |
} // namespaces |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#ifndef JUCE_CLAPAUDIOPLUGINFORMAT_H | |
#define JUCE_CLAPAUDIOPLUGINFORMAT_H | |
#include "JuceHeader.h" | |
#include <dlfcn.h> | |
#include "include/clap/clap.h" | |
#include "include/clap/helpers/event-list.hh" | |
#ifdef _OPENMP | |
#include <omp.h> | |
#endif | |
namespace clap_juce_hosting { | |
const void* clap_juce_host_get_extension(const clap_host_t* host, const char* name); | |
void clap_juce_host_request_restart(const clap_host_t*); | |
void clap_juce_host_request_process(const clap_host_t*); | |
void clap_juce_host_request_callback(const clap_host_t*); | |
class CLAPFactoryContext; | |
class CLAPHostMultiTimer : public juce::MultiTimer { | |
CLAPFactoryContext* ctx; | |
public: | |
explicit CLAPHostMultiTimer(CLAPFactoryContext* ctx) : ctx(ctx) {} | |
void timerCallback (int timerID) override; | |
JUCE_LEAK_DETECTOR (CLAPHostMultiTimer) | |
}; | |
// FIXME: split factory context and instance context | |
class CLAPInstanceContext { | |
public: | |
CLAPFactoryContext* factory; | |
}; | |
class CLAPAudioPluginInstance; | |
class Logger { | |
CLAPFactoryContext* ctx; | |
#define MAX_LOG_MESSAGE_SIZE 4096 | |
struct LogEntry { | |
clap_log_severity severiry; | |
const char* message; // point to an entry in cached_log_messages | |
}; | |
#define MAX_LOG_ENTRIES_PER_CYCLE 128 | |
char cached_log_messages[MAX_LOG_ENTRIES_PER_CYCLE][MAX_LOG_MESSAGE_SIZE]; | |
LogEntry cached_log_entries[MAX_LOG_ENTRIES_PER_CYCLE]; | |
volatile size_t cached_log_entries_count{0}; | |
const char* reached_max_log_entries = "The logger encountered too many log entries to process per its non-realtime cycle. Further log entries per this cycle are discarded."; | |
static void default_logging(clap_log_severity severity, const char *msg) { | |
// TODO: make it RT-ready i.e. just enqueue it for non-RT logging thread. | |
printf("CLAPJUCEHost [%s]: ", getLogLevelDescription(severity)); | |
puts(msg); | |
} | |
public: | |
explicit Logger(CLAPFactoryContext* context) : ctx(context) {} | |
static const char* getLogLevelDescription(clap_log_severity severity) { | |
switch (severity) { | |
case CLAP_LOG_FATAL: return "FATAL"; | |
case CLAP_LOG_ERROR: return "ERROR"; | |
case CLAP_LOG_WARNING: return "WARNING"; | |
case CLAP_LOG_INFO: return "INFO"; | |
case CLAP_LOG_DEBUG: return "DEBUG"; | |
case CLAP_LOG_PLUGIN_MISBEHAVING: return "PLUGIN"; | |
case CLAP_LOG_HOST_MISBEHAVING: return "HOST"; | |
default: return ""; | |
} | |
} | |
void addEntry(clap_log_severity severity, const char* msg) { | |
if (cached_log_entries_count == MAX_LOG_ENTRIES_PER_CYCLE) | |
return; // already reached at the maximum number of cached log entries. | |
if (cached_log_entries_count++ == MAX_LOG_ENTRIES_PER_CYCLE) { | |
cached_log_entries[MAX_LOG_ENTRIES_PER_CYCLE - 1] = LogEntry{CLAP_LOG_WARNING, reached_max_log_entries}; | |
return; | |
} | |
size_t at = cached_log_entries_count - 1; | |
strncpy(cached_log_messages[at], msg, MAX_LOG_MESSAGE_SIZE); | |
cached_log_entries[at] = LogEntry{severity, cached_log_messages[at]}; | |
// TODO: launch a non-RT thread that processes flushLogs(). calling it violates RT safety. | |
flushLogs(); | |
} | |
void flushLogs() { | |
size_t numEntries = cached_log_entries_count; | |
for (size_t i = 0; i < numEntries; i++) { | |
auto &e = cached_log_entries[i]; | |
log(e.severiry, e.message); | |
} | |
for (size_t i = numEntries; i < cached_log_entries_count; i++) | |
cached_log_entries[i - numEntries] = cached_log_entries[i]; | |
cached_log_entries_count -= numEntries; | |
} | |
std::function<void(clap_log_severity, const char*)> log = | |
[](clap_log_severity severity, const char *msg) { default_logging(severity, msg); }; | |
JUCE_LEAK_DETECTOR (Logger) | |
}; | |
class CLAPFactoryContext { | |
public: | |
void* dl{nullptr}; | |
const clap_plugin_entry_t * entrypoint{nullptr}; | |
const clap_plugin_factory_t *factory{nullptr}; | |
clap_host_t host{}; | |
const clap_plugin_t *plugin{nullptr}; | |
CLAPAudioPluginInstance* instance{nullptr}; | |
Logger logger{this}; | |
// they are the actual audio buffers, allocated for all inputs, managed in flat arrays. | |
void* audio_input_buffers[128]; | |
void* audio_output_buffers[128]; | |
#define DEFAULT_PROCESS_EVENT_BUFFER_SIZE 524288 | |
#define DEFAULT_EDITOR_EVENT_BUFFER_SIZE 8192 | |
size_t process_event_buffer_size{DEFAULT_PROCESS_EVENT_BUFFER_SIZE}; | |
size_t editor_event_buffer_size{DEFAULT_EDITOR_EVENT_BUFFER_SIZE}; | |
// One is passed to clap_process_t. Another holds the input events from the UI as well as hold non-RT output events. | |
// Since EventList is not a sorted list, we have to merge two inputs (editor_event_list and MidiBuffer) at procss()-time. | |
clap::helpers::EventList proc_event_list{}; | |
clap::helpers::EventList editor_event_list{}; | |
//void* events_input_buffer{nullptr}; | |
//void* events_output_buffer{nullptr}; | |
// they are assigned to clap_process_t | |
clap_audio_buffer clap_audio_input_array[128]; | |
clap_audio_buffer clap_audio_output_array[128]; | |
clap_process_t process{0, 0, nullptr, clap_audio_input_array, clap_audio_output_array, 0, 0, nullptr, nullptr}; | |
const clap_plugin_audio_ports_t *audio_ports_ext{nullptr}; | |
const clap_plugin_note_ports_t *note_ports_ext{nullptr}; | |
const clap_plugin_tail_t *tail_ext{nullptr}; | |
const clap_plugin_state_t *state_ext{nullptr}; | |
const clap_plugin_params *params_ext{nullptr}; | |
const clap_plugin_gui_t *gui_ext{nullptr}; | |
const clap_plugin_latency_t *latency_ext{nullptr}; | |
const clap_plugin_thread_pool_t *thread_pool_ext{nullptr}; | |
juce::Thread::ThreadID audio_thread_id{nullptr}; | |
CLAPHostMultiTimer timer{this}; | |
int last_timer_id{0}; | |
bool is_processing{false}; | |
juce::ThreadPool thread_pool{}; | |
~CLAPFactoryContext() { | |
if (plugin) | |
plugin->destroy(plugin); | |
if (entrypoint) | |
entrypoint->deinit(); | |
if (dl) | |
dlclose(dl); | |
} | |
bool realtimeParallelFor(int numTasks) { | |
// It turned out that "thread pool" is not really about thread pool like LV2 Worker, but rather about realtime | |
// parallel processing. That is, the host can prepare multiple RT threads depending on the CPU cores and let them | |
// perform the designated work as the plugin's thread_pool `exec()`. | |
// They have to finish within RT throttle, and returns back to the host. | |
// The host only cares about running the tasks (fires and forgets), and does not care if the plugin actually | |
// completed the job or not. `request_exec()` is called by the plugin, and plugins can either wait for the results | |
// from those tasks, e.g. by spinning, or continue without the results (e.g. "unprepared voices are ignored"). | |
// | |
// So, to implement this functionality we would need some implementation for platform-agnostic realtime | |
// parallel processing. | |
#ifdef _OPENMP | |
if (thread_pool_ext != nullptr && !omp_in_parallel() && omp_get_max_threads() - omp_get_num_threads() >= numTasks) { | |
#pragma omp parallel for | |
for (int i = 0; i < numTasks; i++) | |
thread_pool_ext->exec(plugin, i); // We do not (cannot) care if the actual task was completed in hard RT. | |
} | |
#pragma omp end parallel | |
return true; | |
#else | |
return false; | |
#endif | |
} | |
JUCE_LEAK_DETECTOR (CLAPFactoryContext) | |
}; | |
uint32_t clap_juce_host_in_events_size(const struct clap_input_events *list); | |
const clap_event_header_t *clap_juce_host_in_events_get(const struct clap_input_events *list, uint32_t index); | |
bool clap_juce_host_out_events_try_push(const struct clap_output_events *list, const clap_event_header_t *event); | |
// ----------------------------------------------------------------------------------------- | |
// HostedAudioPrpcessorParameter implementation | |
// ----------------------------------------------------------------------------------------- | |
class CLAPHostedAudioProcessorParameter : public HostedAudioProcessorParameter { | |
CLAPFactoryContext* ctx; | |
clap_input_events_t set_value_input_events{}; | |
public: | |
struct HostedParameterNewValue { | |
CLAPHostedAudioProcessorParameter *parameter; | |
float new_value; | |
int32_t sample_position_since_last_process; | |
}; | |
CLAPAudioPluginInstance* instance; | |
clap_param_info_t info; | |
clap_event_param_value set_value_event{}; | |
CLAPHostedAudioProcessorParameter(CLAPAudioPluginInstance* instance, CLAPFactoryContext* ctx, clap_param_info_t& info) | |
: ctx(ctx), instance(instance), info(info) { | |
} | |
float getValue() const override { | |
double value; | |
return ctx->params_ext->get_value(ctx->plugin, info.id, &value) ? (float) value : 0; | |
} | |
void setValue(float newValue) override; | |
float getDefaultValue() const override { | |
return (float) info.default_value; | |
} | |
String getName(int maximumStringLength) const override { | |
return String(info.name).substring(0, maximumStringLength); | |
} | |
String getLabel() const override { | |
return String(info.name); | |
} | |
float getValueForText(const String &text) const override { | |
double ret; | |
return ctx->params_ext->text_to_value(ctx->plugin, info.id, text.toRawUTF8(), &ret) ? (float) ret : 0; | |
} | |
String getParameterID() const override { | |
return String{info.id}; | |
} | |
JUCE_LEAK_DETECTOR (CLAPHostedAudioProcessorParameter) | |
}; | |
// ----------------------------------------------------------------------------------------- | |
// AudioProcessorEditor implementation | |
// ----------------------------------------------------------------------------------------- | |
class CLAPHostAudioProcessorEditor : public AudioProcessorEditor { | |
class EditorComponentListener : public ComponentListener{ | |
CLAPHostAudioProcessorEditor* editor; | |
public: | |
explicit EditorComponentListener(CLAPHostAudioProcessorEditor* editor) : editor(editor) {} | |
void componentVisibilityChanged(Component &component) override { | |
if (editor->isVisible()) | |
editor->ctx->gui_ext->show(editor->ctx->plugin); | |
else | |
editor->ctx->gui_ext->hide(editor->ctx->plugin); | |
} | |
void componentParentHierarchyChanged(Component &component) override { | |
if (&component == editor) { | |
if (editor->getPeer()) | |
editor->onAttachedToNewParent(); | |
else | |
editor->onDetachedFromParent(); | |
} | |
} | |
}; | |
CLAPFactoryContext *ctx; | |
clap_window_t window{}; | |
bool floating{false}; | |
EditorComponentListener component_listener{this}; | |
public: | |
CLAPHostAudioProcessorEditor(AudioProcessor *processor, CLAPFactoryContext *ctx) | |
: AudioProcessorEditor(processor), ctx(ctx) { | |
#if JUCE_WINDOWS | |
const char *default_api = CLAP_WINDOW_API_WIN32; | |
#elif JUCE_MAC | |
const char *default_api = CLAP_WINDOW_API_COCOA; | |
#else | |
const char *default_api = CLAP_WINDOW_API_X11; | |
#endif | |
window.api = default_api; | |
if (!ctx->gui_ext->get_preferred_api(ctx->plugin, &window.api, &floating)) { | |
window.api = default_api; | |
floating = false; | |
assert(ctx->gui_ext->is_api_supported(ctx->plugin, window.api, floating)); | |
} | |
assert(ctx->gui_ext->create(ctx->plugin, window.api, floating)); | |
uint32_t w, h; | |
ctx->gui_ext->get_size(ctx->plugin, &w, &h); | |
setSize((int) w, (int) h); | |
if (floating) { | |
ctx->gui_ext->set_transient(ctx->plugin, &window); | |
ctx->gui_ext->suggest_title(ctx->plugin, "JUCE CLAP Host"); | |
} else { | |
this->setTitle("JUCE CLAP Host"); | |
ctx->gui_ext->set_scale(ctx->plugin, getDesktopScaleFactor()); | |
ctx->gui_ext->show(ctx->plugin); | |
} | |
addComponentListener(&component_listener); | |
} | |
~CLAPHostAudioProcessorEditor() override { | |
ctx->gui_ext->destroy(ctx->plugin); | |
removeComponentListener(&component_listener); | |
processor.editorBeingDeleted(this); | |
AudioProcessorEditor::~AudioProcessorEditor(); | |
} | |
void onAttachedToNewParent() { | |
auto h = getWindowHandle(); | |
if (h == nullptr) | |
return; | |
if (!window.ptr) | |
ctx->gui_ext->show(ctx->plugin); | |
window.ptr = h; | |
if (ctx->gui_ext->set_parent(ctx->plugin, &window)) { | |
auto c = (DocumentWindow*) getTopLevelComponent(); | |
//auto newRect = getBounds().withTrimmedTop(c->getTitleBarHeight()); | |
//setBounds(newRect); | |
} | |
} | |
void onDetachedFromParent() { | |
#if JUCE_WINDOWS | |
window.win32 = nullptr; | |
#elif JUCE_MAC | |
window.cocoa = nullptr; | |
#else | |
window.x11 = 0; | |
#endif | |
ctx->gui_ext->set_parent(ctx->plugin, &window); | |
} | |
bool onShowRequested() { | |
// if it is parented, then parent need to be shown. Otherwise, just indicate the editor itself to show up. | |
if (getPeer()) | |
getPeer()->setVisible(true); | |
else | |
setVisible(true); | |
return true; | |
} | |
bool onHideRequested() { | |
// if it is parented, then parent need to be hidden. Otherwise, just indicate the editor itself to hide. | |
if (getPeer()) | |
getPeer()->setVisible(false); | |
else | |
setVisible(false); | |
return true; | |
} | |
void updateResizeHints() { | |
clap_gui_resize_hints_t hints; | |
ctx->gui_ext->get_resize_hints(ctx->plugin, &hints); | |
auto screenBounds = getScreenBounds(); | |
setResizeLimits( | |
hints.can_resize_horizontally ? getConstrainer()->getMinimumWidth() : getWidth(), | |
hints.can_resize_vertically ? getConstrainer()->getMinimumHeight() : getHeight(), | |
screenBounds.getWidth(), screenBounds.getHeight()); | |
if (hints.preseve_aspect_ratio) // otherwise not supported | |
setScaleFactor(hints.aspect_ratio_height); | |
} | |
JUCE_LEAK_DETECTOR (CLAPHostAudioProcessorEditor) | |
}; | |
// ----------------------------------------------------------------------------------------- | |
// AudioPluginInstance implementation | |
// ----------------------------------------------------------------------------------------- | |
class CLAPAudioPluginInstance : public AudioPluginInstance { | |
friend class CLAPHostedAudioProcessorParameter; | |
std::unique_ptr<CLAPFactoryContext> ctx; | |
PluginDescription desc; | |
String name; | |
double sample_rate; | |
int buffer_size; | |
bool has_midi_in, has_midi_out; | |
public: | |
CLAPAudioPluginInstance(std::unique_ptr<CLAPFactoryContext> context, const PluginDescription& description, double initialSampleRate, int initialBufferSize) | |
: AudioPluginInstance(configureBusesProperties(context.get())), | |
ctx(std::move(context)), | |
sample_rate(initialSampleRate), | |
buffer_size(initialBufferSize) { | |
desc = PluginDescription(description); | |
ctx->instance = this; | |
ctx->note_ports_ext = static_cast<const clap_plugin_note_ports_t *>( | |
ctx->plugin->get_extension(ctx->plugin, CLAP_EXT_NOTE_PORTS)); | |
ctx->audio_ports_ext = (clap_plugin_audio_ports_t *) ctx->plugin->get_extension( | |
ctx->plugin, CLAP_EXT_AUDIO_PORTS); | |
ctx->tail_ext = (clap_plugin_tail_t *) ctx->plugin->get_extension( | |
ctx->plugin, CLAP_EXT_TAIL); | |
ctx->state_ext = (clap_plugin_state_t *) ctx->plugin->get_extension( | |
ctx->plugin, CLAP_EXT_STATE); | |
clap_note_port_info_t port; | |
has_midi_in = ctx->note_ports_ext->count(ctx->plugin, true) && ctx->note_ports_ext->get(ctx->plugin, 0, true, &port) && (port.supported_dialects & CLAP_EVENT_MIDI) != 0; | |
has_midi_out = ctx->note_ports_ext->count(ctx->plugin, false) && ctx->note_ports_ext->get(ctx->plugin, 0, false, &port) && (port.supported_dialects & CLAP_EVENT_MIDI) != 0; | |
buildParameters(); | |
ctx->gui_ext = static_cast<const clap_plugin_gui_t *>(ctx->plugin->get_extension(ctx->plugin, CLAP_EXT_GUI)); | |
ctx->latency_ext = static_cast<const clap_plugin_latency_t *>(ctx->plugin->get_extension(ctx->plugin, CLAP_EXT_LATENCY)); | |
ctx->thread_pool_ext = static_cast<const clap_plugin_thread_pool_t *>(ctx->plugin->get_extension(ctx->plugin, CLAP_EXT_THREAD_POOL)); | |
} | |
static BusesProperties configureBusesProperties(CLAPFactoryContext* ctx) { | |
BusesProperties ret{}; | |
ctx->audio_ports_ext = (clap_plugin_audio_ports_t *) ctx->plugin->get_extension( | |
ctx->plugin, CLAP_EXT_AUDIO_PORTS); | |
for (uint32_t i = 0, n = ctx->audio_ports_ext->count(ctx->plugin, true); i < n; i++) { | |
clap_audio_port_info info; | |
ctx->audio_ports_ext->get(ctx->plugin, i, true, &info); | |
if (!strcmp(info.port_type, CLAP_PORT_MONO)) | |
ret.inputLayouts.add(BusProperties{info.name, AudioChannelSet::mono(), (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0}); | |
else if (!strcmp(info.port_type, CLAP_PORT_STEREO)) | |
ret.inputLayouts.add(BusProperties{info.name, AudioChannelSet::stereo(), (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0}); | |
// TODO: else: implement | |
} | |
for (uint32_t i = 0, n = ctx->audio_ports_ext->count(ctx->plugin, false); i < n; i++) { | |
clap_audio_port_info info; | |
ctx->audio_ports_ext->get(ctx->plugin, i, false, &info); | |
if (!strcmp(info.port_type, CLAP_PORT_MONO)) | |
ret.outputLayouts.add(BusProperties{info.name, AudioChannelSet::mono(), (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0}); | |
else if (!strcmp(info.port_type, CLAP_PORT_STEREO)) | |
ret.outputLayouts.add(BusProperties{info.name, AudioChannelSet::stereo(), (info.flags & CLAP_AUDIO_PORT_IS_MAIN) != 0}); | |
// TODO: else: implement | |
} | |
return ret; | |
} | |
void buildParameters() { | |
ctx->params_ext = static_cast<const clap_plugin_params *>(ctx->plugin->get_extension(ctx->plugin, CLAP_EXT_PARAMS)); | |
AudioProcessorParameterGroup tree{}; | |
for (uint32_t i = 0, n = ctx->params_ext->count(ctx->plugin); i < n; i++) { | |
clap_param_info_t info; | |
if (!ctx->params_ext->get_info(ctx->plugin, i, &info)) { | |
ctx->logger.addEntry(CLAP_LOG_ERROR, String::formatted("Failed to retrieve plugin parameter of \"%s\", at: %d", getName().toRawUTF8(), i).toRawUTF8()); | |
continue; | |
} | |
if (info.module[0] == 0) { | |
tree.addChild(std::make_unique<CLAPHostedAudioProcessorParameter>(this, ctx.get(), info)); | |
} else { | |
static String root{"/"}; | |
AudioProcessorParameterGroup *g = &tree; | |
if ((info.module[0] == '/' && info.module[1] == 0)) | |
g = getOrCreateGroup(g, root); | |
else { | |
StringArray paths{}; | |
std::stringstream ss{info.module}; | |
std::string s{}; | |
while (getline(ss, s, '/')) | |
if (!s.empty()) | |
paths.add(s); | |
for (auto path: paths) | |
if (!path.isEmpty()) | |
g = getOrCreateGroup(g, path); | |
} | |
g->addChild(std::make_unique<CLAPHostedAudioProcessorParameter>(this, ctx.get(), info)); | |
} | |
} | |
setHostedParameterTree(std::move(tree)); | |
} | |
static AudioProcessorParameterGroup* getOrCreateGroup(AudioProcessorParameterGroup *parent, String& subgroupName) { | |
auto children = parent->getSubgroups(false); | |
for (const AudioProcessorParameterGroup* child : children) | |
if (child->getName() == subgroupName) | |
return const_cast<AudioProcessorParameterGroup*>(child); | |
auto newChild = std::make_unique<AudioProcessorParameterGroup>(subgroupName, subgroupName, "/"); | |
auto ret = newChild.get(); | |
parent->addChild(std::move(newChild)); | |
return ret; | |
} | |
const String getName() const override { return desc.descriptiveName; } | |
void prepareToPlay (double sampleRate, | |
int maximumExpectedSamplesPerBlock) override { | |
sample_rate = sampleRate; | |
for (int i = 0, n = getTotalNumInputChannels(); i < n; i++) { | |
if (isUsingDoublePrecision()) | |
ctx->audio_input_buffers[i] = calloc(maximumExpectedSamplesPerBlock, sizeof(double)); | |
else | |
ctx->audio_input_buffers[i] = calloc(maximumExpectedSamplesPerBlock, sizeof(float)); | |
} | |
ctx->audio_input_buffers[getTotalNumInputChannels()] = nullptr; | |
for (int i = 0, n = getTotalNumOutputChannels(); i < n; i++) { | |
if (isUsingDoublePrecision()) | |
ctx->audio_output_buffers[i] = calloc(maximumExpectedSamplesPerBlock, sizeof(double)); | |
else | |
ctx->audio_output_buffers[i] = calloc(maximumExpectedSamplesPerBlock, sizeof(float)); | |
} | |
ctx->audio_output_buffers[getTotalNumOutputChannels()] = nullptr; | |
ctx->proc_event_list.reserveHeap(ctx->process_event_buffer_size); | |
ctx->editor_event_list.reserveHeap(ctx->editor_event_buffer_size); | |
ctx->process.in_events = ctx->proc_event_list.clapInputEvents(); | |
ctx->process.out_events = ctx->proc_event_list.clapOutputEvents(); | |
// set up audio buffers in clap_process_t (in and out). | |
ctx->process.audio_inputs_count = (uint32_t) ctx->audio_ports_ext->count(ctx->plugin, true); | |
auto audioInputs = ctx->clap_audio_input_array; | |
int inChannel = 0; | |
for (uint32_t i = 0, n = ctx->process.audio_inputs_count; i < n; i++) { | |
if (!getBus(true, i)->isEnabled()) | |
continue; | |
auto &inputs = audioInputs[i]; | |
clap_audio_port_info info{}; | |
ctx->audio_ports_ext->get(ctx->plugin, i, true, &info); | |
inputs.channel_count = info.channel_count; | |
if (isUsingDoublePrecision()) { | |
inputs.data64 = (double**) calloc(info.channel_count, sizeof(double*)); | |
for (uint32_t ch = 0, nCh = info.channel_count; ch < nCh; ch++) | |
inputs.data64[ch] = (double *) ctx->audio_input_buffers[inChannel++]; | |
} else { | |
inputs.data32 = (float**) calloc(info.channel_count, sizeof(float*)); | |
for (uint32_t ch = 0, nCh = info.channel_count; ch < nCh; ch++) | |
inputs.data32[ch] = (float *) ctx->audio_input_buffers[inChannel++]; | |
} | |
} | |
ctx->process.audio_inputs = audioInputs; | |
ctx->process.audio_outputs_count = (uint32_t) ctx->audio_ports_ext->count(ctx->plugin, false); | |
clap_audio_buffer_t *outputs = ctx->process.audio_outputs; | |
int outChannel = 0; | |
for (uint32_t i = 0, n = ctx->process.audio_outputs_count; i < n; i++) { | |
if (!getBus(false, i)->isEnabled()) | |
continue; | |
clap_audio_port_info info{}; | |
ctx->audio_ports_ext->get(ctx->plugin, i, false, &info); | |
outputs[i].channel_count = info.channel_count; | |
if (isUsingDoublePrecision()) { | |
outputs[i].data64 = (double**) calloc(info.channel_count, sizeof(double*)); | |
for (uint32_t ch = 0, nCh = info.channel_count; ch < nCh; ch++) | |
outputs[i].data64[ch] = (double *) ctx->audio_output_buffers[outChannel++]; | |
} else { | |
outputs[i].data32 = (float**) calloc(info.channel_count, sizeof(float*)); | |
for (uint32_t ch = 0, nCh = info.channel_count; ch < nCh; ch++) | |
outputs[i].data32[ch] = (float *) ctx->audio_output_buffers[outChannel++]; | |
} | |
} | |
ctx->process.audio_outputs = ctx->clap_audio_output_array; | |
// We are ready to start processing. | |
ctx->plugin->activate(ctx->plugin, sampleRate, 1, static_cast<uint32_t>(maximumExpectedSamplesPerBlock)); | |
} | |
void releaseResources() override { | |
// TODO: how can we call stop_processing() ? It is supposed to be invoked on audio thread, but releaseResources() is not invoked there. | |
//ctx->plugin->stop_processing(ctx->plugin); | |
ctx->is_processing = false; | |
for (int i = 0, n = getTotalNumInputChannels(); i < n; i++) | |
if (ctx->audio_input_buffers[i]) | |
free(ctx->audio_input_buffers[i]); | |
for (int i = 0, n = getTotalNumOutputChannels(); i < n; i++) | |
if (ctx->audio_output_buffers[i]) | |
free(ctx->audio_output_buffers[i]); | |
//if (ctx->events_input_buffer) | |
// free(ctx->events_input_buffer); | |
//if (ctx->events_output_buffer) | |
// free(ctx->events_output_buffer); | |
ctx->proc_event_list.clear(); | |
ctx->editor_event_list.clear(); | |
ctx->plugin->deactivate(ctx->plugin); | |
} | |
void processBlock (AudioBuffer<float>& buffer, | |
MidiBuffer& midiMessages) override { | |
if (!ctx->audio_thread_id) | |
ctx->audio_thread_id = juce::Thread::getCurrentThreadId(); | |
if (!ctx->is_processing) { | |
ctx->is_processing = true; | |
ctx->plugin->start_processing(ctx->plugin); | |
} | |
// Merge MidiBuffer and editor_event_list inputs into proc_event_list inputs. | |
{ | |
size_t editorIndex = 0, editorSize = ctx->editor_event_list.size(); | |
int64_t editorEventPosition = 0, lastMidiPosition = -1; | |
ctx->proc_event_list.empty(); | |
for (auto midiMessage : midiMessages) { | |
// we can skip editor events while the MIDI in events are at the same position | |
// (consider hundreds or thousands of MIDI events at position 0 at the same time...) | |
if (lastMidiPosition < midiMessage.samplePosition) { | |
// insert editor events that are between the previous event and current event, samplePosition wise. | |
while (editorIndex < editorSize) { | |
auto evt = ctx->editor_event_list.get(editorIndex); | |
editorEventPosition = evt->time; | |
if (editorEventPosition <= (int64_t) midiMessage.samplePosition) | |
break; | |
evt->flags |= CLAP_EVENT_IS_LIVE; | |
ctx->proc_event_list.push(evt); | |
editorIndex++; | |
} | |
} | |
// convert MIDI events to CLAP events and insert into proc_event_list. | |
if (midiMessage.numBytes > 0 && midiMessage.data[0] == 0xF0) { | |
clap_event_midi_sysex msg{ | |
clap_event_header_t{(uint32_t) sizeof(clap_event_midi_sysex), (uint32_t) midiMessage.samplePosition, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI_SYSEX, 0}, | |
0, midiMessage.data, (uint32_t) midiMessage.numBytes}; | |
ctx->proc_event_list.push(&msg.header); | |
} else { | |
auto inMsg = midiMessage.getMessage(); | |
/* | |
if (inMsg.isNoteOn()) { | |
clap_event_note msg{ | |
clap_event_header_t{(uint32_t) sizeof(clap_event_midi), (uint32_t) midiMessage.samplePosition, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_NOTE_ON, 0}, | |
-1, 0, (short) inMsg.getChannel(), (short) inMsg.getNoteNumber(), inMsg.getVelocity() / 127.0}; | |
ctx->proc_event_list.push(&msg.header); | |
} else if (inMsg.isNoteOff()) { | |
clap_event_note msg{ | |
clap_event_header_t{(uint32_t) sizeof(clap_event_midi), (uint32_t) midiMessage.samplePosition, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_NOTE_OFF, 0}, | |
-1, 0, (short) inMsg.getChannel(), (short) inMsg.getNoteNumber(), inMsg.getVelocity() / 127.0}; | |
ctx->proc_event_list.push(&msg.header); | |
} else*/ { | |
clap_event_midi msg{ | |
clap_event_header_t{(uint32_t) sizeof(clap_event_midi), (uint32_t) midiMessage.samplePosition, CLAP_CORE_EVENT_SPACE_ID, CLAP_EVENT_MIDI, 0},0}; | |
memcpy(msg.data, midiMessage.data, 3); | |
ctx->proc_event_list.push(&msg.header); | |
} | |
} | |
} | |
// push remaining events to proc_event_list (the loop above does nothing if MIDI list is empty). | |
while (editorIndex < editorSize) { | |
auto evt = ctx->editor_event_list.get(editorIndex++); | |
ctx->proc_event_list.push(evt); | |
} | |
ctx->editor_event_list.empty(); | |
} | |
ctx->process.frames_count = (uint32_t) buffer.getNumSamples(); | |
// Assign the JUCE audio buffer pointers to the CLAP audio buffer pointers in clap_process_t. | |
int inChannel = 0; | |
for (int i = 0, n = getBusCount(true); i < n; i++) { | |
if (!getBus(true, i)->isEnabled()) | |
continue; | |
for (int ch = 0, nCh = getChannelCountOfBus(true, i); ch < nCh; ch++) { | |
if (isUsingDoublePrecision()) | |
ctx->process.audio_inputs[i].data64[ch] = (double*) (void*) buffer.getReadPointer(inChannel); | |
else | |
ctx->process.audio_inputs[i].data32[ch] = (float*) buffer.getReadPointer(inChannel); | |
inChannel++; | |
} | |
} | |
int outChannel = 0; | |
for (int i = 0, n = getBusCount(false); i < n; i++) { | |
if (!getBus(false, i)->isEnabled()) | |
continue; | |
for (int ch = 0, nCh = getChannelCountOfBus(false, i); ch < nCh; ch++) { | |
if (isUsingDoublePrecision()) { | |
ctx->process.audio_outputs[i].data64[ch] = (double*) (void*) buffer.getWritePointer(outChannel);//(double*) ctx->audio_output_buffers[outChannel]; | |
} else { | |
ctx->process.audio_outputs[i].data32[ch] = (float*) buffer.getWritePointer(outChannel); //(float*) ctx->audio_output_buffers[outChannel]; | |
} | |
outChannel++; | |
} | |
} | |
// FIXME: remove this? | |
for (int i = 0, n = getTotalNumInputChannels(); i < n; i++) | |
memset(ctx->audio_input_buffers[i], isUsingDoublePrecision() ? sizeof(double) : sizeof(float), buffer.getNumSamples()); | |
for (int i = 0, n = getTotalNumOutputChannels(); i < n; i++) | |
memset(ctx->audio_output_buffers[i], isUsingDoublePrecision() ? sizeof(double) : sizeof(float), buffer.getNumSamples()); | |
// Let the plugin process it. | |
ctx->plugin->process(ctx->plugin, &ctx->process); | |
{ | |
midiMessages.clear(); | |
// TODO: convert CLAP event outputs to MidiBuffer as outputs (but HOW?) | |
} | |
/* | |
// JUCE output pointers may be the same pointers as input pointers, and we are unsure if the CLAP plugin | |
// anticipates that. So memcpy them instead. | |
outChannel = 0; | |
for (int i = 0, n = getBusCount(false); i < n; i++) { | |
if (!getBus(false, i)->isEnabled()) | |
continue; | |
for (int ch = 0, nCh = getChannelCountOfBus(false, i); ch < nCh; ch++) { | |
if (isUsingDoublePrecision()) | |
memcpy(buffer.getWritePointer(outChannel), ctx->audio_output_buffers[outChannel], | |
sizeof(double) * (size_t) buffer.getNumSamples()); | |
else | |
memcpy(buffer.getWritePointer(outChannel), ctx->audio_output_buffers[outChannel], | |
sizeof(float) * (size_t) buffer.getNumSamples()); | |
outChannel++; | |
} | |
} | |
*/ | |
} | |
double getTailLengthSeconds() const override { | |
return ctx->tail_ext->get(ctx->plugin) * 1.0 / sample_rate; | |
} | |
bool acceptsMidi() const override { return has_midi_in; } | |
bool producesMidi() const override {return has_midi_out; } | |
AudioProcessorEditor* createEditor() override { | |
return ctx->gui_ext ? new CLAPHostAudioProcessorEditor(this, ctx.get()) : nullptr; | |
} | |
bool hasEditor() const override { | |
return ctx->gui_ext != nullptr; | |
} | |
int getNumPrograms() override { | |
return 0; // TODO: implement | |
} | |
int getCurrentProgram() override { | |
return 0; // TODO: implement | |
} | |
void setCurrentProgram (int index) override { | |
// TODO: implement | |
} | |
const String getProgramName (int index) override { | |
return ""; // TODO: implement | |
} | |
void changeProgramName (int index, const String& newName) override { | |
// TODO: implement | |
} | |
static int64_t clap_juce_host_write_to_memory_block(const clap_ostream_t *stream, const void *buffer, uint64_t size) { | |
auto dst = (juce::MemoryBlock*) stream->ctx; | |
dst->append(buffer, (size_t) size); | |
return (int64_t) size; | |
} | |
void getStateInformation (juce::MemoryBlock& destData) override { | |
if (!ctx->state_ext) | |
return; // state is not supported(!?) | |
clap_ostream_t stream{&destData, clap_juce_host_write_to_memory_block}; | |
ctx->state_ext->save(ctx->plugin, &stream); | |
} | |
struct JUCEStateData { | |
const void* data; | |
int sizeInBytes; | |
int current; | |
}; | |
static int64_t clap_juce_host_read_from_state_data(const struct clap_istream *stream, void *buffer, uint64_t size) { | |
auto juce = (JUCEStateData*) stream->ctx; | |
auto remaining = juce->sizeInBytes - juce->current; | |
if (remaining <= 0) | |
return 0; | |
auto actualSize = remaining > (int) size ? (int) size : remaining; | |
memcpy(buffer, (uint8_t*) juce->data + juce->current, (size_t) actualSize); | |
juce->current += actualSize; | |
return actualSize; | |
} | |
void setStateInformation (const void* data, int sizeInBytes) override { | |
if (!ctx->state_ext) | |
return; // state is not supported(!?) | |
JUCEStateData juce{data, sizeInBytes, 0}; | |
clap_istream_t stream{&juce, clap_juce_host_read_from_state_data}; | |
ctx->state_ext->load(ctx->plugin, &stream); | |
} | |
void fillInPluginDescription (PluginDescription& dst) const override { | |
dst.loadFromXml(*desc.createXml()); | |
} | |
JUCE_LEAK_DETECTOR (CLAPAudioPluginInstance) | |
}; | |
// ----------------------------------------------------------------------------------------- | |
// AudioPluginFormat implementation | |
// ----------------------------------------------------------------------------------------- | |
class CLAPAudioPluginFormat : public AudioPluginFormat { | |
public: | |
// CLAPAudioPluginFormat specifics. | |
CLAPAudioPluginFormat (const char* hostName = "CLAPAudioPluginFormat", | |
const char* hostVendor = "atsushieno", | |
const char* hostUrl = "https://github.com/atsushieno/clap-juce-hosting", | |
const char* hostVersion = "1.0") | |
: host_name(hostName), host_vendor(hostVendor), host_url(hostUrl), host_version(hostVersion) { | |
} | |
const char *host_name; | |
const char *host_vendor; | |
const char *host_url; | |
const char *host_version; | |
// JUCE overrides | |
[[nodiscard]] String getName() const override { return "CLAP"; } | |
clap_host_t createClapHost(CLAPFactoryContext* ctx) { | |
return clap_host_t{/*.version = */ ctx->entrypoint->clap_version, | |
/*.host_data = */ ctx, | |
/* .name = */ host_name, | |
/* .vendor = */ host_vendor, | |
/* .url = */ host_url, | |
/* .version = */ host_version, | |
/* .get_extension = */ clap_juce_host_get_extension, | |
/* .request_restart = */ clap_juce_host_request_restart, | |
/* .request_process = */ clap_juce_host_request_process, | |
/* .request_callback = */ clap_juce_host_request_callback | |
}; | |
} | |
bool isInstrument(const char** features) { | |
if (!features) | |
return false; | |
int i = 0; | |
while (true) { | |
if (!features[i]) | |
return false; | |
if (strcmp(features[i], "instrument") == 0) | |
return true; | |
i++; | |
} | |
} | |
static String getCategoryFromFeatures(const char** features) { | |
if (!features) | |
return ""; | |
int i = 0; | |
String ret{}; | |
while (true) { | |
if (!features[i]) | |
return ret; | |
ret += (ret.length() > 0 ? "," : ""); | |
ret += features[i]; | |
i++; | |
} | |
} | |
typedef struct scan_on_main_thread_info { | |
CLAPAudioPluginFormat* format; | |
OwnedArray<PluginDescription>& results; | |
String fileOrIdentifier; | |
} scan_on_main_thread_info_t; | |
static void* staticScanOnMainThread(void* userData) { | |
auto data = (scan_on_main_thread_info_t*) userData; | |
return data->format->scanOnMainThread(data->results, data->fileOrIdentifier); | |
} | |
void* scanOnMainThread(OwnedArray<PluginDescription>& results, String fileOrIdentifier) { | |
File file{fileOrIdentifier}; | |
if (!file.exists()) | |
return nullptr; | |
auto ctx = openContext(fileOrIdentifier); | |
if (!ctx->factory) | |
return nullptr; | |
auto numPlugins = ctx->factory->get_plugin_count(ctx->factory); | |
auto host = createClapHost(ctx.get() ); | |
for (uint i = 0; i < numPlugins; i++) { | |
auto desc = ctx->factory->get_plugin_descriptor(ctx->factory, i); | |
if (desc) { | |
// While CLAP claims it supports faster plugin scanning, we have to instantiate the | |
// plugin to retrieve the numbers of in/out channels to implement this function... | |
auto plugin = ctx->factory->create_plugin(ctx->factory, &host, desc->id); | |
plugin->init(plugin); | |
if (plugin) { | |
auto audioPorts = (clap_plugin_audio_ports_t *) plugin->get_extension(plugin, | |
CLAP_EXT_AUDIO_PORTS); | |
PluginDescription result{}; | |
result.name = desc->id; | |
result.descriptiveName = desc->name; | |
result.pluginFormatName = "CLAP"; | |
result.category = getCategoryFromFeatures(desc->features); | |
result.manufacturerName = desc->vendor; | |
result.version = desc->version; | |
result.fileOrIdentifier = fileOrIdentifier; | |
result.lastFileModTime = file.getLastModificationTime(); | |
result.lastInfoUpdateTime = file.getLastAccessTime(); | |
result.deprecatedUid = String(desc->id).hash(); | |
result.uniqueId = String(desc->id).hash(); | |
result.isInstrument = isInstrument(desc->features); | |
result.numInputChannels = audioPorts ? (int) audioPorts->count(plugin, true) : 0; | |
result.numOutputChannels = audioPorts ? (int) audioPorts->count(plugin, false) : 0; | |
//result.hasSharedContainer = hasSharedContainer(desc->features); | |
//result.hasARAExtension = hasARAExtension(desc->features); | |
results.add(new PluginDescription(result)); | |
plugin->destroy(plugin); | |
} | |
} | |
} | |
} | |
void findAllTypesForFile (OwnedArray<PluginDescription>& results, | |
const String& fileOrIdentifier) override { | |
if (fileOrIdentifier.length() == 0) | |
return; | |
scan_on_main_thread_info_t info{this, results, fileOrIdentifier}; | |
MessageManager::getInstance()->callFunctionOnMessageThread(staticScanOnMainThread, &info); | |
} | |
bool fileMightContainThisPluginType (const String& fileOrIdentifier) override { | |
return fileOrIdentifier.endsWith(".clap"); | |
} | |
String getNameOfPluginFromIdentifier (const String& fileOrIdentifier) override { | |
return File{fileOrIdentifier}.getFileNameWithoutExtension(); // TODO: implement | |
} | |
bool pluginNeedsRescanning (const PluginDescription& desc) override { | |
return File{desc.fileOrIdentifier}.getLastAccessTime() > desc.lastInfoUpdateTime; | |
} | |
bool doesPluginStillExist (const PluginDescription& description) override { | |
return File{description.fileOrIdentifier}.exists(); | |
} | |
[[nodiscard]] bool canScanForPlugins() const override { | |
return true; | |
} | |
[[nodiscard]] bool isTrivialToScan() const override { | |
// > Should return true if this format is both safe and quick to scan - i.e. | |
// > if a file can be scanned within a few milliseconds on a background thread, without actually needing to load an executable. | |
return false; | |
} | |
StringArray searchPathsForPlugins (const FileSearchPath& directoriesToSearch, | |
bool recursive, | |
bool /*allowPluginsWhichRequireAsynchronousInstantiation*/) override { | |
StringArray results{}; | |
for (int i = 0, n = directoriesToSearch.getNumPaths(); i < n; i++) { | |
for (auto& file : directoriesToSearch[i].findChildFiles(File::TypesOfFileToFind::findFiles, recursive)) | |
if (fileMightContainThisPluginType(file.getFullPathName())) | |
results.add(file.getFullPathName()); | |
} | |
return results; | |
} | |
FileSearchPath getDefaultLocationsToSearch() override { | |
auto clapPath = getenv("CLAP_PATH"); | |
#if JUCE_WINDOWS | |
return FileSearchPath { clapPath ? clapPath : | |
String::formatted(R"(%s\CLAP\;%s\Common\CLAP)", getenv("CommonFilesFolder"), getenv("LOCALAPPDATA"))}; | |
#elif JUCE_MAC | |
return FileSearchPath {clapPath ? String{clapPath}.replaceCharacter(':', ';') : | |
String{"/Library/Audio/Plug-Ins/CLAP/;"} + String{getenv("HOME")} + "/Audio/Plug-Ins/CLAP/"}; | |
// FIXME: the specification looks wrong (paths order), needs verification. | |
#else | |
return FileSearchPath {clapPath ? String{clapPath}.replaceCharacter(':', ';') : | |
String{getenv("HOME")} + "/.clap;/usr/lib/clap"}; | |
#endif | |
} | |
[[nodiscard]] bool requiresUnblockedMessageThreadDuringCreation (const PluginDescription&) const override { | |
return false; | |
} | |
std::unique_ptr<CLAPFactoryContext> openContext(String fileOrIdentifier) { | |
auto ctx = std::make_unique<CLAPFactoryContext>(); | |
ctx->dl = dlopen(fileOrIdentifier.toRawUTF8(), RTLD_LOCAL | RTLD_LAZY); | |
if (ctx->dl) { | |
ctx->entrypoint = (const clap_plugin_entry_t *) dlsym(ctx->dl, "clap_entry"); | |
if (ctx->entrypoint && ctx->entrypoint->init(fileOrIdentifier.toRawUTF8())) { | |
ctx->factory = (const clap_plugin_factory_t *) ctx->entrypoint->get_factory(CLAP_PLUGIN_FACTORY_ID); | |
ctx->host = createClapHost(ctx.get()); | |
} | |
} | |
return ctx; | |
} | |
void createPluginInstance (const PluginDescription& desc, double initialSampleRate, | |
int initialBufferSize, PluginCreationCallback callback) override { | |
instancing_context_t info{this, desc, initialSampleRate, initialBufferSize, callback}; | |
MessageManager::getInstance()->callFunctionOnMessageThread(staticCreatePluginInstance, &info); | |
} | |
private: | |
typedef struct instancing_context { | |
CLAPAudioPluginFormat *format; | |
const PluginDescription &desc; | |
double initialSampleRate; | |
int initialBufferSize; | |
PluginCreationCallback callback; | |
} instancing_context_t; | |
static void* staticCreatePluginInstance(void* userData) { | |
auto data = (instancing_context_t*) userData; | |
data->format->doCreatePluginInstance(data->desc, data->initialSampleRate, data->initialBufferSize, data->callback); | |
return nullptr; | |
} | |
void doCreatePluginInstance (const PluginDescription& desc, double initialSampleRate, | |
int initialBufferSize, PluginCreationCallback callback) { | |
auto ctx = openContext(desc.fileOrIdentifier); | |
if (ctx->factory) { | |
ctx->plugin = ctx->factory->create_plugin(ctx->factory, &ctx->host, desc.name.toRawUTF8()); | |
ctx->plugin->init(ctx->plugin); | |
auto instance = std::make_unique<CLAPAudioPluginInstance>( | |
std::move(ctx), desc, initialSampleRate, initialBufferSize); | |
if (instance) { | |
callback(std::move(instance), ""); | |
} | |
else | |
callback(nullptr, String::formatted("Failed to instantiate %s (%d)", desc.name.toRawUTF8(), | |
desc.uniqueId)); | |
} | |
} | |
JUCE_LEAK_DETECTOR (CLAPAudioPluginFormat) | |
}; | |
} // namespaces | |
#endif //JUCE_CLAPAUDIOPLUGINFORMAT_H |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
LICENSE: MIT