Merge branch 'ios'
This commit is contained in:
commit
3804a17432
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.DS_Store
|
||||
out/
|
@ -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"
|
||||
|
38
templates/ios-make/Makefile
Normal file
38
templates/ios-make/Makefile
Normal 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 $^ $@
|
23
templates/ios-make/project.yml
Normal file
23
templates/ios-make/project.yml
Normal 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
|
||||
{{?}}
|
5
templates/ios-make/tibia-index.js
Normal file
5
templates/ios-make/tibia-index.js
Normal 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);
|
||||
};
|
8
templates/ios-make/vars.mk
Normal file
8
templates/ios-make/vars.mk
Normal 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 : "")}}
|
8
templates/ios/src/app-Bridging-Header.h
Normal file
8
templates/ios/src/app-Bridging-Header.h
Normal 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);
|
87
templates/ios/src/app.swift
Normal file
87
templates/ios/src/app.swift
Normal 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
66
templates/ios/src/data.h
Normal 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
|
178
templates/ios/src/index.html
Normal file
178
templates/ios/src/index.html
Normal 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
350
templates/ios/src/native.mm
Normal 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
|
||||
}
|
10
templates/ios/tibia-index.js
Normal file
10
templates/ios/tibia-index.js
Normal 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
8
test/ios-make.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"ios_make": {
|
||||
"headerSearchPaths": [],
|
||||
"sourcesExtra": [],
|
||||
"productBundleIdentifier": "com.example.tibia_test",
|
||||
"deploymentTarget": 16.6
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user