@opendaw/studio-core-wasm 0.0.2
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/dist/WasmEngine.d.ts +14 -0
- package/dist/WasmEngine.d.ts.map +1 -0
- package/dist/WasmEngine.js +152 -0
- package/dist/boot.d.ts +14 -0
- package/dist/boot.d.ts.map +1 -0
- package/dist/boot.js +91 -0
- package/dist/device-linker.d.ts +47 -0
- package/dist/device-linker.d.ts.map +1 -0
- package/dist/device-linker.js +109 -0
- package/dist/engine-exports.d.ts +86 -0
- package/dist/engine-exports.d.ts.map +1 -0
- package/dist/engine-exports.js +13 -0
- package/dist/engine-modules.d.ts +26 -0
- package/dist/engine-modules.d.ts.map +1 -0
- package/dist/engine-modules.js +62 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/nam-bridge.d.ts +7 -0
- package/dist/nam-bridge.d.ts.map +1 -0
- package/dist/nam-bridge.js +265 -0
- package/dist/nam-loader.d.ts +4 -0
- package/dist/nam-loader.d.ts.map +1 -0
- package/dist/nam-loader.js +1 -0
- package/dist/protocol.d.ts +22 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +3 -0
- package/dist/script-bridge.d.ts +11 -0
- package/dist/script-bridge.d.ts.map +1 -0
- package/dist/script-bridge.js +343 -0
- package/dist/script-spielwerk.d.ts +24 -0
- package/dist/script-spielwerk.d.ts.map +1 -0
- package/dist/script-spielwerk.js +179 -0
- package/dist/soundfont-simplify.d.ts +28 -0
- package/dist/soundfont-simplify.d.ts.map +1 -0
- package/dist/soundfont-simplify.js +143 -0
- package/dist/sync/loopback.d.ts +8 -0
- package/dist/sync/loopback.d.ts.map +1 -0
- package/dist/sync/loopback.js +23 -0
- package/dist/sync/serialize-update-tasks.d.ts +4 -0
- package/dist/sync/serialize-update-tasks.d.ts.map +1 -0
- package/dist/sync/serialize-update-tasks.js +40 -0
- package/dist/utf8.d.ts +2 -0
- package/dist/utf8.d.ts.map +1 -0
- package/dist/utf8.js +19 -0
- package/dist/wasm/engine.wasm +0 -0
- package/dist/wasm/plugins/device_apparat.wasm +0 -0
- package/dist/wasm/plugins/device_arpeggio.wasm +0 -0
- package/dist/wasm/plugins/device_compressor.wasm +0 -0
- package/dist/wasm/plugins/device_crusher.wasm +0 -0
- package/dist/wasm/plugins/device_dattorro_reverb.wasm +0 -0
- package/dist/wasm/plugins/device_delay.wasm +0 -0
- package/dist/wasm/plugins/device_fold.wasm +0 -0
- package/dist/wasm/plugins/device_gate.wasm +0 -0
- package/dist/wasm/plugins/device_maximizer.wasm +0 -0
- package/dist/wasm/plugins/device_nano.wasm +0 -0
- package/dist/wasm/plugins/device_neural_amp.wasm +0 -0
- package/dist/wasm/plugins/device_pitch.wasm +0 -0
- package/dist/wasm/plugins/device_playfield_sample.wasm +0 -0
- package/dist/wasm/plugins/device_revamp.wasm +0 -0
- package/dist/wasm/plugins/device_reverb.wasm +0 -0
- package/dist/wasm/plugins/device_soundfont.wasm +0 -0
- package/dist/wasm/plugins/device_spielwerk.wasm +0 -0
- package/dist/wasm/plugins/device_stereo_tool.wasm +0 -0
- package/dist/wasm/plugins/device_tidal.wasm +0 -0
- package/dist/wasm/plugins/device_vaporisateur.wasm +0 -0
- package/dist/wasm/plugins/device_velocity.wasm +0 -0
- package/dist/wasm/plugins/device_vocoder.wasm +0 -0
- package/dist/wasm/plugins/device_waveshaper.wasm +0 -0
- package/dist/wasm/plugins/device_werkstatt.wasm +0 -0
- package/dist/wasm/plugins/device_zeitgeist.wasm +0 -0
- package/dist/wasm-offline-worker.js +97 -0
- package/dist/wasm-offline-worker.js.map +7 -0
- package/dist/wasm-processor.js +97 -0
- package/dist/wasm-processor.js.map +7 -0
- package/dist/worklet-scope.d.ts +2 -0
- package/dist/worklet-scope.d.ts.map +1 -0
- package/dist/worklet-scope.js +10 -0
- package/package.json +53 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Builds the SIMPLIFIED soundfont BLOB the wasm engine consumes (see crates/stock-devices/device-soundfont/
|
|
2
|
+
// src/blob.rs — the format is a WASM CONTRACT mirrored on both sides). The main thread keeps the parsed SF2;
|
|
3
|
+
// this flattens it into a compact, directly-indexable structure so the wasm never parses SF2/RIFF.
|
|
4
|
+
//
|
|
5
|
+
// `encodeSoundfont` serializes a plain description (samples + per-preset regions) into the blob. `simplify-
|
|
6
|
+
// Soundfont` walks a parsed `SoundFont2` into that description, resolving every SF2 generator EXACTLY as the TS
|
|
7
|
+
// `SoundfontVoice` does: preset-zone × instrument-zone flattening, instrument-overrides-preset precedence,
|
|
8
|
+
// timecent→seconds envelope conversion, sustain 1−x/1000, root-key/loop/pan resolution.
|
|
9
|
+
// SF2 generator IDs (spec-fixed), the only ones the TS `SoundfontVoice` honors. Inlined to avoid a brittle
|
|
10
|
+
// cross-package deep import; they are a frozen contract.
|
|
11
|
+
const GeneratorType = {
|
|
12
|
+
Pan: 17,
|
|
13
|
+
AttackVolEnv: 34,
|
|
14
|
+
DecayVolEnv: 36,
|
|
15
|
+
SustainVolEnv: 37,
|
|
16
|
+
ReleaseVolEnv: 38,
|
|
17
|
+
KeyRange: 43,
|
|
18
|
+
VelRange: 44,
|
|
19
|
+
CoarseTune: 51,
|
|
20
|
+
FineTune: 52,
|
|
21
|
+
SampleId: 53,
|
|
22
|
+
SampleModes: 54,
|
|
23
|
+
OverridingRootKey: 58
|
|
24
|
+
};
|
|
25
|
+
// ---- Binary layout (mirrors blob.rs) -----------------------------------------------------------------------
|
|
26
|
+
const MAGIC = 0x4F53_4632; // "OSF2"
|
|
27
|
+
const HEADER_BYTES = 32;
|
|
28
|
+
const SAMPLE_STRIDE = 24;
|
|
29
|
+
const REGION_STRIDE = 40;
|
|
30
|
+
const PRESET_STRIDE = 8;
|
|
31
|
+
export const encodeSoundfont = ({ samples, presets }) => {
|
|
32
|
+
const regionCount = presets.reduce((sum, regions) => sum + regions.length, 0);
|
|
33
|
+
const samplesOff = HEADER_BYTES;
|
|
34
|
+
const regionsOff = samplesOff + samples.length * SAMPLE_STRIDE;
|
|
35
|
+
const presetsOff = regionsOff + regionCount * REGION_STRIDE;
|
|
36
|
+
const pcmOff = presetsOff + presets.length * PRESET_STRIDE;
|
|
37
|
+
const pcmFrames = samples.reduce((sum, sample) => sum + sample.pcm.length, 0);
|
|
38
|
+
const buffer = new ArrayBuffer(pcmOff + pcmFrames * 4);
|
|
39
|
+
const view = new DataView(buffer);
|
|
40
|
+
view.setUint32(0, MAGIC, true);
|
|
41
|
+
view.setUint32(4, 1, true); // version
|
|
42
|
+
view.setUint32(8, samples.length, true);
|
|
43
|
+
view.setUint32(12, regionCount, true);
|
|
44
|
+
view.setUint32(16, presets.length, true);
|
|
45
|
+
view.setUint32(20, samplesOff, true);
|
|
46
|
+
view.setUint32(24, regionsOff, true);
|
|
47
|
+
view.setUint32(28, presetsOff, true);
|
|
48
|
+
// Sample table + PCM (concatenated planes).
|
|
49
|
+
let pcmCursor = pcmOff;
|
|
50
|
+
samples.forEach((sample, index) => {
|
|
51
|
+
const base = samplesOff + index * SAMPLE_STRIDE;
|
|
52
|
+
view.setUint32(base, pcmCursor, true);
|
|
53
|
+
view.setUint32(base + 4, sample.pcm.length, true);
|
|
54
|
+
view.setFloat32(base + 8, sample.sampleRate, true);
|
|
55
|
+
view.setUint32(base + 12, sample.loopStart, true);
|
|
56
|
+
view.setUint32(base + 16, sample.loopEnd, true);
|
|
57
|
+
new Float32Array(buffer, pcmCursor, sample.pcm.length).set(sample.pcm);
|
|
58
|
+
pcmCursor += sample.pcm.length * 4;
|
|
59
|
+
});
|
|
60
|
+
// Region table (all presets flattened) + preset table (each preset's [start, count) range).
|
|
61
|
+
let regionCursor = 0;
|
|
62
|
+
presets.forEach((regions, presetIndex) => {
|
|
63
|
+
const presetBase = presetsOff + presetIndex * PRESET_STRIDE;
|
|
64
|
+
view.setUint32(presetBase, regionCursor, true);
|
|
65
|
+
view.setUint32(presetBase + 4, regions.length, true);
|
|
66
|
+
for (const region of regions) {
|
|
67
|
+
const base = regionsOff + regionCursor * REGION_STRIDE;
|
|
68
|
+
view.setUint8(base, region.keyLo);
|
|
69
|
+
view.setUint8(base + 1, region.keyHi);
|
|
70
|
+
view.setUint8(base + 2, region.velLo);
|
|
71
|
+
view.setUint8(base + 3, region.velHi);
|
|
72
|
+
view.setUint32(base + 4, region.sampleIndex, true);
|
|
73
|
+
view.setUint32(base + 8, region.rootKey, true);
|
|
74
|
+
view.setUint32(base + 12, region.loopMode, true);
|
|
75
|
+
view.setFloat32(base + 16, region.pan, true);
|
|
76
|
+
view.setFloat32(base + 20, region.attack, true);
|
|
77
|
+
view.setFloat32(base + 24, region.decay, true);
|
|
78
|
+
view.setFloat32(base + 28, region.sustain, true);
|
|
79
|
+
view.setFloat32(base + 32, region.release, true);
|
|
80
|
+
regionCursor++;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return buffer;
|
|
84
|
+
};
|
|
85
|
+
const numeric = (zone, type) => zone.generators[type]?.value;
|
|
86
|
+
// instrument-overrides-preset, matching TS `getCombinedGenerator`.
|
|
87
|
+
const combined = (preset, inst, type) => numeric(inst, type) ?? numeric(preset, type);
|
|
88
|
+
const timecentsToSeconds = (value) => value !== undefined ? Math.pow(2.0, value / 1200.0) : 0.005;
|
|
89
|
+
const rangeOf = (zone, type) => zone.generators[type]?.range ?? { lo: 0, hi: 127 };
|
|
90
|
+
export const simplifySoundfont = (soundfont) => {
|
|
91
|
+
const samples = [];
|
|
92
|
+
const sampleIndexByRef = new Map();
|
|
93
|
+
const internSample = (sample) => {
|
|
94
|
+
const existing = sampleIndexByRef.get(sample);
|
|
95
|
+
if (existing !== undefined) {
|
|
96
|
+
return existing;
|
|
97
|
+
}
|
|
98
|
+
const pcm = new Float32Array(sample.data.length);
|
|
99
|
+
for (let i = 0; i < pcm.length; i++) {
|
|
100
|
+
pcm[i] = sample.data[i] / 32768.0;
|
|
101
|
+
}
|
|
102
|
+
const index = samples.length;
|
|
103
|
+
samples.push({ pcm, sampleRate: sample.header.sampleRate, loopStart: sample.header.startLoop, loopEnd: sample.header.endLoop });
|
|
104
|
+
sampleIndexByRef.set(sample, index);
|
|
105
|
+
return index;
|
|
106
|
+
};
|
|
107
|
+
const presets = soundfont.presets.map(preset => {
|
|
108
|
+
const regions = [];
|
|
109
|
+
for (const presetZone of preset.zones) {
|
|
110
|
+
const presetKey = rangeOf(presetZone, GeneratorType.KeyRange);
|
|
111
|
+
const presetVel = rangeOf(presetZone, GeneratorType.VelRange);
|
|
112
|
+
for (const instZone of presetZone.instrument.zones) {
|
|
113
|
+
const instKey = rangeOf(instZone, GeneratorType.KeyRange);
|
|
114
|
+
const instVel = rangeOf(instZone, GeneratorType.VelRange);
|
|
115
|
+
// A note matches iff it is inside BOTH zones' ranges (TS tests each independently) = the intersection.
|
|
116
|
+
const keyLo = Math.max(presetKey.lo, instKey.lo), keyHi = Math.min(presetKey.hi, instKey.hi);
|
|
117
|
+
const velLo = Math.max(presetVel.lo, instVel.lo), velHi = Math.min(presetVel.hi, instVel.hi);
|
|
118
|
+
if (keyLo > keyHi || velLo > velHi) {
|
|
119
|
+
continue;
|
|
120
|
+
} // empty intersection: unreachable, drop it
|
|
121
|
+
const sample = instZone.sample ?? soundfont.samples[numeric(instZone, GeneratorType.SampleId) ?? 0];
|
|
122
|
+
if (sample === undefined) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const sampleIndex = internSample(sample);
|
|
126
|
+
const rootKey = numeric(instZone, GeneratorType.OverridingRootKey) ?? sample.header.originalPitch ?? 60;
|
|
127
|
+
const sampleModes = combined(presetZone, instZone, GeneratorType.SampleModes) ?? 0;
|
|
128
|
+
const sustain = combined(presetZone, instZone, GeneratorType.SustainVolEnv);
|
|
129
|
+
regions.push({
|
|
130
|
+
keyLo, keyHi, velLo, velHi, sampleIndex, rootKey,
|
|
131
|
+
loopMode: sampleModes === 1 || sampleModes === 3 ? 1 : 0,
|
|
132
|
+
pan: (combined(presetZone, instZone, GeneratorType.Pan) ?? 0) / 1000.0,
|
|
133
|
+
attack: timecentsToSeconds(combined(presetZone, instZone, GeneratorType.AttackVolEnv)),
|
|
134
|
+
decay: timecentsToSeconds(combined(presetZone, instZone, GeneratorType.DecayVolEnv)),
|
|
135
|
+
sustain: 1.0 - (sustain ?? 0.0) / 1000.0,
|
|
136
|
+
release: timecentsToSeconds(combined(presetZone, instZone, GeneratorType.ReleaseVolEnv))
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return regions;
|
|
141
|
+
});
|
|
142
|
+
return encodeSoundfont({ samples, presets });
|
|
143
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Terminable } from "@opendaw/lib-std";
|
|
2
|
+
import { Messenger } from "@opendaw/lib-runtime";
|
|
3
|
+
export type SyncLoopback = {
|
|
4
|
+
source: Messenger;
|
|
5
|
+
target: Messenger;
|
|
6
|
+
} & Terminable;
|
|
7
|
+
export declare const createSyncLoopback: () => SyncLoopback;
|
|
8
|
+
//# sourceMappingURL=loopback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loopback.d.ts","sourceRoot":"","sources":["../../src/sync/loopback.ts"],"names":[],"mappings":"AAIA,OAAO,EAA0C,UAAU,EAAC,MAAM,kBAAkB,CAAA;AACpF,OAAO,EAAC,SAAS,EAAC,MAAM,sBAAsB,CAAA;AAE9C,MAAM,MAAM,YAAY,GAAG;IAAC,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,SAAS,CAAA;CAAC,GAAG,UAAU,CAAA;AAE9E,eAAO,MAAM,kBAAkB,QAAO,YAiBrC,CAAA"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// A synchronous in-process Messenger pair: `send` on one side delivers to the other side's subscribers
|
|
2
|
+
// within the SAME call stack. SyncSource's update tasks must be serialized AT emission time — a
|
|
3
|
+
// MessageChannel loopback defers by a macrotask, letting a later transaction delete boxes before the
|
|
4
|
+
// earlier batch resolves its primitive-field codecs against the live source graph.
|
|
5
|
+
import { Notifier, panic } from "@opendaw/lib-std";
|
|
6
|
+
export const createSyncLoopback = () => {
|
|
7
|
+
const toTarget = new Notifier();
|
|
8
|
+
const toSource = new Notifier();
|
|
9
|
+
const port = (outgoing, incoming) => ({
|
|
10
|
+
send: (message) => outgoing.notify(message),
|
|
11
|
+
channel: () => panic("the sync loopback has no channels"),
|
|
12
|
+
subscribe: (observer) => incoming.subscribe(observer),
|
|
13
|
+
terminate: () => { }
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
source: port(toTarget, toSource),
|
|
17
|
+
target: port(toSource, toTarget),
|
|
18
|
+
terminate: () => {
|
|
19
|
+
toTarget.terminate();
|
|
20
|
+
toSource.terminate();
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { BoxGraph, UpdateTask } from "@opendaw/lib-box";
|
|
2
|
+
import { BoxIO } from "@opendaw/studio-boxes";
|
|
3
|
+
export declare const serializeUpdateTasks: (tasks: ReadonlyArray<UpdateTask<BoxIO.TypeMap>>, source: BoxGraph<BoxIO.TypeMap>) => ArrayBuffer;
|
|
4
|
+
//# sourceMappingURL=serialize-update-tasks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialize-update-tasks.d.ts","sourceRoot":"","sources":["../../src/sync/serialize-update-tasks.ts"],"names":[],"mappings":"AAKA,OAAO,EAAU,QAAQ,EAAmC,UAAU,EAAC,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAAC,KAAK,EAAC,MAAM,uBAAuB,CAAA;AAE3C,eAAO,MAAM,oBAAoB,GAC7B,OAAO,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,EAC/C,QAAQ,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,KAAG,WA8BrC,CAAA"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Serialize SyncSource's forward-only UpdateTask[] into the byte stream the Rust engine's
|
|
2
|
+
// decode_forward consumes. Primitive value types are resolved from the source graph's schema, so
|
|
3
|
+
// this must run where that graph lives (the main thread), not in the wasm-only worklet.
|
|
4
|
+
import { ByteArrayOutput, isDefined, UUID } from "@opendaw/lib-std";
|
|
5
|
+
import { Address } from "@opendaw/lib-box";
|
|
6
|
+
export const serializeUpdateTasks = (tasks, source) => {
|
|
7
|
+
const output = ByteArrayOutput.create();
|
|
8
|
+
output.writeInt(tasks.length);
|
|
9
|
+
tasks.forEach(task => {
|
|
10
|
+
output.writeString(task.type);
|
|
11
|
+
if (task.type === "new") {
|
|
12
|
+
UUID.toDataOutput(output, task.uuid);
|
|
13
|
+
output.writeString(task.name);
|
|
14
|
+
output.writeInt(task.buffer.byteLength);
|
|
15
|
+
output.writeBytes(new Int8Array(task.buffer));
|
|
16
|
+
}
|
|
17
|
+
else if (task.type === "update-primitive") {
|
|
18
|
+
const address = Address.reconstruct(task.address);
|
|
19
|
+
address.write(output);
|
|
20
|
+
const field = source.findVertex(address).unwrap(() => `no field at ${address}`);
|
|
21
|
+
const serialization = field.serialization();
|
|
22
|
+
output.writeString(serialization.type);
|
|
23
|
+
serialization.encode(output, task.value);
|
|
24
|
+
}
|
|
25
|
+
else if (task.type === "update-pointer") {
|
|
26
|
+
Address.reconstruct(task.address).write(output);
|
|
27
|
+
if (isDefined(task.target)) {
|
|
28
|
+
output.writeBoolean(true);
|
|
29
|
+
Address.reconstruct(task.target).write(output);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
output.writeBoolean(false);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else if (task.type === "delete") {
|
|
36
|
+
UUID.toDataOutput(output, task.uuid);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
return output.toArrayBuffer();
|
|
40
|
+
};
|
package/dist/utf8.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utf8.d.ts","sourceRoot":"","sources":["../src/utf8.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,UAAU,GAAI,OAAO,UAAU,KAAG,MAc9C,CAAA"}
|
package/dist/utf8.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Decode UTF-8 bytes WITHOUT TextDecoder: the AudioWorkletGlobalScope provides neither TextDecoder nor
|
|
2
|
+
// TextEncoder (see worklet-scope.ts), so constructing one anywhere worklet-reachable kills the module
|
|
3
|
+
// before `registerProcessor` runs. Shared by every worklet host that reads a string out of wasm memory
|
|
4
|
+
// (panic messages, MIDI device ids). Malformed sequences decode as U+FFFD; a truncated tail reads zeros.
|
|
5
|
+
export const decodeUtf8 = (bytes) => {
|
|
6
|
+
let result = "";
|
|
7
|
+
let index = 0;
|
|
8
|
+
const next = () => (bytes[index++] ?? 0) & 0x3F;
|
|
9
|
+
while (index < bytes.length) {
|
|
10
|
+
const byte0 = bytes[index++] ?? 0;
|
|
11
|
+
const codePoint = byte0 < 0x80 ? byte0
|
|
12
|
+
: (byte0 & 0xE0) === 0xC0 ? ((byte0 & 0x1F) << 6) | next()
|
|
13
|
+
: (byte0 & 0xF0) === 0xE0 ? ((byte0 & 0x0F) << 12) | (next() << 6) | next()
|
|
14
|
+
: (byte0 & 0xF8) === 0xF0 ? ((byte0 & 0x07) << 18) | (next() << 12) | (next() << 6) | next()
|
|
15
|
+
: 0xFFFD;
|
|
16
|
+
result += codePoint <= 0x10FFFF ? String.fromCodePoint(codePoint) : "�";
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|