@geastack/apple 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +54 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.js +3076 -0
- package/generated/AVFoundation.d.ts +112 -0
- package/generated/ActivityKit.d.ts +7 -0
- package/generated/AppKit.d.ts +268 -0
- package/generated/CoreGraphics.d.ts +18 -0
- package/generated/CoreLocation.d.ts +28 -0
- package/generated/CoreMedia.d.ts +8 -0
- package/generated/Dispatch.d.ts +2 -0
- package/generated/Foundation.d.ts +13 -0
- package/generated/MapKit.d.ts +28 -0
- package/generated/Metal.d.ts +91 -0
- package/generated/MetalKit.d.ts +22 -0
- package/generated/Photos.d.ts +5 -0
- package/generated/QuartzCore.d.ts +12 -0
- package/generated/SwiftData.d.ts +11 -0
- package/generated/UIKit.d.ts +187 -0
- package/package.json +123 -0
- package/runtime/AVFoundation.js +136 -0
- package/runtime/ActivityKit.js +18 -0
- package/runtime/AppKit.js +313 -0
- package/runtime/CoreGraphics.js +15 -0
- package/runtime/CoreLocation.js +30 -0
- package/runtime/CoreMedia.js +7 -0
- package/runtime/Dispatch.js +11 -0
- package/runtime/Foundation.js +21 -0
- package/runtime/MapKit.js +22 -0
- package/runtime/Metal.js +126 -0
- package/runtime/MetalKit.js +15 -0
- package/runtime/Photos.js +9 -0
- package/runtime/QuartzCore.js +9 -0
- package/runtime/SwiftData.js +29 -0
- package/runtime/UIKit.js +242 -0
- package/runtime/empty.js +1 -0
- package/targets/ios/Info.plist.in +25 -0
- package/targets/ios/README.md +46 -0
- package/targets/ios/build-ios.sh +395 -0
- package/targets/ios/detect-development-team.mjs +109 -0
- package/targets/ios/generate-resident-entry.mjs +67 -0
- package/targets/ios/generate-resident-registry.mjs +162 -0
- package/targets/ios/generate-xcode-project.mjs +520 -0
- package/targets/ios/main/canvas_view.h +7 -0
- package/targets/ios/main/canvas_view.mm +94 -0
- package/targets/ios/main/font_registry.h +17 -0
- package/targets/ios/main/font_registry.mm +129 -0
- package/targets/ios/main/image_bridge.h +13 -0
- package/targets/ios/main/image_bridge.mm +53 -0
- package/targets/ios/main/ios_apps.c +28 -0
- package/targets/ios/main/ios_camera.mm +446 -0
- package/targets/ios/main/ios_display.mm +344 -0
- package/targets/ios/main/ios_main.mm +811 -0
- package/targets/ios/main/ios_memory.cpp +55 -0
- package/targets/ios/main/ios_renderer.h +20 -0
- package/targets/ios/main/ios_renderer.mm +27 -0
- package/targets/ios/main/ios_root_background.cpp +20 -0
- package/targets/ios/main/ios_root_background.h +11 -0
- package/targets/ios/main/ios_sensors.mm +244 -0
- package/targets/ios/main/ios_timers.mm +53 -0
- package/targets/ios/main/renderer/ios_renderer_internal.h +65 -0
- package/targets/ios/main/renderer/ios_renderer_support.mm +111 -0
- package/targets/ios/main/renderer/native_button.mm +179 -0
- package/targets/ios/main/renderer/native_label.mm +161 -0
- package/targets/ios/main/renderer/native_scroll_container.mm +279 -0
- package/targets/ios/main/renderer/view_reconciler.mm +274 -0
- package/targets/macos/Info.plist.in +26 -0
- package/targets/macos/README.md +131 -0
- package/targets/macos/build-child-lifecycle.sh +119 -0
- package/targets/macos/build-macos.sh +2156 -0
- package/targets/macos/check-module-graph-freshness.mjs +82 -0
- package/targets/macos/compiler-input-fingerprint.mjs +32 -0
- package/targets/macos/finalize-apple-native-output.mjs +77 -0
- package/targets/macos/generate-resident-entry.mjs +80 -0
- package/targets/macos/generate-resident-registry.mjs +141 -0
- package/targets/macos/heavy-build-lock.sh +176 -0
- package/targets/macos/main/canvas_view.h +9 -0
- package/targets/macos/main/canvas_view.mm +176 -0
- package/targets/macos/main/color_convert.h +18 -0
- package/targets/macos/main/color_convert.mm +39 -0
- package/targets/macos/main/font_registry.h +35 -0
- package/targets/macos/main/font_registry.mm +350 -0
- package/targets/macos/main/image_bridge.h +16 -0
- package/targets/macos/main/image_bridge.mm +52 -0
- package/targets/macos/main/macos_app_platform.mm +95 -0
- package/targets/macos/main/macos_apps.c +31 -0
- package/targets/macos/main/macos_device_control.mm +56 -0
- package/targets/macos/main/macos_display.mm +484 -0
- package/targets/macos/main/macos_host_device_control.mm +95 -0
- package/targets/macos/main/macos_main.mm +839 -0
- package/targets/macos/main/macos_memory.cpp +31 -0
- package/targets/macos/main/macos_native_shell.h +31 -0
- package/targets/macos/main/macos_native_shell.mm +396 -0
- package/targets/macos/main/macos_network.mm +253 -0
- package/targets/macos/main/macos_renderer.h +39 -0
- package/targets/macos/main/macos_renderer.mm +1969 -0
- package/targets/macos/main/macos_sensors.cpp +148 -0
- package/targets/macos/main/macos_smoke_app.cpp +65 -0
- package/targets/macos/main/macos_timers.mm +52 -0
- package/targets/macos/main/press_bridge.h +21 -0
- package/targets/macos/main/press_bridge.mm +72 -0
- package/targets/macos/main/thermalright_hid_display.h +14 -0
- package/targets/macos/main/thermalright_hid_display.mm +995 -0
- package/targets/macos/native-artifact-provenance.mjs +421 -0
- package/targets/macos/order-compile-edges.mjs +64 -0
- package/targets/macos/preflight-hard-muted-artifact.mjs +93 -0
- package/targets/macos/prune-generated-modules.mjs +147 -0
- package/targets/macos/sync-resources.mjs +153 -0
- package/targets/shared/analyze-audio-capture.mjs +131 -0
- package/targets/shared/apple_audio.mm +1194 -0
- package/targets/shared/apple_audio_selftest.mm +602 -0
- package/targets/shared/apple_audio_testing.h +34 -0
- package/targets/shared/build-audio-selftest.sh +50 -0
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// Real Apple audio backend for gea::platform::audio — shared by the macOS and
|
|
4
|
+
// iOS targets (AVFoundation is on both; the only platform-conditional code is
|
|
5
|
+
// the iOS AVAudioSession activation).
|
|
6
|
+
//
|
|
7
|
+
// Shape
|
|
8
|
+
// -----
|
|
9
|
+
// AVAudioEngine
|
|
10
|
+
// ├─ AVAudioSourceNode ── the synth: every OscillatorNode the app creates
|
|
11
|
+
// │ is rendered here, sample-accurately scheduled and
|
|
12
|
+
// │ mixed against every other live note.
|
|
13
|
+
// ├─ AVAudioPlayerNode ── AudioSystem::playFile
|
|
14
|
+
// └─ AVAudioPlayerNode ── AudioSystem::playPcm
|
|
15
|
+
// └─ mainMixerNode ─ outputNode
|
|
16
|
+
//
|
|
17
|
+
// Timebase
|
|
18
|
+
// --------
|
|
19
|
+
// AudioContext::currentTime() is the engine's own running time: frames rendered
|
|
20
|
+
// by the source node divided by the graph's sample rate (Web Audio semantics —
|
|
21
|
+
// it advances a render quantum at a time and sits slightly ahead of what the DAC
|
|
22
|
+
// has emitted). start(when)/stop(when) are converted to absolute frame numbers
|
|
23
|
+
// in that same timebase, so a note scheduled 150 ms out really starts 150 ms
|
|
24
|
+
// out. Before the engine is running (or if it never starts, e.g. no output
|
|
25
|
+
// device) currentTime() falls back to a steady wall clock so an app whose
|
|
26
|
+
// scheduler is driven off it keeps making progress instead of wedging; the
|
|
27
|
+
// frame offset is seeded from that clock when the engine does start, so the
|
|
28
|
+
// value is monotonic across the handover.
|
|
29
|
+
//
|
|
30
|
+
// Thread safety
|
|
31
|
+
// -------------
|
|
32
|
+
// The render block runs on the CoreAudio realtime thread. It allocates nothing,
|
|
33
|
+
// takes no lock, sends no Objective-C message and logs nothing: it drains a
|
|
34
|
+
// bounded lock-free command ring (producers serialise among themselves on a
|
|
35
|
+
// mutex the render thread never touches) into a fixed voice array it alone
|
|
36
|
+
// owns. Everything the app thread mutates — oscillator records, the AVAudioEngine
|
|
37
|
+
// graph — lives behind app-side mutexes the render thread never takes.
|
|
38
|
+
//
|
|
39
|
+
// Clicks
|
|
40
|
+
// ------
|
|
41
|
+
// Every note gets a ~2.5 ms attack ramp and a ~6 ms release ramp that starts at
|
|
42
|
+
// its scheduled stop time, so a burst of 24 ms notes (button-tetris) neither
|
|
43
|
+
// clicks on attack nor thuds on release.
|
|
44
|
+
//
|
|
45
|
+
// Verification hooks
|
|
46
|
+
// ------------------
|
|
47
|
+
// GEA_AUDIO_OFFLINE=1 build the graph in manual-rendering mode
|
|
48
|
+
// (no output device is touched).
|
|
49
|
+
// GEA_AUDIO_CAPTURE_RAW=<path> as above, plus a thread that pulls the graph
|
|
50
|
+
// at real-time pace and appends raw interleaved
|
|
51
|
+
// float32 stereo @48 kHz to <path>. That is how
|
|
52
|
+
// a real app run is measured end-to-end —
|
|
53
|
+
// analyze it with analyze-audio-capture.mjs.
|
|
54
|
+
// GEA_AUDIO_DISABLED=1 build no graph at all: the same state as
|
|
55
|
+
// "no output device", everything still callable.
|
|
56
|
+
// GEA_AUDIO_DEBUG=1 one stderr line at setup: mode, rate, whether
|
|
57
|
+
// the engine started, and why not.
|
|
58
|
+
// See apple_audio_testing.h for the in-process offline API, and
|
|
59
|
+
// build-audio-selftest.sh for the measurement harness.
|
|
60
|
+
|
|
61
|
+
#import <AVFoundation/AVFoundation.h>
|
|
62
|
+
|
|
63
|
+
#include <TargetConditionals.h>
|
|
64
|
+
|
|
65
|
+
#include "audio.h"
|
|
66
|
+
|
|
67
|
+
#include "apple_audio_testing.h"
|
|
68
|
+
|
|
69
|
+
#include <algorithm>
|
|
70
|
+
#include <atomic>
|
|
71
|
+
#include <chrono>
|
|
72
|
+
#include <cmath>
|
|
73
|
+
#include <cstdint>
|
|
74
|
+
#include <cstdio>
|
|
75
|
+
#include <cstdlib>
|
|
76
|
+
#include <cstring>
|
|
77
|
+
#include <limits>
|
|
78
|
+
#include <mutex>
|
|
79
|
+
#include <string>
|
|
80
|
+
#include <thread>
|
|
81
|
+
#include <vector>
|
|
82
|
+
|
|
83
|
+
namespace gea::platform::audio {
|
|
84
|
+
|
|
85
|
+
namespace {
|
|
86
|
+
|
|
87
|
+
constexpr int kMaxOscillators = 512; // app-side handle pool (round-robin)
|
|
88
|
+
constexpr int kMaxVoices = 64; // notes rendering simultaneously
|
|
89
|
+
constexpr int kCommandCapacity = 2048; // app thread -> render thread ring
|
|
90
|
+
constexpr double kTwoPi = 6.28318530717958647692;
|
|
91
|
+
constexpr double kDefaultFrequency = 440.0;
|
|
92
|
+
constexpr double kMinFrequency = 8.0;
|
|
93
|
+
constexpr double kMaxFrequency = 20000.0;
|
|
94
|
+
constexpr double kAttackSeconds = 0.0025;
|
|
95
|
+
constexpr double kReleaseSeconds = 0.006;
|
|
96
|
+
constexpr double kVoiceGain = 0.25; // headroom: four notes at once still fit
|
|
97
|
+
constexpr double kMaxVoiceSeconds = 30.0; // a never-stopped note cannot run forever
|
|
98
|
+
constexpr double kFallbackSampleRate = 48000.0;
|
|
99
|
+
constexpr double kCaptureSampleRate = 48000.0;
|
|
100
|
+
constexpr AVAudioFrameCount kOfflineMaxFrames = 4096;
|
|
101
|
+
constexpr AVAudioFrameCount kCaptureBlockFrames = 512;
|
|
102
|
+
|
|
103
|
+
double steadySeconds()
|
|
104
|
+
{
|
|
105
|
+
static const std::chrono::steady_clock::time_point origin = std::chrono::steady_clock::now();
|
|
106
|
+
return std::chrono::duration<double>(std::chrono::steady_clock::now() - origin).count();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
OscillatorType normalizeType(OscillatorType type)
|
|
110
|
+
{
|
|
111
|
+
switch (type) {
|
|
112
|
+
case OscillatorType::Sine:
|
|
113
|
+
case OscillatorType::Square:
|
|
114
|
+
case OscillatorType::Sawtooth:
|
|
115
|
+
case OscillatorType::Triangle: return type;
|
|
116
|
+
}
|
|
117
|
+
return OscillatorType::Sine;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
double normalizeFrequency(double frequency)
|
|
121
|
+
{
|
|
122
|
+
if (!std::isfinite(frequency)) return kDefaultFrequency;
|
|
123
|
+
if (frequency < kMinFrequency) return kMinFrequency;
|
|
124
|
+
if (frequency > kMaxFrequency) return kMaxFrequency;
|
|
125
|
+
return frequency;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// phase is [0, 1)
|
|
129
|
+
double waveform(OscillatorType type, double phase)
|
|
130
|
+
{
|
|
131
|
+
switch (type) {
|
|
132
|
+
case OscillatorType::Square: return phase < 0.5 ? 1.0 : -1.0;
|
|
133
|
+
case OscillatorType::Sawtooth: return 2.0 * phase - 1.0;
|
|
134
|
+
case OscillatorType::Triangle:
|
|
135
|
+
if (phase < 0.25) return 4.0 * phase;
|
|
136
|
+
if (phase < 0.75) return 2.0 - 4.0 * phase;
|
|
137
|
+
return 4.0 * phase - 4.0;
|
|
138
|
+
case OscillatorType::Sine: break;
|
|
139
|
+
}
|
|
140
|
+
return std::sin(phase * kTwoPi);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
bool envFlagSet(const char *name)
|
|
144
|
+
{
|
|
145
|
+
const char *value = std::getenv(name);
|
|
146
|
+
return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// app thread -> render thread commands
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
enum class CommandKind : std::uint8_t { StartVoice, StopVoice, SetFrequency, SetType, ScheduleFrequency };
|
|
153
|
+
|
|
154
|
+
struct Command {
|
|
155
|
+
CommandKind kind = CommandKind::StartVoice;
|
|
156
|
+
OscillatorType type = OscillatorType::Sine;
|
|
157
|
+
std::uint64_t voice = 0;
|
|
158
|
+
double frequency = kDefaultFrequency;
|
|
159
|
+
std::int64_t frame = 0;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// Multi-producer (serialised by a mutex the consumer never takes), single
|
|
163
|
+
// consumer, wait-free on the consumer side.
|
|
164
|
+
class CommandQueue {
|
|
165
|
+
public:
|
|
166
|
+
bool push(const Command &command)
|
|
167
|
+
{
|
|
168
|
+
std::lock_guard<std::mutex> guard(producers_);
|
|
169
|
+
const std::uint32_t tail = tail_.load(std::memory_order_relaxed);
|
|
170
|
+
const std::uint32_t next = (tail + 1u) % kCommandCapacity;
|
|
171
|
+
if (next == head_.load(std::memory_order_acquire)) {
|
|
172
|
+
dropped_.fetch_add(1, std::memory_order_relaxed);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
items_[tail] = command;
|
|
176
|
+
tail_.store(next, std::memory_order_release);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
bool pop(Command &out)
|
|
181
|
+
{
|
|
182
|
+
const std::uint32_t head = head_.load(std::memory_order_relaxed);
|
|
183
|
+
if (head == tail_.load(std::memory_order_acquire)) return false;
|
|
184
|
+
out = items_[head];
|
|
185
|
+
head_.store((head + 1u) % kCommandCapacity, std::memory_order_release);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
std::uint64_t dropped() const { return dropped_.load(std::memory_order_relaxed); }
|
|
190
|
+
|
|
191
|
+
private:
|
|
192
|
+
Command items_[kCommandCapacity]{};
|
|
193
|
+
std::atomic<std::uint32_t> head_{0};
|
|
194
|
+
std::atomic<std::uint32_t> tail_{0};
|
|
195
|
+
std::atomic<std::uint64_t> dropped_{0};
|
|
196
|
+
std::mutex producers_;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// render-thread voice state (touched by the render thread only)
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
constexpr int kMaxVoiceEvents = 8; // setValueAtTime events queued per note
|
|
203
|
+
|
|
204
|
+
struct VoiceEvent {
|
|
205
|
+
std::int64_t frame = 0;
|
|
206
|
+
double phaseIncrement = 0.0;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
struct Voice {
|
|
210
|
+
bool active = false;
|
|
211
|
+
std::uint64_t id = 0;
|
|
212
|
+
OscillatorType type = OscillatorType::Sine;
|
|
213
|
+
double phase = 0.0;
|
|
214
|
+
double phaseIncrement = 0.0;
|
|
215
|
+
std::int64_t startFrame = 0;
|
|
216
|
+
std::int64_t stopFrame = 0;
|
|
217
|
+
std::int64_t endFrame = 0; // stopFrame + release, or the runaway cap
|
|
218
|
+
bool stopScheduled = false;
|
|
219
|
+
VoiceEvent events[kMaxVoiceEvents]{};
|
|
220
|
+
int eventHead = 0;
|
|
221
|
+
int eventCount = 0;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// ---------------------------------------------------------------------------
|
|
225
|
+
// app-thread oscillator records
|
|
226
|
+
// ---------------------------------------------------------------------------
|
|
227
|
+
struct ScheduledValue {
|
|
228
|
+
double time = 0.0;
|
|
229
|
+
double value = kDefaultFrequency;
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
struct OscillatorRecord {
|
|
233
|
+
std::uint64_t handle = 0; // 0 == free slot
|
|
234
|
+
OscillatorType type = OscillatorType::Sine;
|
|
235
|
+
double frequency = kDefaultFrequency;
|
|
236
|
+
bool connected = false;
|
|
237
|
+
bool started = false;
|
|
238
|
+
bool stopped = false;
|
|
239
|
+
bool voiceLaunched = false;
|
|
240
|
+
double startTime = 0.0;
|
|
241
|
+
double stopTime = 0.0;
|
|
242
|
+
// setValueAtTime() calls made before the note is handed to the render thread
|
|
243
|
+
// are replayed as scheduled events the moment it is.
|
|
244
|
+
ScheduledValue pending[kMaxVoiceEvents]{};
|
|
245
|
+
int pendingCount = 0;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
class Engine {
|
|
249
|
+
public:
|
|
250
|
+
static Engine &instance()
|
|
251
|
+
{
|
|
252
|
+
// Deliberately leaked: a static destructor racing the CoreAudio render
|
|
253
|
+
// thread at process exit is a crash nobody can debug, and the process is
|
|
254
|
+
// going away anyway. The capture file (if any) is flushed per block.
|
|
255
|
+
static Engine *engine = new Engine();
|
|
256
|
+
return *engine;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ---- timebase -----------------------------------------------------------
|
|
260
|
+
double currentTime()
|
|
261
|
+
{
|
|
262
|
+
ensureSetup();
|
|
263
|
+
if (!running_.load(std::memory_order_relaxed)) return steadySeconds();
|
|
264
|
+
const double rate = sampleRate_.load(std::memory_order_relaxed);
|
|
265
|
+
const std::int64_t frames =
|
|
266
|
+
frameOffset_.load(std::memory_order_relaxed) + renderedFrames_.load(std::memory_order_relaxed);
|
|
267
|
+
return static_cast<double>(frames) / rate;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ---- oscillator lifecycle ----------------------------------------------
|
|
271
|
+
std::uint64_t createOscillator()
|
|
272
|
+
{
|
|
273
|
+
ensureSetup();
|
|
274
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
275
|
+
const int slot = nextSlot_;
|
|
276
|
+
nextSlot_ = (nextSlot_ + 1) % kMaxOscillators;
|
|
277
|
+
const std::uint64_t handle = (++generation_ << 10) | static_cast<std::uint64_t>(slot + 1);
|
|
278
|
+
records_[slot] = OscillatorRecord{};
|
|
279
|
+
records_[slot].handle = handle;
|
|
280
|
+
return handle;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
double frequency(std::uint64_t handle)
|
|
284
|
+
{
|
|
285
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
286
|
+
const OscillatorRecord *record = find(handle);
|
|
287
|
+
return record ? record->frequency : 0.0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
void setFrequency(std::uint64_t handle, double value)
|
|
291
|
+
{
|
|
292
|
+
Command command;
|
|
293
|
+
bool live = false;
|
|
294
|
+
{
|
|
295
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
296
|
+
OscillatorRecord *record = find(handle);
|
|
297
|
+
if (record == nullptr || !std::isfinite(value)) return;
|
|
298
|
+
record->frequency = value;
|
|
299
|
+
live = record->voiceLaunched && !record->stopped;
|
|
300
|
+
command = Command{CommandKind::SetFrequency, record->type, handle, normalizeFrequency(value), 0};
|
|
301
|
+
}
|
|
302
|
+
if (live) commands_.push(command);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
OscillatorType type(std::uint64_t handle)
|
|
306
|
+
{
|
|
307
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
308
|
+
const OscillatorRecord *record = find(handle);
|
|
309
|
+
return record ? record->type : OscillatorType::Sine;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
void setType(std::uint64_t handle, OscillatorType type)
|
|
313
|
+
{
|
|
314
|
+
Command command;
|
|
315
|
+
bool live = false;
|
|
316
|
+
{
|
|
317
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
318
|
+
OscillatorRecord *record = find(handle);
|
|
319
|
+
if (record == nullptr) return;
|
|
320
|
+
record->type = normalizeType(type);
|
|
321
|
+
live = record->voiceLaunched && !record->stopped;
|
|
322
|
+
command = Command{CommandKind::SetType, record->type, handle, record->frequency, 0};
|
|
323
|
+
}
|
|
324
|
+
if (live) commands_.push(command);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// A frequency change requested for a future time on a running note.
|
|
328
|
+
void scheduleFrequency(std::uint64_t handle, double value, double time)
|
|
329
|
+
{
|
|
330
|
+
ensureSetup();
|
|
331
|
+
Command command;
|
|
332
|
+
bool emit = false;
|
|
333
|
+
{
|
|
334
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
335
|
+
OscillatorRecord *record = find(handle);
|
|
336
|
+
if (record == nullptr || !std::isfinite(value)) return;
|
|
337
|
+
if (record->voiceLaunched && !record->stopped) {
|
|
338
|
+
emit = true;
|
|
339
|
+
command = Command{CommandKind::ScheduleFrequency, record->type, handle,
|
|
340
|
+
normalizeFrequency(value), frameFor(time)};
|
|
341
|
+
} else if (record->pendingCount < kMaxVoiceEvents) {
|
|
342
|
+
record->pending[record->pendingCount++] = ScheduledValue{time, value};
|
|
343
|
+
}
|
|
344
|
+
// Web Audio: a value scheduled for the future does not change
|
|
345
|
+
// frequency.value now — only setValue()/`= x` does that.
|
|
346
|
+
}
|
|
347
|
+
if (emit) commands_.push(command);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
void connect(std::uint64_t handle)
|
|
351
|
+
{
|
|
352
|
+
Command commands[kMaxVoiceEvents + 2];
|
|
353
|
+
int count = 0;
|
|
354
|
+
{
|
|
355
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
356
|
+
OscillatorRecord *record = find(handle);
|
|
357
|
+
if (record == nullptr || record->connected) return;
|
|
358
|
+
record->connected = true;
|
|
359
|
+
// start() before connect() is legal; the note becomes audible here.
|
|
360
|
+
if (record->started && !record->voiceLaunched) {
|
|
361
|
+
record->voiceLaunched = true;
|
|
362
|
+
count = launchCommands(*record, handle, commands);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
for (int index = 0; index < count; ++index) commands_.push(commands[index]);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
void start(std::uint64_t handle, double when)
|
|
369
|
+
{
|
|
370
|
+
ensureSetup();
|
|
371
|
+
Command commands[kMaxVoiceEvents + 2];
|
|
372
|
+
int count = 0;
|
|
373
|
+
{
|
|
374
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
375
|
+
OscillatorRecord *record = find(handle);
|
|
376
|
+
if (record == nullptr || record->started) return;
|
|
377
|
+
record->started = true;
|
|
378
|
+
const double now = currentTimeLocked();
|
|
379
|
+
double at = (std::isfinite(when) && when > 0.0) ? when : now;
|
|
380
|
+
if (at < now) at = now;
|
|
381
|
+
record->startTime = at;
|
|
382
|
+
if (record->connected) {
|
|
383
|
+
record->voiceLaunched = true;
|
|
384
|
+
count = launchCommands(*record, handle, commands);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
for (int index = 0; index < count; ++index) commands_.push(commands[index]);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
void stop(std::uint64_t handle, double when)
|
|
391
|
+
{
|
|
392
|
+
ensureSetup();
|
|
393
|
+
Command command;
|
|
394
|
+
bool emit = false;
|
|
395
|
+
{
|
|
396
|
+
std::lock_guard<std::mutex> guard(recordsMutex_);
|
|
397
|
+
OscillatorRecord *record = find(handle);
|
|
398
|
+
if (record == nullptr || !record->started || record->stopped) return;
|
|
399
|
+
record->stopped = true;
|
|
400
|
+
const double now = currentTimeLocked();
|
|
401
|
+
double at = (std::isfinite(when) && when > 0.0) ? when : now;
|
|
402
|
+
if (at < record->startTime) at = record->startTime;
|
|
403
|
+
record->stopTime = at;
|
|
404
|
+
// stop() before connect() is legal too: the stop rides along with the
|
|
405
|
+
// launch commands when connect() finally makes the note audible.
|
|
406
|
+
emit = record->voiceLaunched;
|
|
407
|
+
command = Command{CommandKind::StopVoice, record->type, handle, record->frequency, frameFor(at)};
|
|
408
|
+
}
|
|
409
|
+
if (emit) commands_.push(command);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---- volume -------------------------------------------------------------
|
|
413
|
+
int volume() const { return volume_.load(std::memory_order_relaxed); }
|
|
414
|
+
|
|
415
|
+
void setVolume(int percent)
|
|
416
|
+
{
|
|
417
|
+
if (percent < 0) percent = 0;
|
|
418
|
+
if (percent > 100) percent = 100;
|
|
419
|
+
volume_.store(percent, std::memory_order_relaxed);
|
|
420
|
+
const float linear = static_cast<float>(percent) / 100.0f;
|
|
421
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
422
|
+
@try {
|
|
423
|
+
if (filePlayer_ != nil) filePlayer_.volume = linear;
|
|
424
|
+
if (pcmPlayer_ != nil) pcmPlayer_.volume = linear;
|
|
425
|
+
} @catch (NSException *exception) {
|
|
426
|
+
(void)exception;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ---- sample playback ----------------------------------------------------
|
|
431
|
+
bool playFile(const std::string &path)
|
|
432
|
+
{
|
|
433
|
+
ensureSetup();
|
|
434
|
+
if (path.empty()) return false;
|
|
435
|
+
NSURL *url = resolveURL(path);
|
|
436
|
+
if (url == nil) return false;
|
|
437
|
+
|
|
438
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
439
|
+
if (engine_ == nil) return false;
|
|
440
|
+
@try {
|
|
441
|
+
NSError *error = nil;
|
|
442
|
+
AVAudioFile *file = [[AVAudioFile alloc] initForReading:url error:&error];
|
|
443
|
+
if (file == nil) return false;
|
|
444
|
+
if (filePlayer_ != nil) [filePlayer_ stop];
|
|
445
|
+
if (!ensurePlayerLocked(filePlayer_, filePlayerFormat_, file.processingFormat)) return false;
|
|
446
|
+
[filePlayer_ scheduleFile:file atTime:nil completionHandler:nil];
|
|
447
|
+
[filePlayer_ play];
|
|
448
|
+
return true;
|
|
449
|
+
} @catch (NSException *exception) {
|
|
450
|
+
(void)exception;
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
bool playPcm(const std::int16_t *samples, std::size_t sampleCount, int sampleRate, int channels)
|
|
456
|
+
{
|
|
457
|
+
ensureSetup();
|
|
458
|
+
if (samples == nullptr || sampleCount == 0) return false;
|
|
459
|
+
if (channels < 1) channels = 1;
|
|
460
|
+
if (channels > 2) channels = 2;
|
|
461
|
+
if (sampleRate <= 0) sampleRate = 16000;
|
|
462
|
+
const std::size_t frames = sampleCount / static_cast<std::size_t>(channels);
|
|
463
|
+
if (frames == 0) return false;
|
|
464
|
+
|
|
465
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
466
|
+
if (engine_ == nil) return false;
|
|
467
|
+
@try {
|
|
468
|
+
AVAudioFormat *format = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
|
469
|
+
sampleRate:static_cast<double>(sampleRate)
|
|
470
|
+
channels:static_cast<AVAudioChannelCount>(channels)
|
|
471
|
+
interleaved:NO];
|
|
472
|
+
if (format == nil) return false;
|
|
473
|
+
AVAudioPCMBuffer *buffer =
|
|
474
|
+
[[AVAudioPCMBuffer alloc] initWithPCMFormat:format
|
|
475
|
+
frameCapacity:static_cast<AVAudioFrameCount>(frames)];
|
|
476
|
+
if (buffer == nil) return false;
|
|
477
|
+
buffer.frameLength = static_cast<AVAudioFrameCount>(frames);
|
|
478
|
+
float *const *destination = buffer.floatChannelData;
|
|
479
|
+
if (destination == nullptr) return false;
|
|
480
|
+
for (std::size_t frame = 0; frame < frames; ++frame) {
|
|
481
|
+
for (int channel = 0; channel < channels; ++channel) {
|
|
482
|
+
const std::int16_t sample = samples[frame * static_cast<std::size_t>(channels) + channel];
|
|
483
|
+
destination[channel][frame] = static_cast<float>(sample) / 32768.0f;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (pcmPlayer_ != nil) [pcmPlayer_ stop];
|
|
487
|
+
if (!ensurePlayerLocked(pcmPlayer_, pcmPlayerFormat_, format)) return false;
|
|
488
|
+
[pcmPlayer_ scheduleBuffer:buffer atTime:nil options:0 completionHandler:nil];
|
|
489
|
+
[pcmPlayer_ play];
|
|
490
|
+
return true;
|
|
491
|
+
} @catch (NSException *exception) {
|
|
492
|
+
(void)exception;
|
|
493
|
+
return false;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
void stopPlayback()
|
|
498
|
+
{
|
|
499
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
500
|
+
@try {
|
|
501
|
+
if (filePlayer_ != nil) [filePlayer_ stop];
|
|
502
|
+
if (pcmPlayer_ != nil) [pcmPlayer_ stop];
|
|
503
|
+
} @catch (NSException *exception) {
|
|
504
|
+
(void)exception;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ---- offline rendering (see apple_audio_testing.h) -----------------------
|
|
509
|
+
bool beginOffline(double sampleRate)
|
|
510
|
+
{
|
|
511
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
512
|
+
if (setupDone_) return false;
|
|
513
|
+
offlineRequested_ = true;
|
|
514
|
+
if (sampleRate > 0.0) offlineSampleRate_ = sampleRate;
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
std::size_t renderOffline(float *interleavedStereo, std::size_t frames)
|
|
519
|
+
{
|
|
520
|
+
ensureSetup();
|
|
521
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
522
|
+
return renderOfflineLocked(interleavedStereo, frames);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
double offlineSampleRate() const { return sampleRate_.load(std::memory_order_relaxed); }
|
|
526
|
+
|
|
527
|
+
void endOffline()
|
|
528
|
+
{
|
|
529
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
530
|
+
if (engine_ == nil) return;
|
|
531
|
+
@try {
|
|
532
|
+
[engine_ stop];
|
|
533
|
+
if (engine_.isInManualRenderingMode) [engine_ disableManualRenderingMode];
|
|
534
|
+
} @catch (NSException *exception) {
|
|
535
|
+
(void)exception;
|
|
536
|
+
}
|
|
537
|
+
running_.store(false, std::memory_order_relaxed);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ---- realtime render callback ------------------------------------------
|
|
541
|
+
// Realtime thread. No allocation, no lock, no Objective-C, no logging.
|
|
542
|
+
void renderAudio(AVAudioFrameCount frameCount, AudioBufferList *output, BOOL *isSilence)
|
|
543
|
+
{
|
|
544
|
+
Command command;
|
|
545
|
+
while (commands_.pop(command)) applyCommand(command);
|
|
546
|
+
|
|
547
|
+
const std::int64_t base =
|
|
548
|
+
frameOffset_.load(std::memory_order_relaxed) + renderedFrames_.load(std::memory_order_relaxed);
|
|
549
|
+
const double gain =
|
|
550
|
+
static_cast<double>(volume_.load(std::memory_order_relaxed)) / 100.0 * kVoiceGain;
|
|
551
|
+
const double attack = attackFrames_;
|
|
552
|
+
const double release = releaseFrames_;
|
|
553
|
+
|
|
554
|
+
bool sounded = false;
|
|
555
|
+
const std::uint32_t bufferCount = output != nullptr ? output->mNumberBuffers : 0;
|
|
556
|
+
float *channel0 = bufferCount > 0 ? static_cast<float *>(output->mBuffers[0].mData) : nullptr;
|
|
557
|
+
float *channel1 = bufferCount > 1 ? static_cast<float *>(output->mBuffers[1].mData) : nullptr;
|
|
558
|
+
|
|
559
|
+
for (AVAudioFrameCount index = 0; index < frameCount; ++index) {
|
|
560
|
+
const std::int64_t frame = base + static_cast<std::int64_t>(index);
|
|
561
|
+
double mixed = 0.0;
|
|
562
|
+
for (Voice &voice : voices_) {
|
|
563
|
+
if (!voice.active) continue;
|
|
564
|
+
if (frame >= voice.endFrame) {
|
|
565
|
+
voice.active = false;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
if (frame < voice.startFrame) continue;
|
|
569
|
+
|
|
570
|
+
while (voice.eventCount > 0 && frame >= voice.events[voice.eventHead].frame) {
|
|
571
|
+
voice.phaseIncrement = voice.events[voice.eventHead].phaseIncrement;
|
|
572
|
+
voice.eventHead = (voice.eventHead + 1) % kMaxVoiceEvents;
|
|
573
|
+
--voice.eventCount;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
double level = 1.0;
|
|
577
|
+
const double sinceStart = static_cast<double>(frame - voice.startFrame);
|
|
578
|
+
if (sinceStart < attack) level = sinceStart / attack;
|
|
579
|
+
if (voice.stopScheduled && frame >= voice.stopFrame) {
|
|
580
|
+
const double sinceStop = static_cast<double>(frame - voice.stopFrame);
|
|
581
|
+
const double decay = 1.0 - sinceStop / release;
|
|
582
|
+
level *= decay > 0.0 ? decay : 0.0;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
mixed += waveform(voice.type, voice.phase) * level;
|
|
586
|
+
voice.phase += voice.phaseIncrement;
|
|
587
|
+
if (voice.phase >= 1.0) voice.phase -= 1.0;
|
|
588
|
+
sounded = true;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
double sample = mixed * gain;
|
|
592
|
+
if (sample > 1.0) sample = 1.0;
|
|
593
|
+
if (sample < -1.0) sample = -1.0;
|
|
594
|
+
const float value = static_cast<float>(sample);
|
|
595
|
+
if (channel0 != nullptr) channel0[index] = value;
|
|
596
|
+
if (channel1 != nullptr) channel1[index] = value;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
for (std::uint32_t buffer = 2; buffer < bufferCount; ++buffer) {
|
|
600
|
+
float *extra = static_cast<float *>(output->mBuffers[buffer].mData);
|
|
601
|
+
if (extra == nullptr) continue;
|
|
602
|
+
for (AVAudioFrameCount index = 0; index < frameCount; ++index) extra[index] = 0.0f;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
renderedFrames_.fetch_add(static_cast<std::int64_t>(frameCount), std::memory_order_relaxed);
|
|
606
|
+
if (isSilence != nullptr) *isSilence = sounded ? NO : YES;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private:
|
|
610
|
+
Engine() = default;
|
|
611
|
+
|
|
612
|
+
// ---- records ------------------------------------------------------------
|
|
613
|
+
OscillatorRecord *find(std::uint64_t handle)
|
|
614
|
+
{
|
|
615
|
+
if (handle == 0) return nullptr;
|
|
616
|
+
const std::uint64_t slot = (handle & 0x3ffull);
|
|
617
|
+
if (slot == 0 || slot > static_cast<std::uint64_t>(kMaxOscillators)) return nullptr;
|
|
618
|
+
OscillatorRecord &record = records_[slot - 1];
|
|
619
|
+
return record.handle == handle ? &record : nullptr;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const OscillatorRecord *find(std::uint64_t handle) const
|
|
623
|
+
{
|
|
624
|
+
return const_cast<Engine *>(this)->find(handle);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// The StartVoice command plus any setValueAtTime events queued before launch.
|
|
628
|
+
int launchCommands(const OscillatorRecord &record, std::uint64_t handle, Command *out) const
|
|
629
|
+
{
|
|
630
|
+
double initial = record.frequency;
|
|
631
|
+
int count = 0;
|
|
632
|
+
// An event at or before the note's start time is just the starting value.
|
|
633
|
+
for (int index = 0; index < record.pendingCount; ++index) {
|
|
634
|
+
if (record.pending[index].time <= record.startTime) initial = record.pending[index].value;
|
|
635
|
+
}
|
|
636
|
+
out[count++] = Command{CommandKind::StartVoice, record.type, handle, normalizeFrequency(initial),
|
|
637
|
+
frameFor(record.startTime)};
|
|
638
|
+
for (int index = 0; index < record.pendingCount; ++index) {
|
|
639
|
+
const ScheduledValue &event = record.pending[index];
|
|
640
|
+
if (event.time <= record.startTime) continue;
|
|
641
|
+
out[count++] = Command{CommandKind::ScheduleFrequency, record.type, handle,
|
|
642
|
+
normalizeFrequency(event.value), frameFor(event.time)};
|
|
643
|
+
}
|
|
644
|
+
if (record.stopped) {
|
|
645
|
+
out[count++] = Command{CommandKind::StopVoice, record.type, handle,
|
|
646
|
+
normalizeFrequency(record.frequency), frameFor(record.stopTime)};
|
|
647
|
+
}
|
|
648
|
+
return count;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// currentTime() without ensureSetup() — callable while recordsMutex_ is held.
|
|
652
|
+
double currentTimeLocked() const
|
|
653
|
+
{
|
|
654
|
+
if (!running_.load(std::memory_order_relaxed)) return steadySeconds();
|
|
655
|
+
const double rate = sampleRate_.load(std::memory_order_relaxed);
|
|
656
|
+
const std::int64_t frames =
|
|
657
|
+
frameOffset_.load(std::memory_order_relaxed) + renderedFrames_.load(std::memory_order_relaxed);
|
|
658
|
+
return static_cast<double>(frames) / rate;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
std::int64_t frameFor(double seconds) const
|
|
662
|
+
{
|
|
663
|
+
const double rate = sampleRate_.load(std::memory_order_relaxed);
|
|
664
|
+
const double frame = seconds * rate;
|
|
665
|
+
if (!std::isfinite(frame)) return 0;
|
|
666
|
+
if (frame > 9.0e15) return static_cast<std::int64_t>(9.0e15);
|
|
667
|
+
if (frame < 0.0) return 0;
|
|
668
|
+
return static_cast<std::int64_t>(std::llround(frame));
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// ---- render-thread command application ----------------------------------
|
|
672
|
+
void applyCommand(const Command &command)
|
|
673
|
+
{
|
|
674
|
+
switch (command.kind) {
|
|
675
|
+
case CommandKind::StartVoice: {
|
|
676
|
+
Voice *voice = allocateVoice();
|
|
677
|
+
if (voice == nullptr) return;
|
|
678
|
+
const double rate = sampleRate_.load(std::memory_order_relaxed);
|
|
679
|
+
voice->active = true;
|
|
680
|
+
voice->id = command.voice;
|
|
681
|
+
voice->type = command.type;
|
|
682
|
+
voice->phase = 0.0;
|
|
683
|
+
voice->phaseIncrement = command.frequency / rate;
|
|
684
|
+
voice->startFrame = command.frame;
|
|
685
|
+
voice->stopFrame = 0;
|
|
686
|
+
voice->stopScheduled = false;
|
|
687
|
+
voice->endFrame = command.frame + static_cast<std::int64_t>(kMaxVoiceSeconds * rate);
|
|
688
|
+
voice->eventHead = 0;
|
|
689
|
+
voice->eventCount = 0;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
case CommandKind::StopVoice: {
|
|
693
|
+
Voice *voice = findVoice(command.voice);
|
|
694
|
+
if (voice == nullptr) return;
|
|
695
|
+
std::int64_t at = command.frame;
|
|
696
|
+
if (at < voice->startFrame) at = voice->startFrame;
|
|
697
|
+
voice->stopFrame = at;
|
|
698
|
+
voice->stopScheduled = true;
|
|
699
|
+
voice->endFrame = at + static_cast<std::int64_t>(releaseFrames_) + 1;
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
case CommandKind::SetFrequency: {
|
|
703
|
+
Voice *voice = findVoice(command.voice);
|
|
704
|
+
if (voice == nullptr) return;
|
|
705
|
+
voice->phaseIncrement = command.frequency / sampleRate_.load(std::memory_order_relaxed);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
case CommandKind::SetType: {
|
|
709
|
+
Voice *voice = findVoice(command.voice);
|
|
710
|
+
if (voice == nullptr) return;
|
|
711
|
+
voice->type = command.type;
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
case CommandKind::ScheduleFrequency: {
|
|
715
|
+
Voice *voice = findVoice(command.voice);
|
|
716
|
+
if (voice == nullptr || voice->eventCount >= kMaxVoiceEvents) return;
|
|
717
|
+
const int slot = (voice->eventHead + voice->eventCount) % kMaxVoiceEvents;
|
|
718
|
+
voice->events[slot] =
|
|
719
|
+
VoiceEvent{command.frame, command.frequency / sampleRate_.load(std::memory_order_relaxed)};
|
|
720
|
+
++voice->eventCount;
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
Voice *findVoice(std::uint64_t id)
|
|
727
|
+
{
|
|
728
|
+
for (Voice &voice : voices_)
|
|
729
|
+
if (voice.active && voice.id == id) return &voice;
|
|
730
|
+
return nullptr;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
Voice *allocateVoice()
|
|
734
|
+
{
|
|
735
|
+
Voice *oldest = nullptr;
|
|
736
|
+
for (Voice &voice : voices_) {
|
|
737
|
+
if (!voice.active) return &voice;
|
|
738
|
+
if (oldest == nullptr || voice.endFrame < oldest->endFrame) oldest = &voice;
|
|
739
|
+
}
|
|
740
|
+
return oldest; // steal the note closest to finishing
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// ---- engine setup -------------------------------------------------------
|
|
744
|
+
void ensureSetup()
|
|
745
|
+
{
|
|
746
|
+
if (setupDone_.load(std::memory_order_acquire)) return;
|
|
747
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
748
|
+
if (setupDone_.load(std::memory_order_relaxed)) return;
|
|
749
|
+
setupLocked();
|
|
750
|
+
setupDone_.store(true, std::memory_order_release);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
void setupLocked()
|
|
754
|
+
{
|
|
755
|
+
const bool offline = offlineRequested_ || envFlagSet("GEA_AUDIO_OFFLINE") ||
|
|
756
|
+
(std::getenv("GEA_AUDIO_CAPTURE_RAW") != nullptr);
|
|
757
|
+
const char *capturePath = std::getenv("GEA_AUDIO_CAPTURE_RAW");
|
|
758
|
+
|
|
759
|
+
// GEA_AUDIO_DISABLED=1 builds no graph at all. That is the same state the
|
|
760
|
+
// backend lands in when there is no output device, so it is also how the
|
|
761
|
+
// no-device path is exercised: everything stays callable, currentTime()
|
|
762
|
+
// runs off the wall clock and nothing sounds.
|
|
763
|
+
if (envFlagSet("GEA_AUDIO_DISABLED")) {
|
|
764
|
+
running_.store(false, std::memory_order_relaxed);
|
|
765
|
+
report(offline, sampleRate_.load(std::memory_order_relaxed), false, nil);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
@try {
|
|
770
|
+
#if TARGET_OS_IPHONE
|
|
771
|
+
if (!offline) activateAudioSession();
|
|
772
|
+
#endif
|
|
773
|
+
engine_ = [[AVAudioEngine alloc] init];
|
|
774
|
+
if (engine_ == nil) return;
|
|
775
|
+
|
|
776
|
+
double rate = kFallbackSampleRate;
|
|
777
|
+
if (offline) {
|
|
778
|
+
rate = capturePath != nullptr ? kCaptureSampleRate : offlineSampleRate_;
|
|
779
|
+
} else {
|
|
780
|
+
const double deviceRate = [[engine_ outputNode] outputFormatForBus:0].sampleRate;
|
|
781
|
+
if (deviceRate > 0.0) rate = deviceRate;
|
|
782
|
+
}
|
|
783
|
+
if (!(rate > 0.0)) rate = kFallbackSampleRate;
|
|
784
|
+
sampleRate_.store(rate, std::memory_order_relaxed);
|
|
785
|
+
attackFrames_ = std::max(1.0, kAttackSeconds * rate);
|
|
786
|
+
releaseFrames_ = std::max(1.0, kReleaseSeconds * rate);
|
|
787
|
+
|
|
788
|
+
format_ = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
|
|
789
|
+
sampleRate:rate
|
|
790
|
+
channels:2
|
|
791
|
+
interleaved:NO];
|
|
792
|
+
if (format_ == nil) return;
|
|
793
|
+
|
|
794
|
+
Engine *self = this;
|
|
795
|
+
sourceNode_ = [[AVAudioSourceNode alloc]
|
|
796
|
+
initWithFormat:format_
|
|
797
|
+
renderBlock:^OSStatus(BOOL *isSilence, const AudioTimeStamp *timestamp,
|
|
798
|
+
AVAudioFrameCount frameCount, AudioBufferList *outputData) {
|
|
799
|
+
(void)timestamp;
|
|
800
|
+
self->renderAudio(frameCount, outputData, isSilence);
|
|
801
|
+
return noErr;
|
|
802
|
+
}];
|
|
803
|
+
if (sourceNode_ == nil) return;
|
|
804
|
+
|
|
805
|
+
[engine_ attachNode:sourceNode_];
|
|
806
|
+
[engine_ connect:sourceNode_ to:[engine_ mainMixerNode] format:format_];
|
|
807
|
+
|
|
808
|
+
if (offline) {
|
|
809
|
+
NSError *error = nil;
|
|
810
|
+
if (![engine_ enableManualRenderingMode:AVAudioEngineManualRenderingModeOffline
|
|
811
|
+
format:format_
|
|
812
|
+
maximumFrameCount:kOfflineMaxFrames
|
|
813
|
+
error:&error]) {
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
manualBuffer_ = [[AVAudioPCMBuffer alloc] initWithPCMFormat:engine_.manualRenderingFormat
|
|
817
|
+
frameCapacity:kOfflineMaxFrames];
|
|
818
|
+
if (manualBuffer_ == nil) return;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const double rateNow = sampleRate_.load(std::memory_order_relaxed);
|
|
822
|
+
renderedFrames_.store(0, std::memory_order_relaxed);
|
|
823
|
+
frameOffset_.store(static_cast<std::int64_t>(std::llround(steadySeconds() * rateNow)),
|
|
824
|
+
std::memory_order_relaxed);
|
|
825
|
+
|
|
826
|
+
NSError *startError = nil;
|
|
827
|
+
if (![engine_ startAndReturnError:&startError]) {
|
|
828
|
+
running_.store(false, std::memory_order_relaxed);
|
|
829
|
+
// No output device (or the device refused): stay silent, keep the
|
|
830
|
+
// wall-clock timebase, and retry when the configuration changes.
|
|
831
|
+
report(offline, rateNow, false, startError);
|
|
832
|
+
observeConfigurationChanges();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
running_.store(true, std::memory_order_relaxed);
|
|
836
|
+
report(offline, rateNow, true, nil);
|
|
837
|
+
offlineMode_ = offline;
|
|
838
|
+
const float linear = static_cast<float>(volume_.load(std::memory_order_relaxed)) / 100.0f;
|
|
839
|
+
if (filePlayer_ != nil) filePlayer_.volume = linear;
|
|
840
|
+
if (pcmPlayer_ != nil) pcmPlayer_.volume = linear;
|
|
841
|
+
if (!offline) observeConfigurationChanges();
|
|
842
|
+
if (capturePath != nullptr) startCaptureLocked(capturePath);
|
|
843
|
+
} @catch (NSException *exception) {
|
|
844
|
+
(void)exception;
|
|
845
|
+
running_.store(false, std::memory_order_relaxed);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// GEA_AUDIO_DEBUG=1 prints one line at setup. Never called from the render
|
|
850
|
+
// thread, so stdio here is safe.
|
|
851
|
+
void report(bool offline, double rate, bool started, NSError *error) const
|
|
852
|
+
{
|
|
853
|
+
if (!envFlagSet("GEA_AUDIO_DEBUG")) return;
|
|
854
|
+
const char *reason = error != nil ? error.localizedDescription.UTF8String : "";
|
|
855
|
+
std::fprintf(stderr, "[gea-audio] mode=%s rate=%.0f started=%s%s%s\n",
|
|
856
|
+
offline ? "offline" : "device", rate, started ? "yes" : "no",
|
|
857
|
+
started ? "" : " error=", reason != nullptr ? reason : "");
|
|
858
|
+
std::fflush(stderr);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
#if TARGET_OS_IPHONE
|
|
862
|
+
void activateAudioSession()
|
|
863
|
+
{
|
|
864
|
+
@try {
|
|
865
|
+
AVAudioSession *session = [AVAudioSession sharedInstance];
|
|
866
|
+
NSError *error = nil;
|
|
867
|
+
// Playback (not Ambient) so a game's sound is audible with the ring
|
|
868
|
+
// switch silenced; MixWithOthers so it does not stop the user's music.
|
|
869
|
+
[session setCategory:AVAudioSessionCategoryPlayback
|
|
870
|
+
withOptions:AVAudioSessionCategoryOptionMixWithOthers
|
|
871
|
+
error:&error];
|
|
872
|
+
[session setActive:YES error:&error];
|
|
873
|
+
} @catch (NSException *exception) {
|
|
874
|
+
(void)exception;
|
|
875
|
+
}
|
|
876
|
+
observeInterruptions();
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// A call or Siri deactivates the session and stops the engine; nothing
|
|
880
|
+
// restarts it on its own. Reactivate and restart when the interruption ends.
|
|
881
|
+
void observeInterruptions()
|
|
882
|
+
{
|
|
883
|
+
if (interruptionObserver_ != nil) return;
|
|
884
|
+
Engine *self = this;
|
|
885
|
+
interruptionObserver_ = [[NSNotificationCenter defaultCenter]
|
|
886
|
+
addObserverForName:AVAudioSessionInterruptionNotification
|
|
887
|
+
object:[AVAudioSession sharedInstance]
|
|
888
|
+
queue:nil
|
|
889
|
+
usingBlock:^(NSNotification *note) {
|
|
890
|
+
NSNumber *type = note.userInfo[AVAudioSessionInterruptionTypeKey];
|
|
891
|
+
if (type == nil ||
|
|
892
|
+
type.unsignedIntegerValue != AVAudioSessionInterruptionTypeEnded) {
|
|
893
|
+
self->markStopped();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
self->activateAudioSession();
|
|
897
|
+
self->handleConfigurationChange();
|
|
898
|
+
}];
|
|
899
|
+
}
|
|
900
|
+
#endif
|
|
901
|
+
|
|
902
|
+
void markStopped() { running_.store(false, std::memory_order_relaxed); }
|
|
903
|
+
|
|
904
|
+
// A device disappearing (unplugged headphones, a display with speakers going
|
|
905
|
+
// away) stops the engine; AVAudioEngine posts a configuration change and the
|
|
906
|
+
// graph must be restarted. This also recovers a launch with no output device.
|
|
907
|
+
void observeConfigurationChanges()
|
|
908
|
+
{
|
|
909
|
+
if (configurationObserver_ != nil) return;
|
|
910
|
+
Engine *self = this;
|
|
911
|
+
configurationObserver_ = [[NSNotificationCenter defaultCenter]
|
|
912
|
+
addObserverForName:AVAudioEngineConfigurationChangeNotification
|
|
913
|
+
object:engine_
|
|
914
|
+
queue:nil
|
|
915
|
+
usingBlock:^(NSNotification *note) {
|
|
916
|
+
(void)note;
|
|
917
|
+
self->handleConfigurationChange();
|
|
918
|
+
}];
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
void handleConfigurationChange()
|
|
922
|
+
{
|
|
923
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
924
|
+
if (engine_ == nil || offlineMode_) return;
|
|
925
|
+
@try {
|
|
926
|
+
if (engine_.isRunning) return;
|
|
927
|
+
const double rate = sampleRate_.load(std::memory_order_relaxed);
|
|
928
|
+
renderedFrames_.store(0, std::memory_order_relaxed);
|
|
929
|
+
frameOffset_.store(static_cast<std::int64_t>(std::llround(steadySeconds() * rate)),
|
|
930
|
+
std::memory_order_relaxed);
|
|
931
|
+
NSError *error = nil;
|
|
932
|
+
running_.store([engine_ startAndReturnError:&error] ? true : false, std::memory_order_relaxed);
|
|
933
|
+
} @catch (NSException *exception) {
|
|
934
|
+
(void)exception;
|
|
935
|
+
running_.store(false, std::memory_order_relaxed);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
bool ensurePlayerLocked(AVAudioPlayerNode *__strong &player, AVAudioFormat *__strong ¤t,
|
|
940
|
+
AVAudioFormat *wanted)
|
|
941
|
+
{
|
|
942
|
+
if (engine_ == nil || wanted == nil) return false;
|
|
943
|
+
if (player == nil) {
|
|
944
|
+
player = [[AVAudioPlayerNode alloc] init];
|
|
945
|
+
if (player == nil) return false;
|
|
946
|
+
[engine_ attachNode:player];
|
|
947
|
+
player.volume = static_cast<float>(volume_.load(std::memory_order_relaxed)) / 100.0f;
|
|
948
|
+
}
|
|
949
|
+
if (current == nil || ![current isEqual:wanted]) {
|
|
950
|
+
[engine_ disconnectNodeOutput:player];
|
|
951
|
+
[engine_ connect:player to:[engine_ mainMixerNode] format:wanted];
|
|
952
|
+
current = wanted;
|
|
953
|
+
}
|
|
954
|
+
if (!engine_.isRunning) {
|
|
955
|
+
NSError *error = nil;
|
|
956
|
+
if (![engine_ startAndReturnError:&error]) return false;
|
|
957
|
+
running_.store(true, std::memory_order_relaxed);
|
|
958
|
+
}
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
NSURL *resolveURL(const std::string &path) const
|
|
963
|
+
{
|
|
964
|
+
NSString *text = [NSString stringWithUTF8String:path.c_str()];
|
|
965
|
+
if (text == nil || text.length == 0) return nil;
|
|
966
|
+
if ([text hasPrefix:@"file://"] || [text hasPrefix:@"http://"] || [text hasPrefix:@"https://"]) {
|
|
967
|
+
return [NSURL URLWithString:text];
|
|
968
|
+
}
|
|
969
|
+
if ([text hasPrefix:@"/"]) return [NSURL fileURLWithPath:text];
|
|
970
|
+
|
|
971
|
+
NSString *stripped = [text hasPrefix:@"./"] ? [text substringFromIndex:2] : text;
|
|
972
|
+
NSBundle *bundle = [NSBundle mainBundle];
|
|
973
|
+
if (bundle != nil) {
|
|
974
|
+
NSString *name = [stripped stringByDeletingPathExtension];
|
|
975
|
+
NSString *extension = [stripped pathExtension];
|
|
976
|
+
NSURL *resource = [bundle URLForResource:name withExtension:extension];
|
|
977
|
+
if (resource != nil) return resource;
|
|
978
|
+
NSString *inResources = [[bundle resourcePath] stringByAppendingPathComponent:stripped];
|
|
979
|
+
if (inResources != nil && [[NSFileManager defaultManager] fileExistsAtPath:inResources]) {
|
|
980
|
+
return [NSURL fileURLWithPath:inResources];
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
return [NSURL fileURLWithPath:stripped];
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
std::size_t renderOfflineLocked(float *interleavedStereo, std::size_t frames)
|
|
987
|
+
{
|
|
988
|
+
if (engine_ == nil || manualBuffer_ == nil || !engine_.isInManualRenderingMode) return 0;
|
|
989
|
+
std::size_t done = 0;
|
|
990
|
+
@try {
|
|
991
|
+
while (done < frames) {
|
|
992
|
+
const AVAudioFrameCount want = static_cast<AVAudioFrameCount>(
|
|
993
|
+
std::min<std::size_t>(frames - done, kOfflineMaxFrames));
|
|
994
|
+
NSError *error = nil;
|
|
995
|
+
const AVAudioEngineManualRenderingStatus status =
|
|
996
|
+
[engine_ renderOffline:want toBuffer:manualBuffer_ error:&error];
|
|
997
|
+
if (status != AVAudioEngineManualRenderingStatusSuccess) break;
|
|
998
|
+
const AVAudioFrameCount produced = manualBuffer_.frameLength;
|
|
999
|
+
if (produced == 0) break;
|
|
1000
|
+
float *const *channels = manualBuffer_.floatChannelData;
|
|
1001
|
+
const AVAudioChannelCount channelCount = manualBuffer_.format.channelCount;
|
|
1002
|
+
if (interleavedStereo != nullptr && channels != nullptr) {
|
|
1003
|
+
for (AVAudioFrameCount frame = 0; frame < produced; ++frame) {
|
|
1004
|
+
const float left = channels[0][frame];
|
|
1005
|
+
const float right = channelCount > 1 ? channels[1][frame] : left;
|
|
1006
|
+
interleavedStereo[(done + frame) * 2 + 0] = left;
|
|
1007
|
+
interleavedStereo[(done + frame) * 2 + 1] = right;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
done += produced;
|
|
1011
|
+
if (produced < want) break;
|
|
1012
|
+
}
|
|
1013
|
+
} @catch (NSException *exception) {
|
|
1014
|
+
(void)exception;
|
|
1015
|
+
}
|
|
1016
|
+
return done;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// Real-time-paced offline capture: renders the graph exactly as the device
|
|
1020
|
+
// would consume it and appends raw interleaved float32 stereo to a file.
|
|
1021
|
+
void startCaptureLocked(const char *path)
|
|
1022
|
+
{
|
|
1023
|
+
captureFile_ = std::fopen(path, "wb");
|
|
1024
|
+
if (captureFile_ == nullptr) return;
|
|
1025
|
+
captureRunning_.store(true, std::memory_order_relaxed);
|
|
1026
|
+
Engine *self = this;
|
|
1027
|
+
std::thread([self] { self->captureLoop(); }).detach();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
void captureLoop()
|
|
1031
|
+
{
|
|
1032
|
+
std::vector<float> block(static_cast<std::size_t>(kCaptureBlockFrames) * 2, 0.0f);
|
|
1033
|
+
const double rate = sampleRate_.load(std::memory_order_relaxed);
|
|
1034
|
+
const double blockSeconds = static_cast<double>(kCaptureBlockFrames) / rate;
|
|
1035
|
+
auto deadline = std::chrono::steady_clock::now();
|
|
1036
|
+
while (captureRunning_.load(std::memory_order_relaxed)) {
|
|
1037
|
+
std::size_t produced = 0;
|
|
1038
|
+
{
|
|
1039
|
+
std::lock_guard<std::mutex> guard(graphMutex_);
|
|
1040
|
+
produced = renderOfflineLocked(block.data(), kCaptureBlockFrames);
|
|
1041
|
+
}
|
|
1042
|
+
if (produced > 0 && captureFile_ != nullptr) {
|
|
1043
|
+
std::fwrite(block.data(), sizeof(float), produced * 2, captureFile_);
|
|
1044
|
+
std::fflush(captureFile_);
|
|
1045
|
+
}
|
|
1046
|
+
deadline += std::chrono::duration_cast<std::chrono::steady_clock::duration>(
|
|
1047
|
+
std::chrono::duration<double>(blockSeconds));
|
|
1048
|
+
std::this_thread::sleep_until(deadline);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// ---- state --------------------------------------------------------------
|
|
1053
|
+
std::mutex graphMutex_;
|
|
1054
|
+
std::mutex recordsMutex_;
|
|
1055
|
+
CommandQueue commands_;
|
|
1056
|
+
|
|
1057
|
+
OscillatorRecord records_[kMaxOscillators]{};
|
|
1058
|
+
int nextSlot_ = 0;
|
|
1059
|
+
std::uint64_t generation_ = 0;
|
|
1060
|
+
|
|
1061
|
+
Voice voices_[kMaxVoices]{};
|
|
1062
|
+
|
|
1063
|
+
std::atomic<bool> setupDone_{false};
|
|
1064
|
+
std::atomic<bool> running_{false};
|
|
1065
|
+
std::atomic<double> sampleRate_{kFallbackSampleRate};
|
|
1066
|
+
std::atomic<std::int64_t> renderedFrames_{0};
|
|
1067
|
+
std::atomic<std::int64_t> frameOffset_{0};
|
|
1068
|
+
std::atomic<int> volume_{80};
|
|
1069
|
+
double attackFrames_ = kAttackSeconds * kFallbackSampleRate;
|
|
1070
|
+
double releaseFrames_ = kReleaseSeconds * kFallbackSampleRate;
|
|
1071
|
+
|
|
1072
|
+
bool offlineRequested_ = false;
|
|
1073
|
+
bool offlineMode_ = false;
|
|
1074
|
+
double offlineSampleRate_ = kFallbackSampleRate;
|
|
1075
|
+
|
|
1076
|
+
std::atomic<bool> captureRunning_{false};
|
|
1077
|
+
std::FILE *captureFile_ = nullptr;
|
|
1078
|
+
|
|
1079
|
+
AVAudioEngine *engine_ = nil;
|
|
1080
|
+
AVAudioSourceNode *sourceNode_ = nil;
|
|
1081
|
+
AVAudioFormat *format_ = nil;
|
|
1082
|
+
AVAudioPCMBuffer *manualBuffer_ = nil;
|
|
1083
|
+
AVAudioPlayerNode *filePlayer_ = nil;
|
|
1084
|
+
AVAudioFormat *filePlayerFormat_ = nil;
|
|
1085
|
+
AVAudioPlayerNode *pcmPlayer_ = nil;
|
|
1086
|
+
AVAudioFormat *pcmPlayerFormat_ = nil;
|
|
1087
|
+
id configurationObserver_ = nil;
|
|
1088
|
+
id interruptionObserver_ = nil;
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
} // namespace
|
|
1092
|
+
|
|
1093
|
+
// ---------------------------------------------------------------------------
|
|
1094
|
+
// gea::platform::audio public API
|
|
1095
|
+
// ---------------------------------------------------------------------------
|
|
1096
|
+
|
|
1097
|
+
AudioParam::AudioParam(NativeAudioHandle oscillator) : oscillator_(oscillator) {}
|
|
1098
|
+
|
|
1099
|
+
double AudioParam::value() const
|
|
1100
|
+
{
|
|
1101
|
+
return Engine::instance().frequency(static_cast<std::uint64_t>(oscillator_));
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
void AudioParam::setValue(double value)
|
|
1105
|
+
{
|
|
1106
|
+
Engine::instance().setFrequency(static_cast<std::uint64_t>(oscillator_), value);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
void AudioParam::setValueAtTime(double value, double start_time)
|
|
1110
|
+
{
|
|
1111
|
+
// The engine schedules a note's parameters at start; a change requested for a
|
|
1112
|
+
// future time on an already-running note is applied when that time arrives.
|
|
1113
|
+
Engine &engine = Engine::instance();
|
|
1114
|
+
const auto handle = static_cast<std::uint64_t>(oscillator_);
|
|
1115
|
+
if (!std::isfinite(start_time) || start_time <= engine.currentTime()) {
|
|
1116
|
+
engine.setFrequency(handle, value);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
engine.scheduleFrequency(handle, value, start_time);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
AudioNode::AudioNode(NativeAudioHandle native) : native_(native) {}
|
|
1123
|
+
|
|
1124
|
+
NativeAudioHandle AudioNode::nativeId() const { return native_; }
|
|
1125
|
+
|
|
1126
|
+
AudioDestinationNode::AudioDestinationNode(NativeAudioHandle native) : AudioNode(native) {}
|
|
1127
|
+
|
|
1128
|
+
OscillatorNode::OscillatorNode(NativeAudioHandle native) : AudioNode(native), frequency(native) {}
|
|
1129
|
+
|
|
1130
|
+
OscillatorType OscillatorNode::type() const
|
|
1131
|
+
{
|
|
1132
|
+
return Engine::instance().type(static_cast<std::uint64_t>(nativeId()));
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
void OscillatorNode::setType(OscillatorType type)
|
|
1136
|
+
{
|
|
1137
|
+
Engine::instance().setType(static_cast<std::uint64_t>(nativeId()), type);
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
void OscillatorNode::connect(const AudioDestinationNode &)
|
|
1141
|
+
{
|
|
1142
|
+
Engine::instance().connect(static_cast<std::uint64_t>(nativeId()));
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
void OscillatorNode::start(double when)
|
|
1146
|
+
{
|
|
1147
|
+
Engine::instance().start(static_cast<std::uint64_t>(nativeId()), when);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
void OscillatorNode::stop(double when)
|
|
1151
|
+
{
|
|
1152
|
+
Engine::instance().stop(static_cast<std::uint64_t>(nativeId()), when);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
double AudioContext::currentTime() const { return Engine::instance().currentTime(); }
|
|
1156
|
+
|
|
1157
|
+
AudioDestinationNode AudioContext::destination() const { return AudioDestinationNode(1); }
|
|
1158
|
+
|
|
1159
|
+
OscillatorNode AudioContext::createOscillator() const
|
|
1160
|
+
{
|
|
1161
|
+
return OscillatorNode(static_cast<NativeAudioHandle>(Engine::instance().createOscillator()));
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
AudioContext AudioSystem::sharedContext() { return AudioContext{}; }
|
|
1165
|
+
|
|
1166
|
+
int AudioSystem::volume() { return Engine::instance().volume(); }
|
|
1167
|
+
|
|
1168
|
+
void AudioSystem::setVolume(int volume_percent) { Engine::instance().setVolume(volume_percent); }
|
|
1169
|
+
|
|
1170
|
+
bool AudioSystem::playFile(const std::string &path) { return Engine::instance().playFile(path); }
|
|
1171
|
+
|
|
1172
|
+
bool AudioSystem::playPcm(const std::int16_t *samples, std::size_t sample_count, int sample_rate, int channels)
|
|
1173
|
+
{
|
|
1174
|
+
return Engine::instance().playPcm(samples, sample_count, sample_rate, channels);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
void AudioSystem::stopPlayback() { Engine::instance().stopPlayback(); }
|
|
1178
|
+
|
|
1179
|
+
namespace testing {
|
|
1180
|
+
|
|
1181
|
+
bool beginOfflineRendering(double sample_rate) { return Engine::instance().beginOffline(sample_rate); }
|
|
1182
|
+
|
|
1183
|
+
std::size_t renderOfflineFrames(float *interleaved_stereo, std::size_t frames)
|
|
1184
|
+
{
|
|
1185
|
+
return Engine::instance().renderOffline(interleaved_stereo, frames);
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
double offlineSampleRate() { return Engine::instance().offlineSampleRate(); }
|
|
1189
|
+
|
|
1190
|
+
void endOfflineRendering() { Engine::instance().endOffline(); }
|
|
1191
|
+
|
|
1192
|
+
} // namespace testing
|
|
1193
|
+
|
|
1194
|
+
} // namespace gea::platform::audio
|