@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,602 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// Measures the Apple audio backend (targets/shared/apple_audio.mm) instead of
|
|
4
|
+
// trusting that it links. It drives the real gea::platform::audio API — the same
|
|
5
|
+
// calls button-tetris makes — through AVAudioEngine's manual-rendering mode and
|
|
6
|
+
// checks the rendered samples: RMS while a note is scheduled, silence when none
|
|
7
|
+
// is, the measured fundamental against the requested frequency, mixing,
|
|
8
|
+
// scheduling, volume, click-freedom and PCM/file playback.
|
|
9
|
+
//
|
|
10
|
+
// Build and run: targets/shared/build-audio-selftest.sh
|
|
11
|
+
|
|
12
|
+
#include "audio.h"
|
|
13
|
+
|
|
14
|
+
#include "apple_audio_testing.h"
|
|
15
|
+
|
|
16
|
+
#include <cmath>
|
|
17
|
+
#include <cstdarg>
|
|
18
|
+
#include <cstdint>
|
|
19
|
+
#include <cstdio>
|
|
20
|
+
#include <cstdlib>
|
|
21
|
+
#include <cstring>
|
|
22
|
+
#include <atomic>
|
|
23
|
+
#include <ctime>
|
|
24
|
+
#include <string>
|
|
25
|
+
#include <thread>
|
|
26
|
+
#include <vector>
|
|
27
|
+
|
|
28
|
+
namespace audio = gea::platform::audio;
|
|
29
|
+
|
|
30
|
+
namespace {
|
|
31
|
+
|
|
32
|
+
constexpr double kSampleRate = 48000.0;
|
|
33
|
+
|
|
34
|
+
int g_failures = 0;
|
|
35
|
+
int g_checks = 0;
|
|
36
|
+
|
|
37
|
+
void check(bool ok, const char *label, const char *detail)
|
|
38
|
+
{
|
|
39
|
+
++g_checks;
|
|
40
|
+
if (!ok) ++g_failures;
|
|
41
|
+
std::printf(" [%s] %-46s %s\n", ok ? "PASS" : "FAIL", label, detail);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
std::string format(const char *fmt, ...)
|
|
45
|
+
{
|
|
46
|
+
char buffer[256];
|
|
47
|
+
va_list args;
|
|
48
|
+
va_start(args, fmt);
|
|
49
|
+
std::vsnprintf(buffer, sizeof buffer, fmt, args);
|
|
50
|
+
va_end(args);
|
|
51
|
+
return std::string(buffer);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
struct Capture {
|
|
55
|
+
std::vector<float> interleaved; // stereo
|
|
56
|
+
double sampleRate = kSampleRate;
|
|
57
|
+
|
|
58
|
+
std::size_t frames() const { return interleaved.size() / 2; }
|
|
59
|
+
float left(std::size_t frame) const { return interleaved[frame * 2]; }
|
|
60
|
+
std::size_t frameAt(double seconds) const
|
|
61
|
+
{
|
|
62
|
+
const double frame = seconds * sampleRate;
|
|
63
|
+
if (frame < 0.0) return 0;
|
|
64
|
+
const auto index = static_cast<std::size_t>(frame);
|
|
65
|
+
return index > frames() ? frames() : index;
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
Capture render(double seconds)
|
|
70
|
+
{
|
|
71
|
+
Capture capture;
|
|
72
|
+
capture.sampleRate = audio::testing::offlineSampleRate();
|
|
73
|
+
const auto want = static_cast<std::size_t>(seconds * capture.sampleRate);
|
|
74
|
+
capture.interleaved.assign(want * 2, 0.0f);
|
|
75
|
+
const std::size_t produced = audio::testing::renderOfflineFrames(capture.interleaved.data(), want);
|
|
76
|
+
capture.interleaved.resize(produced * 2);
|
|
77
|
+
return capture;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
double rms(const Capture &capture, double fromSeconds, double toSeconds)
|
|
81
|
+
{
|
|
82
|
+
const std::size_t from = capture.frameAt(fromSeconds);
|
|
83
|
+
const std::size_t to = capture.frameAt(toSeconds);
|
|
84
|
+
if (to <= from) return 0.0;
|
|
85
|
+
double sum = 0.0;
|
|
86
|
+
for (std::size_t frame = from; frame < to; ++frame) {
|
|
87
|
+
const double sample = capture.left(frame);
|
|
88
|
+
sum += sample * sample;
|
|
89
|
+
}
|
|
90
|
+
return std::sqrt(sum / static_cast<double>(to - from));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
double peak(const Capture &capture, double fromSeconds, double toSeconds)
|
|
94
|
+
{
|
|
95
|
+
const std::size_t from = capture.frameAt(fromSeconds);
|
|
96
|
+
const std::size_t to = capture.frameAt(toSeconds);
|
|
97
|
+
double worst = 0.0;
|
|
98
|
+
for (std::size_t frame = from; frame < to; ++frame) worst = std::fmax(worst, std::fabs(capture.left(frame)));
|
|
99
|
+
return worst;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Fundamental from zero crossings of the DC-removed signal. Exact enough for the
|
|
103
|
+
// clean single tones the synth produces (square, sine, triangle, sawtooth).
|
|
104
|
+
double measuredFrequency(const Capture &capture, double fromSeconds, double toSeconds)
|
|
105
|
+
{
|
|
106
|
+
const std::size_t from = capture.frameAt(fromSeconds);
|
|
107
|
+
const std::size_t to = capture.frameAt(toSeconds);
|
|
108
|
+
if (to - from < 16) return 0.0;
|
|
109
|
+
|
|
110
|
+
double mean = 0.0;
|
|
111
|
+
for (std::size_t frame = from; frame < to; ++frame) mean += capture.left(frame);
|
|
112
|
+
mean /= static_cast<double>(to - from);
|
|
113
|
+
|
|
114
|
+
const double threshold = 0.05 * peak(capture, fromSeconds, toSeconds);
|
|
115
|
+
int crossings = 0;
|
|
116
|
+
int sign = 0;
|
|
117
|
+
std::size_t firstCrossing = 0;
|
|
118
|
+
std::size_t lastCrossing = 0;
|
|
119
|
+
for (std::size_t frame = from; frame < to; ++frame) {
|
|
120
|
+
const double value = capture.left(frame) - mean;
|
|
121
|
+
int next = sign;
|
|
122
|
+
if (value > threshold) next = 1;
|
|
123
|
+
else if (value < -threshold) next = -1;
|
|
124
|
+
if (next != sign && sign != 0) {
|
|
125
|
+
if (crossings == 0) firstCrossing = frame;
|
|
126
|
+
lastCrossing = frame;
|
|
127
|
+
++crossings;
|
|
128
|
+
}
|
|
129
|
+
sign = next;
|
|
130
|
+
}
|
|
131
|
+
if (crossings < 2 || lastCrossing <= firstCrossing) return 0.0;
|
|
132
|
+
const double span = static_cast<double>(lastCrossing - firstCrossing) / capture.sampleRate;
|
|
133
|
+
return static_cast<double>(crossings - 1) / (2.0 * span);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Energy at one frequency (Goertzel-style correlation), used to prove that a mix
|
|
137
|
+
// really contains every requested partial.
|
|
138
|
+
double energyAt(const Capture &capture, double frequency, double fromSeconds, double toSeconds)
|
|
139
|
+
{
|
|
140
|
+
const std::size_t from = capture.frameAt(fromSeconds);
|
|
141
|
+
const std::size_t to = capture.frameAt(toSeconds);
|
|
142
|
+
if (to <= from) return 0.0;
|
|
143
|
+
double real = 0.0;
|
|
144
|
+
double imaginary = 0.0;
|
|
145
|
+
const double step = 2.0 * M_PI * frequency / capture.sampleRate;
|
|
146
|
+
for (std::size_t frame = from; frame < to; ++frame) {
|
|
147
|
+
const double phase = step * static_cast<double>(frame - from);
|
|
148
|
+
const double sample = capture.left(frame);
|
|
149
|
+
real += sample * std::cos(phase);
|
|
150
|
+
imaginary += sample * std::sin(phase);
|
|
151
|
+
}
|
|
152
|
+
const double count = static_cast<double>(to - from);
|
|
153
|
+
return 2.0 * std::sqrt(real * real + imaginary * imaginary) / count;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
double maxDelta(const Capture &capture, double fromSeconds, double toSeconds)
|
|
157
|
+
{
|
|
158
|
+
const std::size_t from = capture.frameAt(fromSeconds);
|
|
159
|
+
const std::size_t to = capture.frameAt(toSeconds);
|
|
160
|
+
double worst = 0.0;
|
|
161
|
+
for (std::size_t frame = from + 1; frame < to; ++frame) {
|
|
162
|
+
worst = std::fmax(worst, std::fabs(capture.left(frame) - capture.left(frame - 1)));
|
|
163
|
+
}
|
|
164
|
+
return worst;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
audio::OscillatorNode note(double frequency, audio::OscillatorType type, double start, double stop)
|
|
168
|
+
{
|
|
169
|
+
audio::AudioContext context = audio::AudioSystem::sharedContext();
|
|
170
|
+
audio::OscillatorNode oscillator = context.createOscillator();
|
|
171
|
+
oscillator.setType(type);
|
|
172
|
+
oscillator.frequency.setValue(frequency);
|
|
173
|
+
oscillator.connect(context.destination());
|
|
174
|
+
oscillator.start(start);
|
|
175
|
+
oscillator.stop(stop);
|
|
176
|
+
return oscillator;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
std::string writeTestWav(double frequency, double seconds, int sampleRate)
|
|
180
|
+
{
|
|
181
|
+
const char *tmp = std::getenv("TMPDIR");
|
|
182
|
+
std::string path = (tmp != nullptr && tmp[0] != '\0') ? tmp : "/var/tmp";
|
|
183
|
+
if (path.back() != '/') path.push_back('/');
|
|
184
|
+
path += "gea-apple-audio-selftest.wav";
|
|
185
|
+
|
|
186
|
+
const int frames = static_cast<int>(seconds * sampleRate);
|
|
187
|
+
std::vector<std::int16_t> samples(static_cast<std::size_t>(frames));
|
|
188
|
+
for (int frame = 0; frame < frames; ++frame) {
|
|
189
|
+
const double phase = 2.0 * M_PI * frequency * static_cast<double>(frame) / sampleRate;
|
|
190
|
+
samples[static_cast<std::size_t>(frame)] = static_cast<std::int16_t>(std::sin(phase) * 12000.0);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
std::FILE *file = std::fopen(path.c_str(), "wb");
|
|
194
|
+
if (file == nullptr) return std::string();
|
|
195
|
+
const std::uint32_t dataBytes = static_cast<std::uint32_t>(samples.size() * sizeof(std::int16_t));
|
|
196
|
+
const std::uint32_t riffSize = 36u + dataBytes;
|
|
197
|
+
const std::uint32_t rate = static_cast<std::uint32_t>(sampleRate);
|
|
198
|
+
const std::uint32_t byteRate = rate * 2u;
|
|
199
|
+
const std::uint16_t one = 1;
|
|
200
|
+
const std::uint16_t bits = 16;
|
|
201
|
+
const std::uint16_t blockAlign = 2;
|
|
202
|
+
const std::uint32_t fmtSize = 16;
|
|
203
|
+
std::fwrite("RIFF", 1, 4, file);
|
|
204
|
+
std::fwrite(&riffSize, 4, 1, file);
|
|
205
|
+
std::fwrite("WAVEfmt ", 1, 8, file);
|
|
206
|
+
std::fwrite(&fmtSize, 4, 1, file);
|
|
207
|
+
std::fwrite(&one, 2, 1, file); // PCM
|
|
208
|
+
std::fwrite(&one, 2, 1, file); // mono
|
|
209
|
+
std::fwrite(&rate, 4, 1, file);
|
|
210
|
+
std::fwrite(&byteRate, 4, 1, file);
|
|
211
|
+
std::fwrite(&blockAlign, 2, 1, file);
|
|
212
|
+
std::fwrite(&bits, 2, 1, file);
|
|
213
|
+
std::fwrite("data", 1, 4, file);
|
|
214
|
+
std::fwrite(&dataBytes, 4, 1, file);
|
|
215
|
+
std::fwrite(samples.data(), sizeof(std::int16_t), samples.size(), file);
|
|
216
|
+
std::fclose(file);
|
|
217
|
+
return path;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
} // namespace
|
|
221
|
+
|
|
222
|
+
// GEA_AUDIO_DISABLED=1 puts the backend in exactly the state it reaches when no
|
|
223
|
+
// output device is available: no graph, nothing sounding. Everything must still
|
|
224
|
+
// be callable, and the clock must still advance so an app whose scheduler reads
|
|
225
|
+
// currentTime() keeps making progress instead of wedging.
|
|
226
|
+
int runNoDeviceChecks()
|
|
227
|
+
{
|
|
228
|
+
std::printf("no-device path (GEA_AUDIO_DISABLED=1)\n");
|
|
229
|
+
audio::AudioContext context = audio::AudioSystem::sharedContext();
|
|
230
|
+
const double t0 = context.currentTime();
|
|
231
|
+
|
|
232
|
+
audio::OscillatorNode oscillator = context.createOscillator();
|
|
233
|
+
oscillator.setType(audio::OscillatorType::Square);
|
|
234
|
+
oscillator.frequency.setValue(440.0);
|
|
235
|
+
oscillator.connect(context.destination());
|
|
236
|
+
oscillator.start(t0 + 0.01);
|
|
237
|
+
oscillator.stop(t0 + 0.05);
|
|
238
|
+
|
|
239
|
+
check(static_cast<double>(oscillator.frequency.value()) == 440.0, "frequency still reads back", "440");
|
|
240
|
+
check(oscillator.type() == audio::OscillatorType::Square, "type still reads back", "Square");
|
|
241
|
+
|
|
242
|
+
// A never-drained command ring must not grow or trip: hammer it well past
|
|
243
|
+
// its capacity (this also recycles the oscillator pool many times over).
|
|
244
|
+
for (int index = 0; index < 5000; ++index) {
|
|
245
|
+
audio::OscillatorNode extra = context.createOscillator();
|
|
246
|
+
extra.setType(audio::OscillatorType::Sine);
|
|
247
|
+
extra.frequency.setValue(200.0 + index);
|
|
248
|
+
extra.connect(context.destination());
|
|
249
|
+
extra.start(t0);
|
|
250
|
+
extra.stop(t0 + 0.01);
|
|
251
|
+
}
|
|
252
|
+
audio::OscillatorNode fresh = context.createOscillator();
|
|
253
|
+
fresh.frequency.setValue(123.0);
|
|
254
|
+
check(static_cast<double>(fresh.frequency.value()) == 123.0, "still usable after 5000 oscillators", "123");
|
|
255
|
+
|
|
256
|
+
struct timespec pause = {0, 30 * 1000 * 1000};
|
|
257
|
+
nanosleep(&pause, nullptr);
|
|
258
|
+
const double t1 = context.currentTime();
|
|
259
|
+
check(t1 > t0, "currentTime() still advances (wall clock)", format("%.4f -> %.4f", t0, t1).c_str());
|
|
260
|
+
|
|
261
|
+
const std::int16_t samples[8] = {0, 1000, 2000, 1000, 0, -1000, -2000, -1000};
|
|
262
|
+
check(!audio::AudioSystem::playPcm(samples, 8, 8000, 1), "playPcm reports failure", "false");
|
|
263
|
+
check(!audio::AudioSystem::playFile("/definitely/not/here.wav"), "playFile reports failure", "false");
|
|
264
|
+
audio::AudioSystem::stopPlayback();
|
|
265
|
+
audio::AudioSystem::setVolume(50);
|
|
266
|
+
check(audio::AudioSystem::volume() == 50, "setVolume/volume still work", "50");
|
|
267
|
+
|
|
268
|
+
std::printf("\n%d checks, %d failures\n", g_checks, g_failures);
|
|
269
|
+
return g_failures == 0 ? 0 : 1;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
int main(int argc, char **argv)
|
|
273
|
+
{
|
|
274
|
+
if (argc > 1 && std::strcmp(argv[1], "--no-device") == 0) return runNoDeviceChecks();
|
|
275
|
+
|
|
276
|
+
if (!audio::testing::beginOfflineRendering(kSampleRate)) {
|
|
277
|
+
std::printf("could not enter offline rendering mode\n");
|
|
278
|
+
return 2;
|
|
279
|
+
}
|
|
280
|
+
audio::AudioContext context = audio::AudioSystem::sharedContext();
|
|
281
|
+
audio::AudioSystem::setVolume(100);
|
|
282
|
+
|
|
283
|
+
const double rate = audio::testing::offlineSampleRate();
|
|
284
|
+
std::printf("engine sample rate: %.0f Hz\n\n", rate);
|
|
285
|
+
if (std::fabs(rate - kSampleRate) > 1.0) {
|
|
286
|
+
std::printf("offline engine did not adopt the requested sample rate\n");
|
|
287
|
+
return 2;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------------------------------------------------------------- test 1
|
|
291
|
+
// A scheduled square note: silence before, sound during, silence after, and
|
|
292
|
+
// the fundamental the app asked for.
|
|
293
|
+
std::printf("1. scheduled square note, 440 Hz, 0.05s..0.30s\n");
|
|
294
|
+
{
|
|
295
|
+
const double t0 = context.currentTime();
|
|
296
|
+
audio::OscillatorNode oscillator = note(440.0, audio::OscillatorType::Square, t0 + 0.05, t0 + 0.30);
|
|
297
|
+
check(std::fabs(static_cast<double>(oscillator.frequency.value()) - 440.0) < 1e-9,
|
|
298
|
+
"frequency.value() reads back what was set",
|
|
299
|
+
format("%.3f Hz", static_cast<double>(oscillator.frequency.value())).c_str());
|
|
300
|
+
check(oscillator.type() == audio::OscillatorType::Square, "type() reads back what was set",
|
|
301
|
+
oscillator.type() == audio::OscillatorType::Square ? "Square" : "not Square");
|
|
302
|
+
|
|
303
|
+
const Capture capture = render(0.40);
|
|
304
|
+
const double before = rms(capture, 0.000, 0.045);
|
|
305
|
+
const double during = rms(capture, 0.070, 0.290);
|
|
306
|
+
const double after = rms(capture, 0.320, 0.400);
|
|
307
|
+
const double frequency = measuredFrequency(capture, 0.070, 0.290);
|
|
308
|
+
|
|
309
|
+
check(before < 1e-6, "silence before the scheduled start", format("rms %.9f", before).c_str());
|
|
310
|
+
check(during > 0.05, "non-zero RMS while the note is scheduled", format("rms %.6f", during).c_str());
|
|
311
|
+
check(after < 1e-6, "silence after the scheduled stop", format("rms %.9f", after).c_str());
|
|
312
|
+
check(std::fabs(frequency - 440.0) < 2.0, "measured fundamental matches request",
|
|
313
|
+
format("%.2f Hz vs 440 Hz", frequency).c_str());
|
|
314
|
+
std::printf(" during=%.6f before=%.9f after=%.9f f=%.2f Hz\n", during, before, after, frequency);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ---------------------------------------------------------------- test 2
|
|
318
|
+
// Every waveform sounds, and at the right pitch.
|
|
319
|
+
std::printf("\n2. every OscillatorType sounds at the requested pitch\n");
|
|
320
|
+
{
|
|
321
|
+
struct Case {
|
|
322
|
+
audio::OscillatorType type;
|
|
323
|
+
const char *name;
|
|
324
|
+
double frequency;
|
|
325
|
+
};
|
|
326
|
+
const Case cases[] = {
|
|
327
|
+
{audio::OscillatorType::Sine, "sine", 330.0},
|
|
328
|
+
{audio::OscillatorType::Square, "square", 523.0},
|
|
329
|
+
{audio::OscillatorType::Sawtooth, "sawtooth", 220.0},
|
|
330
|
+
{audio::OscillatorType::Triangle, "triangle", 659.0},
|
|
331
|
+
};
|
|
332
|
+
for (const Case &item : cases) {
|
|
333
|
+
const double t0 = context.currentTime();
|
|
334
|
+
note(item.frequency, item.type, t0 + 0.02, t0 + 0.22);
|
|
335
|
+
const Capture capture = render(0.30);
|
|
336
|
+
const double level = rms(capture, 0.04, 0.21);
|
|
337
|
+
const double frequency = measuredFrequency(capture, 0.04, 0.21);
|
|
338
|
+
check(level > 0.03 && std::fabs(frequency - item.frequency) < 3.0, item.name,
|
|
339
|
+
format("rms %.4f, %.2f Hz vs %.0f Hz", level, frequency, item.frequency).c_str());
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------------------------------------------------------------- test 3
|
|
344
|
+
// Concurrent oscillators mix.
|
|
345
|
+
std::printf("\n3. three concurrent oscillators mix\n");
|
|
346
|
+
{
|
|
347
|
+
const double t0 = context.currentTime();
|
|
348
|
+
note(220.0, audio::OscillatorType::Sine, t0 + 0.02, t0 + 0.25);
|
|
349
|
+
note(440.0, audio::OscillatorType::Sine, t0 + 0.02, t0 + 0.25);
|
|
350
|
+
note(660.0, audio::OscillatorType::Sine, t0 + 0.02, t0 + 0.25);
|
|
351
|
+
const Capture capture = render(0.30);
|
|
352
|
+
const double e220 = energyAt(capture, 220.0, 0.05, 0.24);
|
|
353
|
+
const double e440 = energyAt(capture, 440.0, 0.05, 0.24);
|
|
354
|
+
const double e660 = energyAt(capture, 660.0, 0.05, 0.24);
|
|
355
|
+
const double e1500 = energyAt(capture, 1500.0, 0.05, 0.24);
|
|
356
|
+
check(e220 > 0.05 && e440 > 0.05 && e660 > 0.05 && e1500 < 0.01,
|
|
357
|
+
"all three partials present, nothing at 1500 Hz",
|
|
358
|
+
format("220:%.4f 440:%.4f 660:%.4f 1500:%.5f", e220, e440, e660, e1500).c_str());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------- test 4
|
|
362
|
+
// Scheduling: a note scheduled far ahead does not start early.
|
|
363
|
+
std::printf("\n4. start(t) is honoured, not fired immediately\n");
|
|
364
|
+
{
|
|
365
|
+
const double t0 = context.currentTime();
|
|
366
|
+
note(440.0, audio::OscillatorType::Square, t0 + 0.20, t0 + 0.30);
|
|
367
|
+
const Capture capture = render(0.40);
|
|
368
|
+
const double early = rms(capture, 0.00, 0.19);
|
|
369
|
+
const double late = rms(capture, 0.21, 0.29);
|
|
370
|
+
check(early < 1e-6 && late > 0.05, "silent for 200 ms, then sounds",
|
|
371
|
+
format("first 190ms rms %.9f, then %.6f", early, late).c_str());
|
|
372
|
+
|
|
373
|
+
// The onset lands within a render quantum of the requested time.
|
|
374
|
+
std::size_t onset = 0;
|
|
375
|
+
for (std::size_t frame = 0; frame < capture.frames(); ++frame) {
|
|
376
|
+
if (std::fabs(capture.left(frame)) > 0.01) {
|
|
377
|
+
onset = frame;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const double onsetSeconds = static_cast<double>(onset) / capture.sampleRate;
|
|
382
|
+
check(std::fabs(onsetSeconds - 0.20) < 0.005, "onset within 5 ms of the scheduled time",
|
|
383
|
+
format("onset at %.4f s", onsetSeconds).c_str());
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------- test 5
|
|
387
|
+
// currentTime is the engine's running time.
|
|
388
|
+
std::printf("\n5. currentTime() tracks rendered audio\n");
|
|
389
|
+
{
|
|
390
|
+
const double before = context.currentTime();
|
|
391
|
+
render(0.50);
|
|
392
|
+
const double after = context.currentTime();
|
|
393
|
+
check(std::fabs((after - before) - 0.50) < 0.01, "advanced by exactly the rendered duration",
|
|
394
|
+
format("%.4f s for 0.500 s rendered", after - before).c_str());
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ---------------------------------------------------------------- test 6
|
|
398
|
+
// Volume really changes the output level.
|
|
399
|
+
std::printf("\n6. setVolume() changes the output level\n");
|
|
400
|
+
{
|
|
401
|
+
audio::AudioSystem::setVolume(100);
|
|
402
|
+
double t0 = context.currentTime();
|
|
403
|
+
note(440.0, audio::OscillatorType::Square, t0 + 0.02, t0 + 0.20);
|
|
404
|
+
const double loud = rms(render(0.25), 0.05, 0.18);
|
|
405
|
+
|
|
406
|
+
audio::AudioSystem::setVolume(25);
|
|
407
|
+
t0 = context.currentTime();
|
|
408
|
+
note(440.0, audio::OscillatorType::Square, t0 + 0.02, t0 + 0.20);
|
|
409
|
+
const double quiet = rms(render(0.25), 0.05, 0.18);
|
|
410
|
+
|
|
411
|
+
audio::AudioSystem::setVolume(0);
|
|
412
|
+
t0 = context.currentTime();
|
|
413
|
+
note(440.0, audio::OscillatorType::Square, t0 + 0.02, t0 + 0.20);
|
|
414
|
+
const double muted = rms(render(0.25), 0.05, 0.18);
|
|
415
|
+
|
|
416
|
+
audio::AudioSystem::setVolume(100);
|
|
417
|
+
check(std::fabs(loud / quiet - 4.0) < 0.2, "volume 100 is 4x volume 25",
|
|
418
|
+
format("%.6f / %.6f = %.3f", loud, quiet, loud / quiet).c_str());
|
|
419
|
+
check(muted < 1e-9, "volume 0 is silence", format("rms %.9f", muted).c_str());
|
|
420
|
+
check(audio::AudioSystem::volume() == 100, "volume() reads back", format("%d", audio::AudioSystem::volume()).c_str());
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ---------------------------------------------------------------- test 7
|
|
424
|
+
// No clicks: a sine note's biggest sample-to-sample step stays at the
|
|
425
|
+
// waveform's own slope, at the onset and at the release.
|
|
426
|
+
std::printf("\n7. no click at note start or stop\n");
|
|
427
|
+
{
|
|
428
|
+
const double frequency = 440.0;
|
|
429
|
+
const double t0 = context.currentTime();
|
|
430
|
+
note(frequency, audio::OscillatorType::Sine, t0 + 0.05, t0 + 0.20);
|
|
431
|
+
const Capture capture = render(0.30);
|
|
432
|
+
const double amplitude = peak(capture, 0.06, 0.19);
|
|
433
|
+
const double slope = 2.0 * M_PI * frequency / capture.sampleRate * amplitude;
|
|
434
|
+
const double worst = maxDelta(capture, 0.00, 0.30);
|
|
435
|
+
check(worst < slope * 1.5, "max sample step is the waveform's own slope",
|
|
436
|
+
format("step %.6f vs slope %.6f", worst, slope).c_str());
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ---------------------------------------------------------------- test 8
|
|
440
|
+
// A rapid burst of short notes (button-tetris' worst case) all sound.
|
|
441
|
+
std::printf("\n8. rapid short notes (button-tetris burst)\n");
|
|
442
|
+
{
|
|
443
|
+
const double t0 = context.currentTime();
|
|
444
|
+
for (int index = 0; index < 12; ++index) {
|
|
445
|
+
const double start = t0 + 0.02 + 0.03 * index;
|
|
446
|
+
note(280.0 + 40.0 * index, audio::OscillatorType::Square, start, start + 0.024);
|
|
447
|
+
}
|
|
448
|
+
const Capture capture = render(0.45);
|
|
449
|
+
int sounded = 0;
|
|
450
|
+
for (int index = 0; index < 12; ++index) {
|
|
451
|
+
const double start = 0.02 + 0.03 * index;
|
|
452
|
+
if (rms(capture, start + 0.004, start + 0.020) > 0.05) ++sounded;
|
|
453
|
+
}
|
|
454
|
+
check(sounded == 12, "all 12 notes sounded", format("%d/12", sounded).c_str());
|
|
455
|
+
check(maxDelta(capture, 0.0, 0.45) <= 2.0 * peak(capture, 0.0, 0.45) + 1e-6,
|
|
456
|
+
"no step larger than a full square transition",
|
|
457
|
+
format("step %.4f, peak %.4f", maxDelta(capture, 0.0, 0.45), peak(capture, 0.0, 0.45)).c_str());
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ---------------------------------------------------------------- test 9
|
|
461
|
+
// setValueAtTime schedules.
|
|
462
|
+
std::printf("\n9. setValueAtTime() schedules a frequency change\n");
|
|
463
|
+
{
|
|
464
|
+
const double t0 = context.currentTime();
|
|
465
|
+
audio::OscillatorNode oscillator = context.createOscillator();
|
|
466
|
+
oscillator.setType(audio::OscillatorType::Sine);
|
|
467
|
+
oscillator.frequency.setValue(300.0);
|
|
468
|
+
oscillator.frequency.setValueAtTime(900.0, t0 + 0.15);
|
|
469
|
+
oscillator.connect(context.destination());
|
|
470
|
+
oscillator.start(t0 + 0.02);
|
|
471
|
+
oscillator.stop(t0 + 0.28);
|
|
472
|
+
const Capture capture = render(0.35);
|
|
473
|
+
const double first = measuredFrequency(capture, 0.05, 0.14);
|
|
474
|
+
const double second = measuredFrequency(capture, 0.17, 0.27);
|
|
475
|
+
check(std::fabs(first - 300.0) < 4.0 && std::fabs(second - 900.0) < 6.0,
|
|
476
|
+
"300 Hz before the event, 900 Hz after",
|
|
477
|
+
format("%.1f Hz then %.1f Hz", first, second).c_str());
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// -------------------------------------------------------------- test 9.5
|
|
481
|
+
// connect() after start()/stop() still produces the note (Web Audio allows
|
|
482
|
+
// the graph to be wired up after the schedule is set).
|
|
483
|
+
std::printf("\n9b. connect() after start()/stop()\n");
|
|
484
|
+
{
|
|
485
|
+
const double t0 = context.currentTime();
|
|
486
|
+
audio::OscillatorNode oscillator = context.createOscillator();
|
|
487
|
+
oscillator.setType(audio::OscillatorType::Square);
|
|
488
|
+
oscillator.frequency.setValue(600.0);
|
|
489
|
+
oscillator.start(t0 + 0.02);
|
|
490
|
+
oscillator.stop(t0 + 0.16);
|
|
491
|
+
oscillator.connect(context.destination());
|
|
492
|
+
const Capture capture = render(0.25);
|
|
493
|
+
const double level = rms(capture, 0.04, 0.15);
|
|
494
|
+
const double after = rms(capture, 0.19, 0.25);
|
|
495
|
+
const double frequency = measuredFrequency(capture, 0.04, 0.15);
|
|
496
|
+
check(level > 0.05 && after < 1e-6 && std::fabs(frequency - 600.0) < 4.0,
|
|
497
|
+
"late connect still sounds, and still stops",
|
|
498
|
+
format("rms %.4f then %.9f, %.1f Hz", level, after, frequency).c_str());
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// --------------------------------------------------------------- test 10
|
|
502
|
+
// playPcm.
|
|
503
|
+
std::printf("\n10. AudioSystem::playPcm\n");
|
|
504
|
+
{
|
|
505
|
+
const int sampleRate = 44100;
|
|
506
|
+
const int frames = sampleRate / 4;
|
|
507
|
+
std::vector<std::int16_t> samples(static_cast<std::size_t>(frames));
|
|
508
|
+
for (int frame = 0; frame < frames; ++frame) {
|
|
509
|
+
const double phase = 2.0 * M_PI * 500.0 * static_cast<double>(frame) / sampleRate;
|
|
510
|
+
samples[static_cast<std::size_t>(frame)] = static_cast<std::int16_t>(std::sin(phase) * 20000.0);
|
|
511
|
+
}
|
|
512
|
+
const bool played = audio::AudioSystem::playPcm(samples.data(), samples.size(), sampleRate, 1);
|
|
513
|
+
const Capture capture = render(0.20);
|
|
514
|
+
const double level = rms(capture, 0.02, 0.18);
|
|
515
|
+
const double frequency = measuredFrequency(capture, 0.02, 0.18);
|
|
516
|
+
check(played, "playPcm reported success", played ? "true" : "false");
|
|
517
|
+
check(level > 0.02, "PCM buffer produced output", format("rms %.6f", level).c_str());
|
|
518
|
+
check(std::fabs(frequency - 500.0) < 5.0, "PCM played back at the right rate",
|
|
519
|
+
format("%.2f Hz vs 500 Hz", frequency).c_str());
|
|
520
|
+
audio::AudioSystem::stopPlayback();
|
|
521
|
+
const double afterStop = rms(render(0.10), 0.02, 0.10);
|
|
522
|
+
check(afterStop < 1e-6, "stopPlayback() silenced it", format("rms %.9f", afterStop).c_str());
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// --------------------------------------------------------------- test 11
|
|
526
|
+
// playFile.
|
|
527
|
+
std::printf("\n11. AudioSystem::playFile\n");
|
|
528
|
+
{
|
|
529
|
+
const std::string path = writeTestWav(700.0, 0.4, 44100);
|
|
530
|
+
if (path.empty()) {
|
|
531
|
+
check(false, "could not write the test wav", "");
|
|
532
|
+
} else {
|
|
533
|
+
const bool played = audio::AudioSystem::playFile(path);
|
|
534
|
+
const Capture capture = render(0.20);
|
|
535
|
+
const double level = rms(capture, 0.02, 0.18);
|
|
536
|
+
const double frequency = measuredFrequency(capture, 0.02, 0.18);
|
|
537
|
+
check(played, "playFile reported success", played ? "true" : "false");
|
|
538
|
+
check(level > 0.02, "file produced output", format("rms %.6f", level).c_str());
|
|
539
|
+
check(std::fabs(frequency - 700.0) < 6.0, "file played at the right pitch",
|
|
540
|
+
format("%.2f Hz vs 700 Hz", frequency).c_str());
|
|
541
|
+
audio::AudioSystem::stopPlayback();
|
|
542
|
+
check(!audio::AudioSystem::playFile("/definitely/not/here.wav"), "missing file reports failure", "false");
|
|
543
|
+
std::remove(path.c_str());
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// --------------------------------------------------------------- test 12
|
|
548
|
+
// The oscillator pool recycles: an app that creates one oscillator per note
|
|
549
|
+
// forever (button-tetris does) must keep sounding indefinitely.
|
|
550
|
+
std::printf("\n12. 1500 sequential notes (oscillator pool recycles)\n");
|
|
551
|
+
{
|
|
552
|
+
int silent = 0;
|
|
553
|
+
double worstRms = 1.0;
|
|
554
|
+
for (int index = 0; index < 1500; ++index) {
|
|
555
|
+
const double t0 = context.currentTime();
|
|
556
|
+
note(440.0, audio::OscillatorType::Square, t0 + 0.004, t0 + 0.020);
|
|
557
|
+
const Capture capture = render(0.030);
|
|
558
|
+
const double level = rms(capture, 0.008, 0.018);
|
|
559
|
+
if (level < 0.05) ++silent;
|
|
560
|
+
worstRms = std::fmin(worstRms, level);
|
|
561
|
+
}
|
|
562
|
+
check(silent == 0, "every one of 1500 notes sounded",
|
|
563
|
+
format("%d silent, worst rms %.5f", silent, worstRms).c_str());
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// --------------------------------------------------------------- test 13
|
|
567
|
+
// The app thread mutates oscillator state while the graph renders. Build this
|
|
568
|
+
// with -fsanitize=thread (build-audio-selftest.sh --tsan) to check the
|
|
569
|
+
// lock-free command ring's memory ordering rather than just its behaviour.
|
|
570
|
+
std::printf("\n13. concurrent mutation while rendering\n");
|
|
571
|
+
{
|
|
572
|
+
std::atomic<bool> stop{false};
|
|
573
|
+
std::atomic<int> created{0};
|
|
574
|
+
std::thread mutator([&] {
|
|
575
|
+
audio::AudioContext local = audio::AudioSystem::sharedContext();
|
|
576
|
+
while (!stop.load()) {
|
|
577
|
+
const double now = local.currentTime();
|
|
578
|
+
audio::OscillatorNode oscillator = local.createOscillator();
|
|
579
|
+
oscillator.setType(audio::OscillatorType::Sine);
|
|
580
|
+
oscillator.frequency.setValue(300.0 + (created.load() % 400));
|
|
581
|
+
oscillator.connect(local.destination());
|
|
582
|
+
oscillator.start(now + 0.005);
|
|
583
|
+
oscillator.frequency.setValue(500.0);
|
|
584
|
+
oscillator.stop(now + 0.030);
|
|
585
|
+
(void)oscillator.frequency.value();
|
|
586
|
+
(void)oscillator.type();
|
|
587
|
+
created.fetch_add(1);
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
double loudest = 0.0;
|
|
591
|
+
for (int block = 0; block < 40; ++block) loudest = std::fmax(loudest, rms(render(0.02), 0.0, 0.02));
|
|
592
|
+
stop.store(true);
|
|
593
|
+
mutator.join();
|
|
594
|
+
check(created.load() > 0 && loudest > 0.0, "no crash, and audio kept flowing",
|
|
595
|
+
format("%d oscillators, loudest block rms %.5f", created.load(), loudest).c_str());
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
audio::testing::endOfflineRendering();
|
|
599
|
+
|
|
600
|
+
std::printf("\n%d checks, %d failures\n", g_checks, g_failures);
|
|
601
|
+
return g_failures == 0 ? 0 : 1;
|
|
602
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#pragma once
|
|
3
|
+
|
|
4
|
+
#include <cstddef>
|
|
5
|
+
|
|
6
|
+
// Offline-rendering hooks for the Apple audio backend (targets/shared/apple_audio.mm).
|
|
7
|
+
//
|
|
8
|
+
// AVAudioEngine's manual-rendering mode drives the exact same graph the shipping
|
|
9
|
+
// build uses — source node, scheduler, envelope, mixer — without opening an
|
|
10
|
+
// output device. That makes the backend measurable (RMS, frequency) on a machine
|
|
11
|
+
// with no audio hardware and in CI, and it is what
|
|
12
|
+
// targets/shared/apple_audio_selftest.mm uses to prove the backend really
|
|
13
|
+
// synthesises sound.
|
|
14
|
+
//
|
|
15
|
+
// beginOfflineRendering() must be called before any other audio call: the engine
|
|
16
|
+
// graph is built lazily on first use and its mode is fixed at that point.
|
|
17
|
+
|
|
18
|
+
namespace gea::platform::audio::testing {
|
|
19
|
+
|
|
20
|
+
// Switches the (not yet started) engine into offline manual-rendering mode.
|
|
21
|
+
// Returns false if the engine is already running or the mode could not be set.
|
|
22
|
+
bool beginOfflineRendering(double sample_rate);
|
|
23
|
+
|
|
24
|
+
// Pulls `frames` frames from the engine into `interleaved_stereo`
|
|
25
|
+
// (2 floats per frame). Returns the number of frames actually rendered.
|
|
26
|
+
std::size_t renderOfflineFrames(float *interleaved_stereo, std::size_t frames);
|
|
27
|
+
|
|
28
|
+
// Sample rate the offline engine is actually running at.
|
|
29
|
+
double offlineSampleRate();
|
|
30
|
+
|
|
31
|
+
// Stops the engine and leaves manual-rendering mode.
|
|
32
|
+
void endOfflineRendering();
|
|
33
|
+
|
|
34
|
+
} // namespace gea::platform::audio::testing
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Builds and runs the Apple audio backend self-test (apple_audio_selftest.mm).
|
|
3
|
+
#
|
|
4
|
+
# It compiles the SHIPPING backend (apple_audio.mm — the same translation unit
|
|
5
|
+
# the macOS and iOS targets build) against AVFoundation and renders it offline,
|
|
6
|
+
# so what it measures is the real synth, scheduler and mixer, not a stand-in.
|
|
7
|
+
#
|
|
8
|
+
# Output goes to $GEA_AUDIO_SELFTEST_OUT (default: a scratch dir outside the repo
|
|
9
|
+
# trees), never to /tmp-only state that matters.
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
SHARED_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
APPLE_DIR="$(cd "$SHARED_DIR/../.." && pwd)"
|
|
14
|
+
# @geastack/core comes from node_modules, the way node resolves it.
|
|
15
|
+
resolve_core() {
|
|
16
|
+
node -e "process.stdout.write(require('path').dirname(require.resolve('@geastack/core/package.json',{paths:[process.argv[1],process.cwd()]})))" "$1" 2>/dev/null || true
|
|
17
|
+
}
|
|
18
|
+
GEA_CORE_DIR="${GEA_CORE_DIR:-$(resolve_core "$APPLE_DIR")}"
|
|
19
|
+
[ -n "$GEA_CORE_DIR" ] && [ -d "$GEA_CORE_DIR/include" ] || {
|
|
20
|
+
echo "Cannot resolve @geastack/core — run \`npm install\` in $APPLE_DIR, or set GEA_CORE_DIR" >&2
|
|
21
|
+
exit 1
|
|
22
|
+
}
|
|
23
|
+
AUDIO_INCLUDE="$GEA_CORE_DIR/include"
|
|
24
|
+
# Build output belongs to the caller, not to a sibling repo's working copy.
|
|
25
|
+
OUT_DIR="${GEA_AUDIO_SELFTEST_OUT:-$APPLE_DIR/dist/apple-audio-selftest}"
|
|
26
|
+
|
|
27
|
+
mkdir -p "$OUT_DIR"
|
|
28
|
+
|
|
29
|
+
# --tsan builds with ThreadSanitizer, which checks the lock-free command ring's
|
|
30
|
+
# memory ordering rather than only its observable behaviour.
|
|
31
|
+
SANITIZE=()
|
|
32
|
+
if [[ "${1:-}" == "--tsan" ]]; then
|
|
33
|
+
SANITIZE=(-fsanitize=thread -g)
|
|
34
|
+
shift
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
clang++ -std=c++20 -ObjC++ -fobjc-arc -O2 -Wall -Wextra \
|
|
38
|
+
${SANITIZE[@]+"${SANITIZE[@]}"} \
|
|
39
|
+
-I"$AUDIO_INCLUDE" -I"$SHARED_DIR" \
|
|
40
|
+
-framework AVFoundation -framework Foundation -framework AudioToolbox \
|
|
41
|
+
"$SHARED_DIR/apple_audio.mm" "$SHARED_DIR/apple_audio_selftest.mm" \
|
|
42
|
+
-o "$OUT_DIR/apple-audio-selftest"
|
|
43
|
+
|
|
44
|
+
echo "built $OUT_DIR/apple-audio-selftest"
|
|
45
|
+
echo
|
|
46
|
+
|
|
47
|
+
"$OUT_DIR/apple-audio-selftest" "$@"
|
|
48
|
+
|
|
49
|
+
echo
|
|
50
|
+
GEA_AUDIO_DISABLED=1 "$OUT_DIR/apple-audio-selftest" --no-device
|