Merge branch 'ios'

This commit is contained in:
Stefano D'Angelo 2024-02-05 13:57:33 +01:00
commit 3804a17432
15 changed files with 806 additions and 14 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.DS_Store
out/

View File

@ -1 +1,4 @@
TBA
iOS app

View File

@ -16,23 +16,21 @@
#if PARAMETERS_N > 0
# include <algorithm>
#endif
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
# define MINIAUDIO_IMPLEMENTATION
# define MA_ENABLE_ONLY_SPECIFIC_BACKENDS
# define MA_ENABLE_AAUDIO
# include <miniaudio.h>
# define BLOCK_SIZE 32
#endif
#if NUM_MIDI_INPUTS > 0
# include <vector>
# include <amidi/AMidi.h>
#endif
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
static ma_device device;
#endif
static plugin instance;
static void * mem;
#if (NUM_NON_OPT_CHANNELS_IN > NUM_CHANNELS_IN) || (NUM_NON_OPT_CHANNELS_OUT > NUM_CHANNELS_OUT)
@ -129,6 +127,9 @@ static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput,
for (ma_uint32 j = 0; j < n; j++)
for (size_t k = 0; k < NUM_CHANNELS_OUT; k++, iy++)
out_buf[iy] = y_buf[BLOCK_SIZE * k + j];
#elif NUM_CHANNELS_IN == 0
for (ma_uint32 j = 0; j < n; j++)
out_buf[j] = 0;
#endif
i += n;
@ -149,6 +150,10 @@ JNI_FUNC(nativeAudioStart)(JNIEnv* env, jobject thiz) {
# else
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_duplex);
# endif
#else
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback);
#endif
deviceConfig.periodSizeInFrames = BLOCK_SIZE;
deviceConfig.periods = 1;
deviceConfig.performanceProfile = ma_performance_profile_low_latency;
@ -163,12 +168,15 @@ JNI_FUNC(nativeAudioStart)(JNIEnv* env, jobject thiz) {
deviceConfig.capture.shareMode = ma_share_mode_shared;
deviceConfig.playback.pDeviceID = NULL;
deviceConfig.playback.format = ma_format_f32;
deviceConfig.playback.channels = NUM_CHANNELS_OUT;
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
deviceConfig.playback.channels = NUM_CHANNELS_OUT;
#else
deviceConfig.playback.channels = 1; // Fake & muted
#endif
deviceConfig.playback.shareMode = ma_share_mode_shared;
if (ma_device_init(NULL, &deviceConfig, &device) != MA_SUCCESS)
return false;
#endif
plugin_init(&instance);
@ -186,9 +194,7 @@ JNI_FUNC(nativeAudioStart)(JNIEnv* env, jobject thiz) {
mem = malloc(req);
if (mem == NULL) {
plugin_fini(&instance);
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
ma_device_uninit(&device);
#endif
return false;
}
plugin_mem_set(&instance, mem);
@ -250,14 +256,12 @@ JNI_FUNC(nativeAudioStart)(JNIEnv* env, jobject thiz) {
y = NULL;
#endif
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
if (ma_device_start(&device) != MA_SUCCESS) {
if (mem != NULL)
free(mem);
ma_device_uninit(&device);
return false;
}
#endif
return true;
}
@ -268,13 +272,11 @@ JNI_FUNC(nativeAudioStop)(JNIEnv* env, jobject thiz) {
(void)env;
(void)thiz;
ma_device_stop(&device);
ma_device_uninit(&device);
if (mem != NULL)
free(mem);
plugin_fini(&instance);
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
ma_device_stop(&device);
ma_device_uninit(&device);
#endif
}
extern "C"

View File

@ -0,0 +1,38 @@
include vars.mk
COMMON_DIR := $(or $(COMMON_DIR),.)
DATA_DIR := $(or $(DATA_DIR),.)
PLUGIN_DIR := $(or $(PLUGIN_DIR),src)
SOURCES := \
${DATA_DIR}/src/data.h \
${DATA_DIR}/src/index.html \
${COMMON_DIR}/src/app.swift \
${COMMON_DIR}/src/native.mm \
${COMMON_DIR}/src/app-Bridging-Header.h \
${PLUGIN_DIR}/plugin.h \
${C_SRCS_EXTRA} \
${CXX_SRCS_EXTRA}
SOURCES_OUT = $(addprefix build/gen/src/, $(notdir $(SOURCES)))
all: build/gen/${BUNDLE_NAME}.xcodeproj
build/gen/${BUNDLE_NAME}.xcodeproj: ${SOURCES_OUT}
xcodegen generate --spec project.yml
mv ${BUNDLE_NAME}.xcodeproj build/gen/${BUNDLE_NAME}.xcodeproj
mv Info.plist build/gen/Info.plist
build/gen/src:
mkdir -p $@
clean:
rm -fr build
.PHONY: all clean
.SECONDEXPANSION:
PERCENT := %
$(SOURCES_OUT): build/gen/src/%: $$(filter $$(PERCENT)/%,$$(SOURCES)) | build/gen/src
cp $^ $@

View File

@ -0,0 +1,23 @@
name: {{=it.product.bundleName}}
targets:
{{=it.product.bundleName}}:
platform: [iOS]
deploymentTarget:
iOS: {{=it.ios_make.deploymentTarget}}
type: application
sources:
- path: src
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: {{=it.ios_make.productBundleIdentifier}}
SWIFT_OBJC_BRIDGING_HEADER: src/app-Bridging-Header.h
{{?it.ios_make.headerSearchPaths}}
HEADER_SEARCH_PATHS: {{~it.ios_make.headerSearchPaths :p}}
- {{=p}}{{~}}
{{?}}
info:
path: Info.plist
{{?it.product.buses.filter(x => x.type == "audio" && x.direction == "input").length > 0}}
properties:
NSMicrophoneUsageDescription: Need audio input for processing sound
{{?}}

View File

@ -0,0 +1,5 @@
module.exports = function (data, api) {
api.copyFile(`Makefile`, `Makefile`);
api.generateFileFromTemplateFile(`vars.mk`, `vars.mk`, data);
api.generateFileFromTemplateFile(`project.yml`, `project.yml`, data);
};

View File

@ -0,0 +1,8 @@
BUNDLE_NAME := {{=it.product.bundleName}}
C_SRCS_EXTRA := {{=it.make && it.make.cSrcs ? it.make.cSrcs : ""}} {{=it.ios_make && it.ios_make.cSrcs ? it.ios_make.cSrcs : ""}}
CXX_SRCS_EXTRA := {{=it.make && it.make.cxxSrcs ? it.make.cxxSrcs : ""}} {{=it.ios_make && it.ios_make.cxxSrcs ? it.ios_make.cxxSrcs : ""}}
COMMON_DIR := {{=it.ios_make && it.ios_make.commonDir ? it.ios_make.commonDir : (it.make && it.make.commonDir ? it.make.commonDir : "")}}
DATA_DIR := {{=it.ios_make && it.ios_make.dataDir ? it.ios_make.dataDir : (it.make && it.make.dataDir ? it.make.dataDir : "")}}
PLUGIN_DIR := {{=it.ios_make && it.ios_make.pluginDir ? it.ios_make.pluginDir : (it.make && it.make.pluginDir ? it.make.pluginDir : "")}}

View File

@ -0,0 +1,8 @@
/*
* Copyright (C) 2023, 2024 Orastron Srl unipersonale
*/
char audioStart();
void audioStop();
void setParameter(int i, float v);
float getParameter(int i);

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2023, 2024 Orastron Srl unipersonale
*/
import SwiftUI
import WebKit
import AVFoundation
struct WebView: UIViewRepresentable {
class Coordinator: NSObject, WKScriptMessageHandlerWithReply {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage, replyHandler: @escaping (Any?, String?) -> Void) {
guard let body = message.body as? [String : Any] else { return }
guard let name = body["name"] as? String else { return }
switch (name) {
case "needAudioPermission":
replyHandler(AVCaptureDevice.authorizationStatus(for: .audio) != .authorized, nil)
break;
case "requestAudioPermission":
AVAudioSession.sharedInstance().requestRecordPermission { granted in }
replyHandler(nil, nil)
break;
case "audioStart":
replyHandler(audioStart() != 0, nil)
break;
case "audioStop":
audioStop()
replyHandler(nil, nil)
break;
case "setParameter":
guard let index = body["index"] as? Int32 else { return }
guard let value = body["value"] as? Double else { return }
setParameter(index, Float(value))
replyHandler(nil, nil)
break;
case "getParameter":
guard let index = body["index"] as? Int32 else { return }
let v = getParameter(index)
replyHandler(v, nil)
break;
default:
break;
}
}
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
let url: URL
func makeUIView(context: Context) -> WKWebView {
let configuration = WKWebViewConfiguration()
configuration.userContentController.addScriptMessageHandler(Coordinator(), contentWorld: .page, name: "listener")
let webView = WKWebView(frame: .zero, configuration: configuration)
webView.isInspectable = true
return webView
}
func updateUIView(_ webView: WKWebView, context: Context) {
let request = URLRequest(url: url)
webView.load(request)
}
}
struct ContentView: View {
var body: some View {
let url = Bundle.main.url(forResource: "index", withExtension: "html")
WebView(url: url!)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
@main
struct templateApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}

66
templates/ios/src/data.h Normal file
View File

@ -0,0 +1,66 @@
#include <stddef.h>
#include <stdint.h>
#define NUM_AUDIO_BUSES_IN {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "input").length}}
#define NUM_AUDIO_BUSES_OUT {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "output").length}}
#define AUDIO_BUS_IN {{=it.product.buses.findIndex(x => x.type == "audio" && x.direction == "input" && !x.cv && !x.sidechain)}}
#define AUDIO_BUS_OUT {{=it.product.buses.findIndex(x => x.type == "audio" && x.direction == "output" && !x.cv && !x.sidechain)}}
#define NUM_CHANNELS_IN {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "input" && !x.cv && !x.sidechain) ? (it.product.buses.filter(x => x.type == "audio" && x.direction == "input" && !x.cv && !x.sidechain)[0].channels == "mono" ? 1 : 2) : 0}}
#define NUM_CHANNELS_OUT {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "output" && !x.cv && !x.sidechain) ? (it.product.buses.filter(x => x.type == "audio" && x.direction == "output" && !x.cv && !x.sidechain)[0].channels == "mono" ? 1 : 2) : 0}}
#define NUM_NON_OPT_CHANNELS_IN {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "input" && !x.optional).reduce((a, x) => a + (x.channels == "mono" ? 1 : 2), 0)}}
#define NUM_NON_OPT_CHANNELS_OUT {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "output" && !x.optional).reduce((a, x) => a + (x.channels == "mono" ? 1 : 2), 0)}}
#define NUM_ALL_CHANNELS_IN {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "input").reduce((a, x) => a + (x.channels == "mono" ? 1 : 2), 0)}}
#define NUM_ALL_CHANNELS_OUT {{=it.product.buses.filter(x => x.type == "audio" && x.direction == "output").reduce((a, x) => a + (x.channels == "mono" ? 1 : 2), 0)}}
#define NUM_MIDI_INPUTS {{=it.product.buses.filter(x => x.type == "midi" && x.direction == "input").length}}
#define MIDI_BUS_IN {{=it.product.buses.findIndex(x => x.type == "midi" && x.direction == "input")}}
#if (AUDIO_BUS_IN >= 0) || (AUDIO_BUS_OUT >= 0)
static struct {
size_t index;
char out;
char optional;
char channels;
} audio_bus_data[NUM_AUDIO_BUSES_IN + NUM_AUDIO_BUSES_OUT] = {
{{~it.product.buses :b:i}}
{{?b.type == "audio"}}
{
/* .index = */ {{=i}},
/* .out = */ {{=b.direction == "output" ? 1 : 0}},
/* .optional = */ {{=b.optional ? 1 : 0}},
/* .channels = */ {{=b.channels == "mono" ? 1 : 2}}
},
{{?}}
{{~}}
};
#endif
#define PARAMETERS_N {{=it.product.parameters.length}}
#if PARAMETERS_N > 0
# define PARAM_BYPASS 1
# define PARAM_TOGGLED (1<<1)
# define PARAM_INTEGER (1<<2)
static struct {
char out;
float def;
float min;
float max;
uint32_t flags;
} param_data[PARAMETERS_N] = {
{{~it.product.parameters :p}}
{
/* .out = */ {{=p.direction == "output" ? 1 : 0}},
/* .def = */ {{=p.defaultValue.toExponential()}},
/* .min = */ {{=p.minimum.toExponential()}}f,
/* .max = */ {{=p.maximum.toExponential()}}f,
/* .flags = */ {{?p.isBypass}}PARAM_BYPASS{{??p.isLatency}}PARAM_INTEGER{{??}}0{{?p.toggled}} | PARAM_TOGGLED{{?}}{{?p.integer}} | PARAM_INTEGER{{?}}{{?}}
},
{{~}}
};
#endif

View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
<title>{{=it.product.name}}</title>
<script type="text/javascript">
function request(data) {
return window.webkit.messageHandlers.listener.postMessage(data);
}
function needAudioPermission() {
return request({ name: "needAudioPermission" });
}
function requestAudioPermission() {
return request({ name: "requestAudioPermission" });
}
function audioStart() {
return request({ name: "audioStart" });
}
function audioStop() {
return request({ name: "audioStop" });
}
function setParameter(index, value) {
return request({ name: "setParameter", index: index, value: value });
}
function getParameter(index) {
return request({ name: "getParameter", index: index });
}
var data = {
buses: {{=JSON.stringify(it.product.buses, null, 2)}},
parameters: {{=JSON.stringify(it.product.parameters, null, 2)}}
};
function map(index, value) {
var p = data.parameters[index];
return p.map == "logarithmic" ? p.minimum * Math.exp((2.0 * Math.log(Math.sqrt(p.maximum * p.minimum) / Math.abs(p.minimum))) * value) : p.minimum + (p.maximum - p.minimum) * value;
}
function unmap(index, value) {
var p = data.parameters[index];
return p.map == "logarithmic" ? Math.log(value / p.minimum) / (2.0 * Math.log(Math.sqrt(p.maximum * p.minimum) / Math.abs(p.minimum))) : (value - p.minimum) / (p.maximum - p.minimum);
}
var hasAudioPermission = true;
var audioStarted = false;
var topButtonElem;
var outParamInterval;
window.onload = async function () {
topButtonElem = document.getElementById("topButton");
var paramsElem = document.getElementById("params");
for (var i = 0; i < data.buses.length; i++)
if (!data.buses[i].output) {
hasAudioPermission = !await needAudioPermission();
break;
}
topButtonElem.value = hasAudioPermission ? "START" : "INIT";
topButtonElem.addEventListener("click", async function () {
if (hasAudioPermission) {
if (audioStarted) {
clearInterval(outParamInterval);
await audioStop();
paramsElem.innerHTML = "";
topButtonElem.value = "START";
audioStarted = false;
} else {
if (await audioStart()) {
for (var i = 0; i < data.parameters.length; i++) {
var div = document.createElement("div");
var label = document.createElement("label");
label.setAttribute("for", "p" + i);
label.innerText = data.parameters[i].name;
var range = document.createElement("input");
range.classList.add("range");
range.setAttribute("type", "range");
range.setAttribute("id", "p" + i);
range.setAttribute("name", "p" + i);
if (data.parameters[i].isBypass || data.parameters[i].toggled) {
range.setAttribute("min", 0);
range.setAttribute("max", 1);
range.setAttribute("step", 1);
} else {
range.setAttribute("min", 0);
range.setAttribute("max", 1);
range.setAttribute("step", data.parameters[i].integer ? 1 / (data.parameters[i].maximum - data.parameters[i].minimum) : "any");
}
range.value = unmap(i, data.parameters[i].defaultValue);
if (data.parameters[i].direction == "output")
range.setAttribute("readonly", "true");
else {
let index = i;
range.addEventListener("input",
async function (ev) {
await setParameter(index, map(index, parseFloat(ev.target.value)));
});
}
div.appendChild(label);
div.appendChild(document.createElement("br"));
div.appendChild(range);
paramsElem.appendChild(div);
}
outParamInterval = setInterval(
async function () {
for (var i = 0; i < data.parameters.length; i++)
if (data.parameters[i].direction == "output") {
document.getElementById("p" + i).value = unmap(i, await getParameter(i));
}
}, 50);
topButtonElem.value = "STOP";
audioStarted = true;
} else
alert("Could not start audio");
}
} else {
await requestAudioPermission();
var interval = setInterval(
async function () {
if (!await needAudioPermission()) {
gotAudioPermission();
clearInterval(interval);
}
}, 50);
}
});
};
function gotAudioPermission() {
hasAudioPermission = true;
topButtonElem.value = "START";
}
</script>
<style>
* {
margin: 0;
padding: 0;
user-select: none;
}
body {
margin: 1em;
}
#topButton {
width: 100%;
border: 0;
background-color: #04aa6d;
color: white;
padding: 0.5em;
text-align: center;
margin-bottom: 1em;
padding: 1em;
}
.range {
width: 90%;
}
</style>
</head>
<body>
<input id="topButton" type="button">
<div id="params"></div>
</body>
</html>

350
templates/ios/src/native.mm Normal file
View File

@ -0,0 +1,350 @@
/*
* Copyright (C) 2023, 2024 Orastron Srl unipersonale
*/
#include "data.h"
#include "plugin.h"
#if PARAMETERS_N > 0
#include <algorithm>
#endif
#if PARAMETERS_N + NUM_MIDI_INPUTS > 0
#include <mutex>
#endif
#include <vector>
#define MINIAUDIO_IMPLEMENTATION
#define MA_NO_RUNTIME_LINKING
#include "miniaudio.h"
#define BLOCK_SIZE 32
#define NUM_BUFS (NUM_CHANNELS_IN > NUM_CHANNELS_OUT ? NUM_CHANNELS_IN : NUM_CHANNELS_OUT)
static ma_device device;
static plugin instance;
static void *mem;
#if (NUM_NON_OPT_CHANNELS_IN > NUM_CHANNELS_IN) || (NUM_NON_OPT_CHANNELS_OUT > NUM_CHANNELS_OUT)
float zero[BLOCK_SIZE];
#endif
#if NUM_CHANNELS_IN > 0
float x_buf[NUM_CHANNELS_IN * BLOCK_SIZE];
#endif
#if NUM_ALL_CHANNELS_IN > 0
const float *x[NUM_ALL_CHANNELS_IN];
#else
const float **x;
#endif
#if NUM_CHANNELS_OUT > 0
float y_buf[NUM_CHANNELS_OUT * BLOCK_SIZE];
#endif
#if NUM_ALL_CHANNELS_OUT > 0
float *y[NUM_ALL_CHANNELS_IN];
#else
float **y;
#endif
#if PARAMETERS_N > 0
std::mutex mutex;
float param_values[PARAMETERS_N];
float param_values_prev[PARAMETERS_N];
#endif
#if NUM_MIDI_INPUTS > 0
CFStringRef midiClientName = NULL;
MIDIClientRef midiClient = NULL;
CFStringRef midiInputName = NULL;
MIDIPortRef midiPort = NULL;
#define MIDIBUFFERLEN 1023
uint8_t midiBuffer[MIDIBUFFERLEN];
int midiBuffer_i = 0;
#endif
static void data_callback(ma_device* pDevice, void* pOutput, const void* pInput, ma_uint32 frameCount) {
(void)pDevice;
#if PARAMETERS_N + NUM_MIDI_INPUTS > 0
if (mutex.try_lock()) {
# if PARAMETERS_N > 0
for (size_t i = 0; i < PARAMETERS_N; i++) {
if (param_data[i].out)
param_values_prev[i] = param_values[i] = plugin_get_parameter(&instance, i);
else if (param_values_prev[i] != param_values[i]) {
plugin_set_parameter(&instance, i, param_values[i]);
param_values_prev[i] = param_values[i];
}
}
# endif
# if NUM_MIDI_INPUTS > 0
if (midiBuffer_i > 0) {
for (int i = 0; i < midiBuffer_i; i+=3) {
plugin_midi_msg_in(&instance, MIDI_BUS_IN, &(midiBuffer[i]));
}
}
midiBuffer_i = 0;
# endif
mutex.unlock();
}
#endif
#if NUM_CHANNELS_IN == 0
(void)pInput;
#else
const float * in_buf = reinterpret_cast<const float *>(pInput);
#endif
float * out_buf = reinterpret_cast<float *>(pOutput);
ma_uint32 i = 0;
#if NUM_CHANNELS_IN > 0
size_t ix = 0;
#endif
#if NUM_CHANNELS_OUT > 0
size_t iy = 0;
#endif
while (i < frameCount) {
ma_uint32 n = std::min(frameCount - i, static_cast<ma_uint32>(BLOCK_SIZE));
#if NUM_CHANNELS_IN > 0
for (ma_uint32 j = 0; j < n; j++)
for (size_t k = 0; k < NUM_CHANNELS_IN; k++, ix++)
x_buf[BLOCK_SIZE * k + j] = in_buf[ix];
#endif
#if NUM_NON_OPT_CHANNELS_IN > NUM_CHANNELS_IN
memset(zero, 0, BLOCK_SIZE * sizeof(float));
#endif
plugin_process(&instance, x, y, n);
#if NUM_CHANNELS_OUT > 0
for (ma_uint32 j = 0; j < n; j++)
for (size_t k = 0; k < NUM_CHANNELS_OUT; k++, iy++)
out_buf[iy] = y_buf[BLOCK_SIZE * k + j];
#elif NUM_CHANNELS_IN == 0
for (ma_uint32 j = 0; j < n; j++)
out_buf[j] = 0.f;
#endif
i += n;
}
}
#if (NUM_MIDI_INPUTS > 0)
void (^midiNotifyBlock)(const MIDINotification *message) = ^(const MIDINotification *message) {
if (message->messageID != kMIDIMsgObjectAdded)
return;
const MIDIObjectAddRemoveNotification *n = reinterpret_cast<const MIDIObjectAddRemoveNotification *>(message);
MIDIEndpointRef endPoint = n->child;
MIDIPortConnectSource(midiPort, endPoint, NULL);
};
void (^midiReceiveBlock)(const MIDIEventList *evtlist, void *srcConnRefCon) = ^(const MIDIEventList *evtlist, void *srcConnRefCon) {
const MIDIEventPacket *p = evtlist->packet;
for (UInt32 i = 0; i < evtlist->numPackets; i++) {
for (UInt32 j = 0; j < p->wordCount; j++) {
const UInt32 w = p->words[j];
const uint8_t* t = (uint8_t*) &(w);
if ((t[3] & 0xf0) != 32)
continue; // We only support MIDI 1.0
if ((t[2] & 0xF0) == 0xF0)
continue;
mutex.lock();
if (midiBuffer_i < MIDIBUFFERLEN - 3) {
midiBuffer[midiBuffer_i ] = t[2];
midiBuffer[midiBuffer_i + 1] = t[1];
midiBuffer[midiBuffer_i + 2] = t[0];
midiBuffer_i += 3;
}
mutex.unlock();
}
p = MIDIEventPacketNext(p);
}
};
#endif
extern "C"
char audioStart() {
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
# if NUM_CHANNELS_IN == 0
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback);
# elif NUM_CHANNELS_OUT == 0
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_capture);
# else
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_duplex);
# endif
#else
ma_device_config deviceConfig = ma_device_config_init(ma_device_type_playback);
#endif
deviceConfig.periodSizeInFrames = BLOCK_SIZE;
deviceConfig.periods = 1;
deviceConfig.performanceProfile = ma_performance_profile_low_latency;
deviceConfig.noPreSilencedOutputBuffer = 1;
deviceConfig.noClip = 0;
deviceConfig.noDisableDenormals = 0;
deviceConfig.noFixedSizedCallback = 1;
deviceConfig.dataCallback = data_callback;
deviceConfig.capture.pDeviceID = NULL;
deviceConfig.capture.format = ma_format_f32;
deviceConfig.capture.channels = NUM_CHANNELS_IN;
deviceConfig.capture.shareMode = ma_share_mode_shared;
deviceConfig.playback.pDeviceID = NULL;
deviceConfig.playback.format = ma_format_f32;
#if NUM_CHANNELS_IN + NUM_CHANNELS_OUT > 0
deviceConfig.playback.channels = NUM_CHANNELS_OUT;
#else
deviceConfig.playback.channels = 1; // Fake & muted
#endif
deviceConfig.playback.shareMode = ma_share_mode_shared;
if (ma_device_init(NULL, &deviceConfig, &device) != MA_SUCCESS)
return false;
#if (NUM_MIDI_INPUTS > 0)
if (midiClientName == NULL) {
midiClientName = CFSTR("template");
if (midiClientName == NULL)
return false; // Check unint
}
if (midiClient == (MIDIClientRef)NULL) {
if (MIDIClientCreateWithBlock(midiClientName, &midiClient, midiNotifyBlock) != 0)
return false;
}
if (midiInputName == NULL) {
midiInputName = CFSTR("Input");
if (midiInputName == NULL)
return false;
}
if (midiPort == (MIDIPortRef)NULL) {
if (MIDIInputPortCreateWithProtocol(midiClient, midiInputName, kMIDIProtocol_1_0, &midiPort, midiReceiveBlock) != 0)
return false;
ItemCount n = MIDIGetNumberOfSources();
for (ItemCount i = 0; i < n; i++) {
MIDIEndpointRef endPoint = MIDIGetSource(i);
MIDIPortConnectSource(midiPort, endPoint, NULL);
}
}
#endif
plugin_init(&instance);
#if PARAMETERS_N > 0
for (size_t i = 0; i < PARAMETERS_N; i++) {
if (!param_data[i].out)
plugin_set_parameter(&instance, i, param_data[i].def);
param_values_prev[i] = param_values[i] = param_data[i].def;
}
#endif
plugin_set_sample_rate(&instance, (float)device.sampleRate);
size_t req = plugin_mem_req(&instance);
if (req != 0) {
mem = malloc(req);
if (mem == NULL) {
plugin_fini(&instance);
ma_device_uninit(&device);
return false;
}
plugin_mem_set(&instance, mem);
} else
mem = NULL;
plugin_reset(&instance);
#if NUM_ALL_CHANNELS_IN > 0
# if AUDIO_BUS_IN >= 0
size_t ix = 0;
size_t ixb = 0;
for (size_t j = 0; j < NUM_AUDIO_BUSES_IN + NUM_AUDIO_BUSES_OUT; j++) {
if (audio_bus_data[j].out)
continue;
if (audio_bus_data[j].index == AUDIO_BUS_IN)
for (char k = 0; k < audio_bus_data[j].channels; k++, ix++, ixb++)
x[ix] = x_buf + BLOCK_SIZE * ixb;
# if NUM_NON_OPT_CHANNELS_IN > NUM_CHANNELS_IN
else if (!audio_bus_data[j].optional)
for (char k = 0; k < audio_bus_data[j].channels; k++, ix++)
x[ix] = zero;
# endif
else
for (char k = 0; k < audio_bus_data[j].channels; k++, ix++)
x[ix] = NULL;
}
# else
for (size_t i = 0; i < NUM_ALL_CHANNELS_IN; i++)
x[i] = NULL;
# endif
#else
x = NULL;
#endif
#if NUM_ALL_CHANNELS_OUT > 0
# if AUDIO_BUS_OUT >= 0
size_t iy = 0;
size_t iyb = 0;
for (size_t j = 0; j < NUM_AUDIO_BUSES_IN + NUM_AUDIO_BUSES_OUT; j++) {
if (!audio_bus_data[j].out)
continue;
if (audio_bus_data[j].index == AUDIO_BUS_OUT)
for (char k = 0; k < audio_bus_data[j].channels; k++, iy++, iyb++)
y[iy] = y_buf + BLOCK_SIZE * iyb;
# if NUM_NON_OPT_CHANNELS_OUT > NUM_CHANNELS_OUT
else if (!audio_bus_data[j].optional)
for (char k = 0; k < audio_bus_data[j].channels; k++, iy++)
y[iy] = zero;
# endif
else
for (char k = 0; k < audio_bus_data[j].channels; k++, iy++)
y[iy] = NULL;
}
# else
for (size_t i = 0; i < NUM_ALL_CHANNELS_OUT; i++)
y[i] = NULL;
# endif
#else
y = NULL;
#endif
if (ma_device_start(&device) != MA_SUCCESS) {
if (mem != NULL)
free(mem);
ma_device_uninit(&device);
return false;
}
return true;
}
extern "C"
void audioStop() {
ma_device_stop(&device);
ma_device_uninit(&device);
if (mem != NULL)
free(mem);
plugin_fini(&instance);
// No need to close MIDI connections (e.g. via MIDIClientDispose), the system terminates them when the app terminates.
}
extern "C"
float getParameter(int i) {
#if PARAMETERS_N > 0
mutex.lock();
float v = param_values[i];
mutex.unlock();
return v;
#else
(void)i;
return 0.f;
#endif
}
extern "C"
void setParameter(int i, float v) {
if (param_data[i].flags & (PARAM_BYPASS | PARAM_TOGGLED))
v = v > 0.5f ? 1.f : 0.f;
else if (param_data[i].flags & PARAM_INTEGER)
v = (int32_t)(v + 0.5f);
v = std::min(std::max(v, param_data[i].min), param_data[i].max);
#if PARAMETERS_N > 0
mutex.lock();
param_values[i] = v;
mutex.unlock();
#endif
}

