@openmixer/omx-contract 0.0.0 → 1.0.0
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 +674 -0
- package/README.md +50 -5
- package/bin/omx-contract.mjs +175 -0
- package/data/kernels/chorus.json +15 -0
- package/data/kernels/comp.json +81 -0
- package/data/kernels/delay.json +66 -0
- package/data/kernels/drive.json +12 -0
- package/data/kernels/eq.json +59 -0
- package/data/kernels/gate.json +81 -0
- package/data/kernels/limiter.json +33 -0
- package/data/kernels/pitch.json +35 -0
- package/data/kernels/reverb.json +15 -0
- package/data/kernels/transient.json +73 -0
- package/data/plugins/delay.json +48 -0
- package/data/primitives.json +47 -0
- package/data/rates.json +30 -0
- package/dist/ts/omx-contract.d.ts +517 -0
- package/dist/ts/omx-contract.js +562 -0
- package/include/omxcontract/omx_contract_limits.h +201 -0
- package/include/omxcontract/params/omx_delay_params.h +47 -0
- package/include/omxcontract/params/omx_plugin_param.h +25 -0
- package/lib/data.mjs +182 -0
- package/lib/fmt.mjs +63 -0
- package/lib/semver.mjs +124 -0
- package/lib/validate.mjs +107 -0
- package/package.json +44 -4
- package/render/c/contract-limits-render.mjs +370 -0
- package/render/c/omx_plugin_param.h +25 -0
- package/render/c/params-generate.mjs +83 -0
- package/render/c.mjs +104 -0
- package/render/doors.mjs +64 -0
- package/render/json.mjs +16 -0
- package/render/ts.mjs +42 -0
- package/schema/omx-contract.schema.json +194 -0
- package/share/omx-contract/omx-contract.json +597 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
#ifndef OMX_CONTRACT_LIMITS_H
|
|
4
|
+
#define OMX_CONTRACT_LIMITS_H
|
|
5
|
+
/*
|
|
6
|
+
* GENERATED — DO NOT EDIT BY HAND.
|
|
7
|
+
* Produced by `omx-contract render --target c` from FreeMixer/omx-contract's data/, the declaration
|
|
8
|
+
* as data (each item's reason is its `doc`). Change the data, never this file: a consumer builds
|
|
9
|
+
* against the render of the omx-contract version it pins and commits none.
|
|
10
|
+
*
|
|
11
|
+
* A C contract (OMX_PRE/OMX_POST/OMX_INVARIANT) reads a limit from HERE, never a literal.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/* Per-parameter numeric travel, one block per `travels` table named <PREFIX>_LIMITS:
|
|
15
|
+
* OMX_<PREFIX>_<FIELD>_MIN/_MAX/_DEFAULT, float literals. */
|
|
16
|
+
#define OMX_COMP_THRESHOLD_DB_MIN -60.0f
|
|
17
|
+
#define OMX_COMP_THRESHOLD_DB_MAX 0.0f
|
|
18
|
+
#define OMX_COMP_THRESHOLD_DB_DEFAULT -18.0f
|
|
19
|
+
#define OMX_COMP_RATIO_MIN 1.0f
|
|
20
|
+
#define OMX_COMP_RATIO_MAX 20.0f
|
|
21
|
+
#define OMX_COMP_RATIO_DEFAULT 4.0f
|
|
22
|
+
#define OMX_COMP_KNEE_DB_MIN 0.0f
|
|
23
|
+
#define OMX_COMP_KNEE_DB_MAX 24.0f
|
|
24
|
+
#define OMX_COMP_KNEE_DB_DEFAULT 6.0f
|
|
25
|
+
#define OMX_COMP_ATTACK_MS_MIN 0.1f
|
|
26
|
+
#define OMX_COMP_ATTACK_MS_MAX 100.0f
|
|
27
|
+
#define OMX_COMP_ATTACK_MS_DEFAULT 5.0f
|
|
28
|
+
#define OMX_COMP_RELEASE_MS_MIN 5.0f
|
|
29
|
+
#define OMX_COMP_RELEASE_MS_MAX 3000.0f
|
|
30
|
+
#define OMX_COMP_RELEASE_MS_DEFAULT 200.0f
|
|
31
|
+
#define OMX_COMP_MAKEUP_DB_MIN 0.0f
|
|
32
|
+
#define OMX_COMP_MAKEUP_DB_MAX 24.0f
|
|
33
|
+
#define OMX_COMP_MAKEUP_DB_DEFAULT 0.0f
|
|
34
|
+
#define OMX_COMP_MIX_PCT_MIN 0.0f
|
|
35
|
+
#define OMX_COMP_MIX_PCT_MAX 100.0f
|
|
36
|
+
#define OMX_COMP_MIX_PCT_DEFAULT 100.0f
|
|
37
|
+
|
|
38
|
+
#define OMX_GATE_THRESHOLD_DB_MIN -80.0f
|
|
39
|
+
#define OMX_GATE_THRESHOLD_DB_MAX 0.0f
|
|
40
|
+
#define OMX_GATE_THRESHOLD_DB_DEFAULT -40.0f
|
|
41
|
+
#define OMX_GATE_RANGE_DB_MIN -90.0f
|
|
42
|
+
#define OMX_GATE_RANGE_DB_MAX 0.0f
|
|
43
|
+
#define OMX_GATE_RANGE_DB_DEFAULT -90.0f
|
|
44
|
+
#define OMX_GATE_KNEE_START_DB_MIN -80.0f
|
|
45
|
+
#define OMX_GATE_KNEE_START_DB_MAX 0.0f
|
|
46
|
+
#define OMX_GATE_KNEE_START_DB_DEFAULT -43.0f
|
|
47
|
+
#define OMX_GATE_KNEE_END_DB_MIN -80.0f
|
|
48
|
+
#define OMX_GATE_KNEE_END_DB_MAX 0.0f
|
|
49
|
+
#define OMX_GATE_KNEE_END_DB_DEFAULT -37.0f
|
|
50
|
+
#define OMX_GATE_ATTACK_MS_MIN 0.0f
|
|
51
|
+
#define OMX_GATE_ATTACK_MS_MAX 500.0f
|
|
52
|
+
#define OMX_GATE_ATTACK_MS_DEFAULT 1.0f
|
|
53
|
+
#define OMX_GATE_HOLD_MS_MIN 0.0f
|
|
54
|
+
#define OMX_GATE_HOLD_MS_MAX 2000.0f
|
|
55
|
+
#define OMX_GATE_HOLD_MS_DEFAULT 10.0f
|
|
56
|
+
#define OMX_GATE_RELEASE_MS_MIN 0.0f
|
|
57
|
+
#define OMX_GATE_RELEASE_MS_MAX 5000.0f
|
|
58
|
+
#define OMX_GATE_RELEASE_MS_DEFAULT 100.0f
|
|
59
|
+
#define OMX_GATE_HYSTERESIS_DB_MIN 0.0f
|
|
60
|
+
#define OMX_GATE_HYSTERESIS_DB_MAX 24.0f
|
|
61
|
+
#define OMX_GATE_HYSTERESIS_DB_DEFAULT 3.0f
|
|
62
|
+
#define OMX_GATE_RATIO_MIN 1.0f
|
|
63
|
+
#define OMX_GATE_RATIO_MAX 100.0f
|
|
64
|
+
#define OMX_GATE_RATIO_DEFAULT 16.0f
|
|
65
|
+
|
|
66
|
+
#define OMX_LIMITER_CEILING_DB_MIN -12.0f
|
|
67
|
+
#define OMX_LIMITER_CEILING_DB_MAX 0.0f
|
|
68
|
+
#define OMX_LIMITER_CEILING_DB_DEFAULT -1.0f
|
|
69
|
+
#define OMX_LIMITER_LOOKAHEAD_MS_MIN 0.5f
|
|
70
|
+
#define OMX_LIMITER_LOOKAHEAD_MS_MAX 5.0f
|
|
71
|
+
#define OMX_LIMITER_LOOKAHEAD_MS_DEFAULT 1.5f
|
|
72
|
+
#define OMX_LIMITER_RELEASE_MS_MIN 1.0f
|
|
73
|
+
#define OMX_LIMITER_RELEASE_MS_MAX 1000.0f
|
|
74
|
+
#define OMX_LIMITER_RELEASE_MS_DEFAULT 50.0f
|
|
75
|
+
|
|
76
|
+
#define OMX_PITCH_SEMITONES_MIN -12.0f
|
|
77
|
+
#define OMX_PITCH_SEMITONES_MAX 12.0f
|
|
78
|
+
#define OMX_PITCH_SEMITONES_DEFAULT 0.0f
|
|
79
|
+
#define OMX_PITCH_CENTS_MIN -50.0f
|
|
80
|
+
#define OMX_PITCH_CENTS_MAX 50.0f
|
|
81
|
+
#define OMX_PITCH_CENTS_DEFAULT 0.0f
|
|
82
|
+
#define OMX_PITCH_MIX_MIN 0.0f
|
|
83
|
+
#define OMX_PITCH_MIX_MAX 100.0f
|
|
84
|
+
#define OMX_PITCH_MIX_DEFAULT 100.0f
|
|
85
|
+
|
|
86
|
+
#define OMX_TRANSIENT_ATTACK_DB_MIN -24.0f
|
|
87
|
+
#define OMX_TRANSIENT_ATTACK_DB_MAX 24.0f
|
|
88
|
+
#define OMX_TRANSIENT_ATTACK_DB_DEFAULT 0.0f
|
|
89
|
+
#define OMX_TRANSIENT_SUSTAIN_DB_MIN -24.0f
|
|
90
|
+
#define OMX_TRANSIENT_SUSTAIN_DB_MAX 24.0f
|
|
91
|
+
#define OMX_TRANSIENT_SUSTAIN_DB_DEFAULT 0.0f
|
|
92
|
+
#define OMX_TRANSIENT_ATTACK_TIME_MS_MIN 2.0f
|
|
93
|
+
#define OMX_TRANSIENT_ATTACK_TIME_MS_MAX 50.0f
|
|
94
|
+
#define OMX_TRANSIENT_ATTACK_TIME_MS_DEFAULT 10.0f
|
|
95
|
+
#define OMX_TRANSIENT_SUSTAIN_TIME_MS_MIN 50.0f
|
|
96
|
+
#define OMX_TRANSIENT_SUSTAIN_TIME_MS_MAX 2000.0f
|
|
97
|
+
#define OMX_TRANSIENT_SUSTAIN_TIME_MS_DEFAULT 250.0f
|
|
98
|
+
#define OMX_TRANSIENT_OUTPUT_DB_MIN -24.0f
|
|
99
|
+
#define OMX_TRANSIENT_OUTPUT_DB_MAX 12.0f
|
|
100
|
+
#define OMX_TRANSIENT_OUTPUT_DB_DEFAULT 0.0f
|
|
101
|
+
|
|
102
|
+
/* The second C spelling of a travel, its item's `c.alias` { fact, field }:
|
|
103
|
+
* OMX_<FACT>_<FIELD>_MIN/_MAX/_DEFAULT. Removed by a MAJOR release once no consumer reads it. */
|
|
104
|
+
#define OMX_DELAY_TIME_MS_MIN 0.0f
|
|
105
|
+
#define OMX_DELAY_TIME_MS_MAX 2000.0f
|
|
106
|
+
#define OMX_DELAY_TIME_MS_DEFAULT 300.0f
|
|
107
|
+
|
|
108
|
+
/* Every scalar, sheet leaf and single travel's numbers, one block sorted by macro. An integer value
|
|
109
|
+
* is an integer literal, any other a float literal AND its OMX_<macro>_DOUBLE spelling. */
|
|
110
|
+
#define OMX_ALLPASS_UNITY_TOLERANCE 0.0001f
|
|
111
|
+
#define OMX_ALLPASS_UNITY_TOLERANCE_DOUBLE 0.0001
|
|
112
|
+
#define OMX_BUTTERWORTH_Q 0.7071067811865476f
|
|
113
|
+
#define OMX_BUTTERWORTH_Q_DOUBLE 0.7071067811865476
|
|
114
|
+
#define OMX_CHORUS_SPREAD_RANGE_DEFAULT 0
|
|
115
|
+
#define OMX_CHORUS_SPREAD_RANGE_MAX 0.5f
|
|
116
|
+
#define OMX_CHORUS_SPREAD_RANGE_MAX_DOUBLE 0.5
|
|
117
|
+
#define OMX_CHORUS_SPREAD_RANGE_MIN 0
|
|
118
|
+
#define OMX_CHORUS_SPREAD_RANGE_STEP 0.01f
|
|
119
|
+
#define OMX_CHORUS_SPREAD_RANGE_STEP_DOUBLE 0.01
|
|
120
|
+
#define OMX_DELAY_MIX_RANGE_BY_KIND_FX_RETURN 1
|
|
121
|
+
#define OMX_DELAY_MIX_RANGE_DEFAULT 0.3f
|
|
122
|
+
#define OMX_DELAY_MIX_RANGE_DEFAULT_DOUBLE 0.3
|
|
123
|
+
#define OMX_DELAY_MIX_RANGE_MAX 1
|
|
124
|
+
#define OMX_DELAY_MIX_RANGE_MIN 0
|
|
125
|
+
#define OMX_DELAY_MIX_RANGE_STEP 0
|
|
126
|
+
#define OMX_DELAY_TONE_RANGE_DEFAULT 0.3f
|
|
127
|
+
#define OMX_DELAY_TONE_RANGE_DEFAULT_DOUBLE 0.3
|
|
128
|
+
#define OMX_DELAY_TONE_RANGE_MAX 1
|
|
129
|
+
#define OMX_DELAY_TONE_RANGE_MIN 0
|
|
130
|
+
#define OMX_DELAY_TONE_RANGE_STEP 0
|
|
131
|
+
#define OMX_DRIVE_BIAS_MAX 0.7071067811865476f
|
|
132
|
+
#define OMX_DRIVE_BIAS_MAX_DOUBLE 0.7071067811865476
|
|
133
|
+
#define OMX_DSP_KNOB_REFERENCE_RATE 96000
|
|
134
|
+
#define OMX_EQ_MAX_BANDS 24
|
|
135
|
+
#define OMX_FDELAY_MOD_READ_ORDER 3
|
|
136
|
+
#define OMX_FDELAY_READ_L1_NORM 1.25f
|
|
137
|
+
#define OMX_FDELAY_READ_L1_NORM_DOUBLE 1.25
|
|
138
|
+
#define OMX_FX_DELAY_FEEDBACK_RANGE_DEFAULT 0.3f
|
|
139
|
+
#define OMX_FX_DELAY_FEEDBACK_RANGE_DEFAULT_DOUBLE 0.3
|
|
140
|
+
#define OMX_FX_DELAY_FEEDBACK_RANGE_MAX 0.99f
|
|
141
|
+
#define OMX_FX_DELAY_FEEDBACK_RANGE_MAX_DOUBLE 0.99
|
|
142
|
+
#define OMX_FX_DELAY_FEEDBACK_RANGE_MIN 0
|
|
143
|
+
#define OMX_FX_DELAY_FEEDBACK_RANGE_STEP 0
|
|
144
|
+
#define OMX_FX_DELAY_TIME_RANGE_DEFAULT 300
|
|
145
|
+
#define OMX_FX_DELAY_TIME_RANGE_MAX 2000
|
|
146
|
+
#define OMX_FX_DELAY_TIME_RANGE_MIN 0
|
|
147
|
+
#define OMX_FX_DELAY_TIME_RANGE_STEP 1
|
|
148
|
+
#define OMX_GEQ_BANDS 31
|
|
149
|
+
#define OMX_PITCH_PREFILTER_CEILING_HZ 20000
|
|
150
|
+
#define OMX_PITCH_PREFILTER_Q 0.7071067811865476f
|
|
151
|
+
#define OMX_PITCH_PREFILTER_Q_DOUBLE 0.7071067811865476
|
|
152
|
+
#define OMX_PITCH_WINDOW_MS 40
|
|
153
|
+
#define OMX_PROGRAM_RELEASE_CHARGE_MS 1000
|
|
154
|
+
#define OMX_PROGRAM_RELEASE_DISCHARGE_MS 5000
|
|
155
|
+
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_FAST_MS 70
|
|
156
|
+
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SHARE 0.5f
|
|
157
|
+
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SHARE_DOUBLE 0.5
|
|
158
|
+
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MAX_MS 1100
|
|
159
|
+
#define OMX_PROGRAM_RELEASE_PROFILES_OPTO_SLOW_MIN_MS 110
|
|
160
|
+
#define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_DEFAULT 100
|
|
161
|
+
#define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_MAX 400
|
|
162
|
+
#define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_MIN 0
|
|
163
|
+
#define OMX_REVERB_PLATE_MOD_DEPTH_RANGE_STEP 1
|
|
164
|
+
#define OMX_RT_HARD_TARGET_QUANTUM 128
|
|
165
|
+
#define OMX_RT_HARD_TARGET_RATE 192000
|
|
166
|
+
#define OMX_TRANSIENT_FAST_ATTACK_MS 0.5f
|
|
167
|
+
#define OMX_TRANSIENT_FAST_ATTACK_MS_DOUBLE 0.5
|
|
168
|
+
#define OMX_TRANSIENT_FAST_RELEASE_MS 20
|
|
169
|
+
#define OMX_TRANSIENT_FLOOR_LIN 0.00001f
|
|
170
|
+
#define OMX_TRANSIENT_FLOOR_LIN_DOUBLE 0.00001
|
|
171
|
+
#define OMX_TRANSIENT_REF_DB 6
|
|
172
|
+
#define OMX_XOVER_LR2_SECTION_Q 0.5f
|
|
173
|
+
#define OMX_XOVER_LR2_SECTION_Q_DOUBLE 0.5
|
|
174
|
+
#define OMX_XOVER_LR4_SECTION_Q 0.7071067811865476f
|
|
175
|
+
#define OMX_XOVER_LR4_SECTION_Q_DOUBLE 0.7071067811865476
|
|
176
|
+
#define OMX_XOVER_PARTITION_TOLERANCE 0.00001f
|
|
177
|
+
#define OMX_XOVER_PARTITION_TOLERANCE_DOUBLE 0.00001
|
|
178
|
+
|
|
179
|
+
/* Every list rendered to C, sorted by list: OMX_<LIST>_COUNT and OMX_<LIST>_INIT { a, b, … }. */
|
|
180
|
+
#define OMX_STANDARD_SAMPLE_RATES_COUNT 6u
|
|
181
|
+
#define OMX_STANDARD_SAMPLE_RATES_INIT { 44100, 48000, 88200, 96000, 176400, 192000 }
|
|
182
|
+
|
|
183
|
+
#endif /* OMX_CONTRACT_LIMITS_H */
|
|
184
|
+
|
|
185
|
+
/* The sample rates the console declares (core's STANDARD_SAMPLE_RATES, row-codecs.ts): the set a
|
|
186
|
+
* contract's rate-is-declared precondition accepts, through omx_rate_is_declared(), and the grid a
|
|
187
|
+
* native tool iterates. Guarded on its own so a tool built against a pinned copy of this header
|
|
188
|
+
* (lv2-inprocess-pin.sh) can include this tree's copy after it and gain the rates without mixing
|
|
189
|
+
* the pin's limits. */
|
|
190
|
+
#ifndef OMX_DECLARED_RATE_COUNT
|
|
191
|
+
#define OMX_DECLARED_RATE_COUNT 6u
|
|
192
|
+
static const float OMX_DECLARED_RATES[OMX_DECLARED_RATE_COUNT] = { 44100.0f, 48000.0f, 88200.0f, 96000.0f, 176400.0f, 192000.0f };
|
|
193
|
+
/* The declared rates that are whole multiples of `base` (a tool's default grid: the 48 kHz family),
|
|
194
|
+
* in declaration order, at most `cap` of them; returns how many were written to `out`. */
|
|
195
|
+
static inline int omx_declared_rates_multiple_of(unsigned base, int *out, int cap) {
|
|
196
|
+
int n = 0;
|
|
197
|
+
for (unsigned k = 0; k < OMX_DECLARED_RATE_COUNT && n < cap; k++)
|
|
198
|
+
if ((unsigned)OMX_DECLARED_RATES[k] % base == 0) out[n++] = (int)OMX_DECLARED_RATES[k];
|
|
199
|
+
return n;
|
|
200
|
+
}
|
|
201
|
+
#endif
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
#ifndef OMX_DELAY_PARAMS_H
|
|
4
|
+
#define OMX_DELAY_PARAMS_H
|
|
5
|
+
/*
|
|
6
|
+
* GENERATED — DO NOT EDIT BY HAND.
|
|
7
|
+
* Produced by `omx-contract render --target c` from FreeMixer/omx-contract's data/plugins/delay.json
|
|
8
|
+
* (DELAY_PLUGIN): FX_DELAY_TIME_RANGE, FX_DELAY_FEEDBACK_RANGE, DELAY_MIX_RANGE for kind input, DELAY_TONE_RANGE, FX_DELAY_PINGPONG_DEFAULT.
|
|
9
|
+
* Change the data, never this file. Order is append-only (omx-contract spec §3.3).
|
|
10
|
+
*/
|
|
11
|
+
#include "omx_plugin_param.h"
|
|
12
|
+
|
|
13
|
+
enum {
|
|
14
|
+
OMX_DELAY_PARAM_TIME_MS = 0,
|
|
15
|
+
OMX_DELAY_PARAM_FEEDBACK = 1,
|
|
16
|
+
OMX_DELAY_PARAM_MIX = 2,
|
|
17
|
+
OMX_DELAY_PARAM_TONE = 3,
|
|
18
|
+
OMX_DELAY_PARAM_PINGPONG = 4,
|
|
19
|
+
OMX_DELAY_PARAM_COUNT = 5
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
static const omx_plugin_param OMX_DELAY_PARAMS[OMX_DELAY_PARAM_COUNT] = {
|
|
23
|
+
{ "timeMs", "Time", "ms", 0.0f, 2000.0f, 300.0f, OMX_PLUGIN_PARAM_INTEGER },
|
|
24
|
+
{ "feedback", "Feedback", "", 0.0f, 0.99f, 0.3f, 0u },
|
|
25
|
+
{ "mix", "Mix", "", 0.0f, 1.0f, 0.3f, 0u },
|
|
26
|
+
{ "tone", "Tone", "", 0.0f, 1.0f, 0.3f, 0u },
|
|
27
|
+
{ "pingpong", "Pingpong", "", 0.0f, 1.0f, 0.0f, OMX_PLUGIN_PARAM_TOGGLE },
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/* One macro per declared bound: what a C face reads where a constant is needed. */
|
|
31
|
+
#define OMX_DELAY_PARAM_TIME_MS_MIN 0.0f
|
|
32
|
+
#define OMX_DELAY_PARAM_TIME_MS_MAX 2000.0f
|
|
33
|
+
#define OMX_DELAY_PARAM_TIME_MS_DEFAULT 300.0f
|
|
34
|
+
#define OMX_DELAY_PARAM_FEEDBACK_MIN 0.0f
|
|
35
|
+
#define OMX_DELAY_PARAM_FEEDBACK_MAX 0.99f
|
|
36
|
+
#define OMX_DELAY_PARAM_FEEDBACK_DEFAULT 0.3f
|
|
37
|
+
#define OMX_DELAY_PARAM_MIX_MIN 0.0f
|
|
38
|
+
#define OMX_DELAY_PARAM_MIX_MAX 1.0f
|
|
39
|
+
#define OMX_DELAY_PARAM_MIX_DEFAULT 0.3f
|
|
40
|
+
#define OMX_DELAY_PARAM_TONE_MIN 0.0f
|
|
41
|
+
#define OMX_DELAY_PARAM_TONE_MAX 1.0f
|
|
42
|
+
#define OMX_DELAY_PARAM_TONE_DEFAULT 0.3f
|
|
43
|
+
#define OMX_DELAY_PARAM_PINGPONG_MIN 0.0f
|
|
44
|
+
#define OMX_DELAY_PARAM_PINGPONG_MAX 1.0f
|
|
45
|
+
#define OMX_DELAY_PARAM_PINGPONG_DEFAULT 0.0f
|
|
46
|
+
|
|
47
|
+
#endif /* OMX_DELAY_PARAMS_H */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
* Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
*
|
|
4
|
+
* omx_plugin_param.h — the SHAPE of one generated plugin parameter, and nothing else: no number
|
|
5
|
+
* lives here. The rows are rendered by `omx-contract render --target c` into omx_<plugin>_params.h
|
|
6
|
+
* beside this file, from FreeMixer/omx-contract's data/plugins/ (omx-contract spec §4.2); a DPF
|
|
7
|
+
* plugin's initParameter reads them and types no range of its own.
|
|
8
|
+
*/
|
|
9
|
+
#ifndef OMX_PLUGIN_PARAM_H
|
|
10
|
+
#define OMX_PLUGIN_PARAM_H
|
|
11
|
+
|
|
12
|
+
/** A whole-number travel (declared `step` >= 1 over integer bounds). */
|
|
13
|
+
#define OMX_PLUGIN_PARAM_INTEGER 1u
|
|
14
|
+
/** A declared boolean: domain {0, 1}, the come-up value declared. */
|
|
15
|
+
#define OMX_PLUGIN_PARAM_TOGGLE 2u
|
|
16
|
+
|
|
17
|
+
typedef struct {
|
|
18
|
+
const char *symbol; /* the declared field name — the LV2 symbol, the CLAP param's identity */
|
|
19
|
+
const char *name; /* derived from the symbol, never a label table */
|
|
20
|
+
const char *unit;
|
|
21
|
+
float min, max, def;
|
|
22
|
+
unsigned flags;
|
|
23
|
+
} omx_plugin_param;
|
|
24
|
+
|
|
25
|
+
#endif /* OMX_PLUGIN_PARAM_H */
|
package/lib/data.mjs
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The declaration as data: load every `data/**.json` file, then resolve each item's references
|
|
5
|
+
* and derivations into plain values. The renderers are pure functions of the resolved items.
|
|
6
|
+
*/
|
|
7
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
8
|
+
import { join, relative } from 'node:path';
|
|
9
|
+
|
|
10
|
+
/** Every `.json` file under `dir`, sorted by its path relative to `dir`. */
|
|
11
|
+
export function dataFiles(dir) {
|
|
12
|
+
const out = [];
|
|
13
|
+
const walk = (d) => {
|
|
14
|
+
for (const f of readdirSync(d).sort()) {
|
|
15
|
+
const p = join(d, f);
|
|
16
|
+
if (statSync(p).isDirectory()) walk(p);
|
|
17
|
+
else if (f.endsWith('.json')) out.push(p);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
walk(dir);
|
|
21
|
+
return out.sort((a, b) => (relative(dir, a) < relative(dir, b) ? -1 : 1));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The raw data: `{ files: [{ path, rel, doc }], items: Map<name, { name, rel, item }> }`, items in
|
|
26
|
+
* file order then key order. `files` may be handed in as `{ rel: text }` (a tag's data, a test).
|
|
27
|
+
* @throws naming the file when one is not JSON, and an item declared by two files.
|
|
28
|
+
*/
|
|
29
|
+
export function loadData(dir, texts) {
|
|
30
|
+
const files = [];
|
|
31
|
+
const entries = texts ? Object.entries(texts).sort(([a], [b]) => (a < b ? -1 : 1)) : dataFiles(dir).map((p) => [relative(join(dir, '..'), p), readFileSync(p, 'utf8')]);
|
|
32
|
+
const items = new Map();
|
|
33
|
+
for (const [rel, text] of entries) {
|
|
34
|
+
let doc;
|
|
35
|
+
try { doc = JSON.parse(text); } catch (e) { throw new Error(`${rel}: not JSON: ${e.message}`); }
|
|
36
|
+
files.push({ rel, text, doc });
|
|
37
|
+
for (const [name, item] of Object.entries(doc)) {
|
|
38
|
+
if (name === '$schema') continue;
|
|
39
|
+
if (items.has(name)) throw new Error(`${rel}: ${name}: already declared by ${items.get(name).rel}`);
|
|
40
|
+
items.set(name, { name, rel, item });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { files, items };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** An item's own value before resolution, by kind. */
|
|
47
|
+
export function rawValue(item) {
|
|
48
|
+
switch (item.kind) {
|
|
49
|
+
case 'scalar': return item.value;
|
|
50
|
+
case 'travels': return item.fields ?? item.travel;
|
|
51
|
+
case 'sheet': return item.value;
|
|
52
|
+
case 'list': return item.values;
|
|
53
|
+
case 'plugin': return item.params;
|
|
54
|
+
default: return undefined;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The closed list of derivations (spec §3.3). A new one is a change here with its own test. */
|
|
59
|
+
export const DERIVATIONS = {
|
|
60
|
+
count: (d, val) => {
|
|
61
|
+
const v = val(typeof d.of === 'string' ? { ref: d.of } : d.of);
|
|
62
|
+
if (!Array.isArray(v)) throw new Error(`count: ${JSON.stringify(d.of)} is not a list`);
|
|
63
|
+
return v.length;
|
|
64
|
+
},
|
|
65
|
+
sqrt: (d, val) => {
|
|
66
|
+
const v = val(d.of);
|
|
67
|
+
if (typeof v !== 'number' || v < 0) throw new Error(`sqrt: ${JSON.stringify(d.of)} is not a non-negative number`);
|
|
68
|
+
return Math.sqrt(v);
|
|
69
|
+
},
|
|
70
|
+
lagrangeReadL1: (d, val) => {
|
|
71
|
+
const order = val(d.order);
|
|
72
|
+
if (!Number.isInteger(order) || order < 1) throw new Error(`lagrangeReadL1: order ${order} is not a positive integer`);
|
|
73
|
+
return lagrangeReadL1(order);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** The taps of the order-`order` Lagrange fractional read at fraction `f` (the fdelay kernel's read). */
|
|
78
|
+
export function lagrangeReadTaps(order, f) {
|
|
79
|
+
const off = Math.floor((order - 1) / 2);
|
|
80
|
+
const c = [];
|
|
81
|
+
for (let k = 0; k <= order; k++) {
|
|
82
|
+
let num = 1;
|
|
83
|
+
let den = 1;
|
|
84
|
+
for (let j = 0; j <= order; j++) {
|
|
85
|
+
if (j === k) continue;
|
|
86
|
+
num *= f - (j - off);
|
|
87
|
+
den *= k - j;
|
|
88
|
+
}
|
|
89
|
+
c.push(num / den);
|
|
90
|
+
}
|
|
91
|
+
return c;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The worst L1 norm of the read over 1024 fractions, rounded to 1e-9 (core's readKernelL1Norm). */
|
|
95
|
+
export function lagrangeReadL1(order) {
|
|
96
|
+
const FRACTION_STEPS = 1024;
|
|
97
|
+
let worst = 0;
|
|
98
|
+
for (let i = 0; i < FRACTION_STEPS; i++) {
|
|
99
|
+
const l1 = lagrangeReadTaps(order, i / FRACTION_STEPS).reduce((a, c) => a + Math.abs(c), 0);
|
|
100
|
+
if (l1 > worst) worst = l1;
|
|
101
|
+
}
|
|
102
|
+
return Math.round(worst * 1e9) / 1e9;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Resolve every item: `{ name, rel, kind, doc, why?, unit?, c?, shape?, value }`, where `value` has
|
|
109
|
+
* every `{ref}` and `{derive}` evaluated. A plugin's value is its parameters resolved against the
|
|
110
|
+
* items they read.
|
|
111
|
+
* @throws naming the item: a reference that does not resolve, a cycle, an unknown derivation.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveData(data) {
|
|
114
|
+
const done = new Map();
|
|
115
|
+
const busy = new Set();
|
|
116
|
+
const lookup = (path, from) => {
|
|
117
|
+
const [head, ...rest] = path.split('.');
|
|
118
|
+
const entry = data.items.get(head);
|
|
119
|
+
if (!entry) throw new Error(`${from}: reference ${path}: no item ${head}`);
|
|
120
|
+
let v = resolveItem(head).value;
|
|
121
|
+
for (const k of rest) {
|
|
122
|
+
if (!isObj(v) || !(k in v)) throw new Error(`${from}: reference ${path}: ${head} has no ${k}`);
|
|
123
|
+
v = v[k];
|
|
124
|
+
}
|
|
125
|
+
return v;
|
|
126
|
+
};
|
|
127
|
+
const val = (v, from) => {
|
|
128
|
+
if (Array.isArray(v)) return v.map((x) => val(x, from));
|
|
129
|
+
if (!isObj(v)) return v;
|
|
130
|
+
if ('ref' in v) return lookup(v.ref, from);
|
|
131
|
+
if ('derive' in v) {
|
|
132
|
+
const fn = DERIVATIONS[v.derive];
|
|
133
|
+
if (!fn) throw new Error(`${from}: unknown derivation ${v.derive}`);
|
|
134
|
+
try { return fn(v, (x) => val(x, from)); } catch (e) { throw new Error(`${from}: ${e.message}`); }
|
|
135
|
+
}
|
|
136
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, val(x, from)]));
|
|
137
|
+
};
|
|
138
|
+
function resolveItem(name) {
|
|
139
|
+
if (done.has(name)) return done.get(name);
|
|
140
|
+
if (busy.has(name)) throw new Error(`${name}: reference cycle through ${[...busy].join(' -> ')} -> ${name}`);
|
|
141
|
+
busy.add(name);
|
|
142
|
+
const { rel, item } = data.items.get(name);
|
|
143
|
+
const r = { name, rel, kind: item.kind, doc: item.doc };
|
|
144
|
+
if (item.why !== undefined) r.why = item.why;
|
|
145
|
+
if (item.unit !== undefined) r.unit = item.unit;
|
|
146
|
+
if (item.c !== undefined) r.c = item.c;
|
|
147
|
+
if (item.kind === 'travels') r.shape = item.fields ? 'table' : 'travel';
|
|
148
|
+
if (item.kind === 'plugin') {
|
|
149
|
+
for (const k of ['plugin', 'header', 'prefix', 'source']) r[k] = item[k];
|
|
150
|
+
r.value = item.params.map((p) => resolveParam(name, p));
|
|
151
|
+
} else {
|
|
152
|
+
r.value = val(rawValue(item), name);
|
|
153
|
+
}
|
|
154
|
+
busy.delete(name);
|
|
155
|
+
done.set(name, r);
|
|
156
|
+
return r;
|
|
157
|
+
}
|
|
158
|
+
function resolveParam(plugin, p) {
|
|
159
|
+
const from = `${plugin}.${p.symbol}`;
|
|
160
|
+
const read = lookup(p.from, from);
|
|
161
|
+
const toggle = (p.flags ?? []).includes('toggle');
|
|
162
|
+
if (toggle) {
|
|
163
|
+
if (typeof read !== 'boolean') throw new Error(`${from}: a toggle reads a boolean, ${p.from} is ${JSON.stringify(read)}`);
|
|
164
|
+
return { symbol: p.symbol, min: 0, max: 1, def: read ? 1 : 0, step: 1, unit: '', toggle: true };
|
|
165
|
+
}
|
|
166
|
+
if (!isObj(read)) throw new Error(`${from}: ${p.from} is not a travel`);
|
|
167
|
+
let def = read.default;
|
|
168
|
+
if (p.forKind !== undefined) def = read.byKind?.[p.forKind] ?? read.default;
|
|
169
|
+
for (const [k, v] of [['min', read.min], ['max', read.max], ['default', def], ['step', read.step], ['unit', read.unit]]) {
|
|
170
|
+
if (v === undefined) throw new Error(`${from}: ${p.from} declares no ${k}`);
|
|
171
|
+
}
|
|
172
|
+
return { symbol: p.symbol, min: read.min, max: read.max, def, step: read.step, unit: read.unit, toggle: false };
|
|
173
|
+
}
|
|
174
|
+
const out = new Map();
|
|
175
|
+
for (const name of data.items.keys()) out.set(name, resolveItem(name));
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The package version (package.json), which names the tag, the release and the npm version. */
|
|
180
|
+
export function packageVersion(root) {
|
|
181
|
+
return JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')).version;
|
|
182
|
+
}
|
package/lib/fmt.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* The one formatting of a data file (spec §3.2): two-space indent, one key per line, keys in schema
|
|
5
|
+
* order, a trailing newline. Names (items, fields, sheet keys) keep the order the author gave them:
|
|
6
|
+
* a travels table's field order is its C block order.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const ORDER = {
|
|
10
|
+
item: ['kind', 'doc', 'why', 'unit', 'plugin', 'header', 'prefix', 'source', 'value', 'values', 'fields', 'travel', 'params', 'c'],
|
|
11
|
+
travel: ['min', 'max', 'step', 'unit', 'default', 'defaultFrom', 'byKind', 'doc', 'why'],
|
|
12
|
+
param: ['symbol', 'from', 'forKind', 'flags', 'scale', 'since', 'doc'],
|
|
13
|
+
c: ['alias', 'render'],
|
|
14
|
+
alias: ['fact', 'field'],
|
|
15
|
+
number: ['ref', 'derive', 'of', 'order'],
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function ordered(obj, order) {
|
|
19
|
+
const known = order.filter((k) => k in obj);
|
|
20
|
+
const rest = Object.keys(obj).filter((k) => !order.includes(k));
|
|
21
|
+
return Object.fromEntries([...known, ...rest].map((k) => [k, obj[k]]));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isObj = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
25
|
+
|
|
26
|
+
function number(v) {
|
|
27
|
+
if (!isObj(v)) return v;
|
|
28
|
+
return Object.fromEntries(Object.entries(ordered(v, ORDER.number)).map(([k, x]) => [k, number(x)]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sheet(v) {
|
|
32
|
+
if (!isObj(v) || 'ref' in v || 'derive' in v) return number(v);
|
|
33
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, sheet(x)]));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function travel(t) {
|
|
37
|
+
const o = ordered(t, ORDER.travel);
|
|
38
|
+
for (const k of ['min', 'max', 'step', 'default']) if (k in o) o[k] = number(o[k]);
|
|
39
|
+
if (isObj(o.byKind)) o.byKind = Object.fromEntries(Object.entries(o.byKind).map(([k, x]) => [k, number(x)]));
|
|
40
|
+
return o;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function item(it) {
|
|
44
|
+
if (!isObj(it)) return it;
|
|
45
|
+
const o = ordered(it, ORDER.item);
|
|
46
|
+
if ('value' in o) o.value = o.kind === 'sheet' ? sheet(o.value) : number(o.value);
|
|
47
|
+
if (isObj(o.fields)) o.fields = Object.fromEntries(Object.entries(o.fields).map(([k, t]) => [k, isObj(t) ? travel(t) : t]));
|
|
48
|
+
if (isObj(o.travel)) o.travel = travel(o.travel);
|
|
49
|
+
if (Array.isArray(o.params)) o.params = o.params.map((p) => (isObj(p) ? ordered(p, ORDER.param) : p));
|
|
50
|
+
if (isObj(o.c)) {
|
|
51
|
+
o.c = ordered(o.c, ORDER.c);
|
|
52
|
+
if (isObj(o.c.alias)) o.c.alias = ordered(o.c.alias, ORDER.alias);
|
|
53
|
+
}
|
|
54
|
+
return o;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The canonical text of a parsed data file. */
|
|
58
|
+
export function formatDoc(doc) {
|
|
59
|
+
const out = {};
|
|
60
|
+
if ('$schema' in doc) out.$schema = doc.$schema;
|
|
61
|
+
for (const [k, v] of Object.entries(doc)) if (k !== '$schema') out[k] = item(v);
|
|
62
|
+
return `${JSON.stringify(out, null, 2)}\n`;
|
|
63
|
+
}
|
package/lib/semver.mjs
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* `omx-contract semver`: classify every change between two resolved declarations (spec §3.6) and
|
|
5
|
+
* refuse a version whose bump is shorter than the largest class found.
|
|
6
|
+
*
|
|
7
|
+
* major an item, field, parameter or alias removed; a unit or kind changed; a travel narrowed; a
|
|
8
|
+
* plugin's parameters reordered or changed; a constant a kernel computes with changed (a
|
|
9
|
+
* scalar, sheet leaf or derived value that is not a travel bound or default; a list member
|
|
10
|
+
* changed or removed)
|
|
11
|
+
* minor an item, field, sheet leaf, parameter or list member added (appended); a travel widened;
|
|
12
|
+
* a default moved inside its travel; a step changed
|
|
13
|
+
* patch doc and why text
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const RANK = { none: 0, patch: 1, minor: 2, major: 3 };
|
|
17
|
+
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
18
|
+
|
|
19
|
+
function travelChanges(at, a, b, say) {
|
|
20
|
+
if (b.min > a.min) say('major', `${at}: travel narrowed (min ${a.min} -> ${b.min})`);
|
|
21
|
+
if (b.max < a.max) say('major', `${at}: travel narrowed (max ${a.max} -> ${b.max})`);
|
|
22
|
+
if (b.min < a.min || b.max > a.max) say('minor', `${at}: travel widened ([${a.min}, ${a.max}] -> [${b.min}, ${b.max}])`);
|
|
23
|
+
if (a.unit !== b.unit) say('major', `${at}: unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
|
|
24
|
+
if (a.default !== b.default) say(a.default === undefined || b.default === undefined ? 'major' : 'minor', `${at}: default ${a.default} -> ${b.default}`);
|
|
25
|
+
if (a.step !== b.step) say('minor', `${at}: step ${a.step} -> ${b.step}`);
|
|
26
|
+
if (a.defaultFrom !== b.defaultFrom) say('minor', `${at}: defaultFrom ${a.defaultFrom} -> ${b.defaultFrom}`);
|
|
27
|
+
const ka = a.byKind ?? {};
|
|
28
|
+
const kb = b.byKind ?? {};
|
|
29
|
+
for (const k of Object.keys(ka)) if (!(k in kb)) say('major', `${at}.byKind.${k} removed`);
|
|
30
|
+
for (const k of Object.keys(kb)) {
|
|
31
|
+
if (!(k in ka)) say('minor', `${at}.byKind.${k} added`);
|
|
32
|
+
else if (ka[k] !== kb[k]) say('minor', `${at}.byKind.${k}: ${ka[k]} -> ${kb[k]}`);
|
|
33
|
+
}
|
|
34
|
+
if ((a.doc ?? '') !== (b.doc ?? '') || (a.why ?? '') !== (b.why ?? '')) say('patch', `${at}: doc`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function leafChanges(at, a, b, say) {
|
|
38
|
+
if (typeof a === 'object' && a !== null && typeof b === 'object' && b !== null) {
|
|
39
|
+
for (const k of Object.keys(a)) {
|
|
40
|
+
if (!(k in b)) say('major', `${at}.${k} removed`);
|
|
41
|
+
else leafChanges(`${at}.${k}`, a[k], b[k], say);
|
|
42
|
+
}
|
|
43
|
+
for (const k of Object.keys(b)) if (!(k in a)) say('minor', `${at}.${k} added`);
|
|
44
|
+
} else if (a !== b) {
|
|
45
|
+
say('major', `${at}: ${JSON.stringify(a)} -> ${JSON.stringify(b)} (a constant a kernel computes with)`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `{ level, changes: [{ level, item, what }] }` from `prev` to `next` (Maps name -> resolved item). */
|
|
50
|
+
export function classify(prev, next) {
|
|
51
|
+
const changes = [];
|
|
52
|
+
for (const [name, a] of prev) {
|
|
53
|
+
const say = (level, what) => changes.push({ level, item: name, what });
|
|
54
|
+
const b = next.get(name);
|
|
55
|
+
if (!b) { say('major', 'item removed or renamed'); continue; }
|
|
56
|
+
if (a.kind !== b.kind || (a.shape ?? '') !== (b.shape ?? '')) { say('major', `kind ${a.kind} -> ${b.kind}`); continue; }
|
|
57
|
+
if ((a.unit ?? '') !== (b.unit ?? '')) say('major', `unit ${JSON.stringify(a.unit)} -> ${JSON.stringify(b.unit)}`);
|
|
58
|
+
if (a.doc !== b.doc || (a.why ?? '') !== (b.why ?? '')) say('patch', 'doc');
|
|
59
|
+
if (!same(a.c?.alias, b.c?.alias)) say(a.c?.alias ? 'major' : 'minor', a.c?.alias ? 'c.alias removed or renamed' : 'c.alias added');
|
|
60
|
+
if (!same(a.c?.render, b.c?.render)) say(b.c?.render === false ? 'major' : 'minor', 'C render of the list changed');
|
|
61
|
+
switch (a.kind) {
|
|
62
|
+
case 'travels':
|
|
63
|
+
if (a.shape === 'travel') travelChanges(name, a.value, b.value, say);
|
|
64
|
+
else {
|
|
65
|
+
for (const f of Object.keys(a.value)) {
|
|
66
|
+
if (!(f in b.value)) say('major', `${name}.${f} removed`);
|
|
67
|
+
else travelChanges(`${name}.${f}`, a.value[f], b.value[f], say);
|
|
68
|
+
}
|
|
69
|
+
for (const f of Object.keys(b.value)) if (!(f in a.value)) say('minor', `${name}.${f} added`);
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case 'scalar':
|
|
73
|
+
case 'sheet':
|
|
74
|
+
leafChanges(name, a.value, b.value, say);
|
|
75
|
+
break;
|
|
76
|
+
case 'list':
|
|
77
|
+
a.value.forEach((v, i) => {
|
|
78
|
+
if (i >= b.value.length) say('major', `${name}[${i}] removed`);
|
|
79
|
+
else if (b.value[i] !== v) say('major', `${name}[${i}]: ${v} -> ${b.value[i]}`);
|
|
80
|
+
});
|
|
81
|
+
if (b.value.length > a.value.length) say('minor', `${name}: ${b.value.length - a.value.length} member(s) appended`);
|
|
82
|
+
break;
|
|
83
|
+
case 'plugin':
|
|
84
|
+
for (const k of ['plugin', 'header', 'prefix']) if (a[k] !== b[k]) say('major', `${k} ${a[k]} -> ${b[k]}`);
|
|
85
|
+
if (a.source !== b.source) say('patch', 'source text');
|
|
86
|
+
a.value.forEach((p, i) => {
|
|
87
|
+
const q = b.value[i];
|
|
88
|
+
if (!q) say('major', `parameter ${p.symbol} removed`);
|
|
89
|
+
else if (q.symbol !== p.symbol) say('major', `parameter ${i} ${p.symbol} -> ${q.symbol} (reordered or renamed)`);
|
|
90
|
+
else if (!same(p, q)) say(p.min < q.min || p.max > q.max || p.unit !== q.unit || p.toggle !== q.toggle ? 'major' : 'minor', `parameter ${p.symbol} changed`);
|
|
91
|
+
});
|
|
92
|
+
if (b.value.length > a.value.length) say('minor', `${b.value.length - a.value.length} parameter(s) appended`);
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const name of next.keys()) if (!prev.has(name)) changes.push({ level: 'minor', item: name, what: 'item added' });
|
|
99
|
+
const level = changes.reduce((m, c) => (RANK[c.level] > RANK[m] ? c.level : m), 'none');
|
|
100
|
+
return { level, changes };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parse = (v) => {
|
|
104
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(v);
|
|
105
|
+
if (!m) throw new Error(`not a version: ${v}`);
|
|
106
|
+
return m.slice(1).map(Number);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** The class of the bump from version `from` to `to`: 'major' | 'minor' | 'patch' | 'none'; throws if `to` is lower. */
|
|
110
|
+
export function bumpOf(from, to) {
|
|
111
|
+
const [a, b] = [parse(from), parse(to)];
|
|
112
|
+
for (let i = 0; i < 3; i++) {
|
|
113
|
+
if (b[i] > a[i]) return ['major', 'minor', 'patch'][i];
|
|
114
|
+
if (b[i] < a[i]) throw new Error(`version ${to} is lower than ${from}`);
|
|
115
|
+
}
|
|
116
|
+
return 'none';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** null when bumping `from` -> `to` covers `level`, else the refusal. */
|
|
120
|
+
export function refusal(level, from, to) {
|
|
121
|
+
let bump;
|
|
122
|
+
try { bump = bumpOf(from, to); } catch (e) { return e.message; }
|
|
123
|
+
return RANK[bump] >= RANK[level] ? null : `the changes since ${from} are ${level.toUpperCase()}, and ${from} -> ${to} is a ${bump} bump`;
|
|
124
|
+
}
|