View File

@ -0,0 +1,10 @@
var path = require("path");
var sep = path.sep;
module.exports = function (data, api) {
api.generateFileFromTemplateFile(`src${sep}data.h`, `src${sep}data.h`, data);
api.generateFileFromTemplateFile(`src${sep}index.html`, `src${sep}index.html`, data);
api.copyFile(`src${sep}native.mm`, `src${sep}native.mm`);
api.copyFile(`src${sep}app-Bridging-Header.h`, `src${sep}app-Bridging-Header.h`);
api.copyFile(`src${sep}app.swift`, `src${sep}app.swift`, data);
};

8
test/ios-make.json Normal file
View File

@ -0,0 +1,8 @@
{
"ios_make": {
"headerSearchPaths": [],
"sourcesExtra": [],
"productBundleIdentifier": "com.example.tibia_test",
"deploymentTarget": 16.6
}
}

View File

@ -19,6 +19,10 @@ $dir/../tibia $dir/product.json,$dir/company.json,$dir/android.json,$dir/android
cp $dir/keystore.jks $dir/../out/android
cp $dir/plugin.h $dir/../out/android/src
$dir/../tibia $dir/product.json,$dir/company.json $dir/../templates/ios $dir/../out/ios
$dir/../tibia $dir/product.json,$dir/company.json,$dir/ios-make.json $dir/../templates/ios-make $dir/../out/ios
cp $dir/plugin.h $dir/../out/ios/src
$dir/../tibia $dir/product.json,$dir/company.json,$dir/cmd.json $dir/../templates/cmd $dir/../out/cmd
$dir/../tibia $dir/product.json,$dir/company.json,$dir/cmd.json,$dir/cmd-make.json $dir/../templates/cmd-make $dir/../out/cmd
cp $dir/plugin.h $dir/../out/cmd/src