Skip to content

Commit c031bda

Browse files
committed
synthio: implement a range compressor with hard knee
This really improves the loudness of the output with multiple notes while being a nice simple algorithm to implement.
1 parent 9a9f322 commit c031bda

File tree

5 files changed

+153
-130
lines changed

5 files changed

+153
-130
lines changed

shared-module/synthio/__init__.c

Lines changed: 87 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -145,20 +145,30 @@ STATIC synthio_envelope_definition_t *synthio_synth_get_note_envelope(synthio_sy
145145
}
146146

147147

148-
STATIC uint32_t synthio_synth_sum_envelope(synthio_synth_t *synth) {
149-
uint32_t result = 0;
150-
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
151-
mp_obj_t note_obj = synth->span.note_obj[chan];
152-
if (note_obj != SYNTHIO_SILENCE) {
153-
synthio_envelope_state_t *state = &synth->envelope_state[chan];
154-
if (state->state == SYNTHIO_ENVELOPE_STATE_ATTACK) {
155-
result += state->level;
156-
} else {
157-
result += synthio_synth_get_note_envelope(synth, note_obj)->attack_level;
158-
}
159-
}
148+
#define RANGE_LOW (-28000)
149+
#define RANGE_HIGH (28000)
150+
#define RANGE_SHIFT (16)
151+
#define RANGE_SCALE (0xfffffff / (32768 * CIRCUITPY_SYNTHIO_MAX_CHANNELS - RANGE_HIGH))
152+
153+
// dynamic range compression via a downward compressor with hard knee
154+
//
155+
// When the output value is within the range +-28000 (about 85% of full scale),
156+
// it is unchanged. Otherwise, it undergoes a gain reduction so that the
157+
// largest possible values, (+32768,-32767) * CIRCUITPY_SYNTHIO_MAX_CHANNELS,
158+
// still fit within the output range
159+
//
160+
// This produces a much louder overall volume with multiple voices, without
161+
// much additional processing.
162+
//
163+
// https://en.wikipedia.org/wiki/Dynamic_range_compression
164+
STATIC
165+
int16_t mix_down_sample(int32_t sample) {
166+
if (sample < RANGE_LOW) {
167+
sample = (((sample - RANGE_LOW) * RANGE_SCALE) >> RANGE_SHIFT) + RANGE_LOW;
168+
} else if (sample > RANGE_HIGH) {
169+
sample = (((sample - RANGE_HIGH) * RANGE_SCALE) >> RANGE_SHIFT) + RANGE_HIGH;
160170
}
161-
return result;
171+
return sample;
162172
}
163173

164174
void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t *buffer_length, uint8_t channel) {
@@ -172,89 +182,83 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t
172182
synth->buffer_index = !synth->buffer_index;
173183
synth->other_channel = 1 - channel;
174184
synth->other_buffer_index = synth->buffer_index;
175-
int16_t *out_buffer = (int16_t *)(void *)synth->buffers[synth->buffer_index];
176185

177186
uint16_t dur = MIN(SYNTHIO_MAX_DUR, synth->span.dur);
178187
synth->span.dur -= dur;
179-
memset(out_buffer, 0, synth->buffer_length);
180188

181189
int32_t sample_rate = synth->sample_rate;
182-
uint32_t total_envelope = synthio_synth_sum_envelope(synth);
183-
if (total_envelope < synth->total_envelope) {
184-
// total envelope is decreasing. Slowly let remaining notes get louder
185-
// the time constant is arbitrary, on the order of 1s at 48kHz
186-
total_envelope = synth->total_envelope = (
187-
total_envelope + synth->total_envelope * 255) / 256;
188-
} else {
189-
// total envelope is steady or increasing, so just store this as
190-
// the high water mark
191-
synth->total_envelope = total_envelope;
192-
}
193-
if (total_envelope > 0) {
194-
uint16_t ovl_loudness = 0x7fffffff / MAX(0x8000, total_envelope);
190+
int32_t out_buffer32[dur];
195191

196-
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
197-
mp_obj_t note_obj = synth->span.note_obj[chan];
198-
if (note_obj == SYNTHIO_SILENCE) {
199-
synth->accum[chan] = 0;
200-
continue;
201-
}
192+
memset(out_buffer32, 0, sizeof(out_buffer32));
193+
for (int chan = 0; chan < CIRCUITPY_SYNTHIO_MAX_CHANNELS; chan++) {
194+
mp_obj_t note_obj = synth->span.note_obj[chan];
195+
if (note_obj == SYNTHIO_SILENCE) {
196+
synth->accum[chan] = 0;
197+
continue;
198+
}
202199

203-
if (synth->envelope_state[chan].level == 0) {
204-
// note is truly finished, but we only just noticed
205-
synth->span.note_obj[chan] = SYNTHIO_SILENCE;
206-
continue;
207-
}
200+
if (synth->envelope_state[chan].level == 0) {
201+
// note is truly finished, but we only just noticed
202+
synth->span.note_obj[chan] = SYNTHIO_SILENCE;
203+
continue;
204+
}
208205

209-
// adjust loudness by envelope
210-
uint16_t loudness = (ovl_loudness * synth->envelope_state[chan].level) >> 16;
211-
212-
uint32_t dds_rate;
213-
const int16_t *waveform = synth->waveform;
214-
uint32_t waveform_length = synth->waveform_length;
215-
if (mp_obj_is_small_int(note_obj)) {
216-
uint8_t note = mp_obj_get_int(note_obj);
217-
uint8_t octave = note / 12;
218-
uint16_t base_freq = notes[note % 12];
219-
// rate = base_freq * waveform_length
220-
// den = sample_rate * 2 ^ (10 - octave)
221-
// den = sample_rate * 2 ^ 10 / 2^octave
222-
// dds_rate = 2^SHIFT * rate / den
223-
// dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate
224-
dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate;
225-
} else {
226-
synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj);
227-
int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, &loudness);
228-
if (note->waveform_buf.buf) {
229-
waveform = note->waveform_buf.buf;
230-
waveform_length = note->waveform_buf.len / 2;
231-
}
232-
dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate);
206+
// adjust loudness by envelope
207+
uint16_t loudness = synth->envelope_state[chan].level;
208+
209+
uint32_t dds_rate;
210+
const int16_t *waveform = synth->waveform;
211+
uint32_t waveform_length = synth->waveform_length;
212+
if (mp_obj_is_small_int(note_obj)) {
213+
uint8_t note = mp_obj_get_int(note_obj);
214+
uint8_t octave = note / 12;
215+
uint16_t base_freq = notes[note % 12];
216+
// rate = base_freq * waveform_length
217+
// den = sample_rate * 2 ^ (10 - octave)
218+
// den = sample_rate * 2 ^ 10 / 2^octave
219+
// dds_rate = 2^SHIFT * rate / den
220+
// dds_rate = 2^(SHIFT-10+octave) * base_freq * waveform_length / sample_rate
221+
dds_rate = (sample_rate / 2 + ((uint64_t)(base_freq * waveform_length) << (SYNTHIO_FREQUENCY_SHIFT - 10 + octave))) / sample_rate;
222+
} else {
223+
synthio_note_obj_t *note = MP_OBJ_TO_PTR(note_obj);
224+
int32_t frequency_scaled = synthio_note_step(note, sample_rate, dur, &loudness);
225+
if (note->waveform_buf.buf) {
226+
waveform = note->waveform_buf.buf;
227+
waveform_length = note->waveform_buf.len / 2;
233228
}
229+
dds_rate = synthio_frequency_convert_scaled_to_dds((uint64_t)frequency_scaled * waveform_length, sample_rate);
230+
}
234231

235-
uint32_t accum = synth->accum[chan];
236-
uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT;
237-
if (dds_rate > lim / 2) {
238-
// beyond nyquist, can't play note
239-
continue;
240-
}
232+
uint32_t accum = synth->accum[chan];
233+
uint32_t lim = waveform_length << SYNTHIO_FREQUENCY_SHIFT;
234+
if (dds_rate > lim / 2) {
235+
// beyond nyquist, can't play note
236+
continue;
237+
}
241238

242-
// can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided
243-
if (accum > lim) {
244-
accum %= lim;
245-
}
239+
// can happen if note waveform gets set mid-note, but the expensive modulo is usually avoided
240+
if (accum > lim) {
241+
accum %= lim;
242+
}
246243

247-
for (uint16_t i = 0; i < dur; i++) {
248-
accum += dds_rate;
249-
// because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed
250-
if (accum > lim) {
251-
accum -= lim;
252-
}
253-
int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT;
254-
out_buffer[i] += (waveform[idx] * loudness) / 65536;
244+
for (uint16_t i = 0; i < dur; i++) {
245+
accum += dds_rate;
246+
// because dds_rate is low enough, the subtraction is guaranteed to go back into range, no expensive modulo needed
247+
if (accum > lim) {
248+
accum -= lim;
255249
}
256-
synth->accum[chan] = accum;
250+
int16_t idx = accum >> SYNTHIO_FREQUENCY_SHIFT;
251+
out_buffer32[i] += (waveform[idx] * loudness) / 65536;
257252
}
253+
synth->accum[chan] = accum;
254+
}
255+
256+
int16_t *out_buffer16 = (int16_t *)(void *)synth->buffers[synth->buffer_index];
257+
258+
// mix down audio
259+
for (size_t i = 0; i < dur; i++) {
260+
int32_t sample = out_buffer32[i];
261+
out_buffer16[i] = mix_down_sample(sample);
258262
}
259263

260264
// advance envelope states
@@ -267,7 +271,7 @@ void synthio_synth_synthesize(synthio_synth_t *synth, uint8_t **bufptr, uint32_t
267271
}
268272

269273
*buffer_length = synth->last_buffer_length = dur * SYNTHIO_BYTES_PER_SAMPLE;
270-
*bufptr = (uint8_t *)out_buffer;
274+
*bufptr = (uint8_t *)out_buffer16;
271275
}
272276

273277
void synthio_synth_reset_buffer(synthio_synth_t *synth, bool single_channel_output, uint8_t channel) {

tests/circuitpython-manual/synthio/note/code.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,25 @@ def synthesize4(synth):
8484
yield 36
8585

8686

87+
def synthesize5(synth):
88+
notes = [
89+
synthio.Note(
90+
frequency=synthio.midi_to_hz(60 + i + o),
91+
waveform=sine,
92+
envelope=envelope,
93+
)
94+
for i in [0, 4, 7]
95+
for o in [0, -12, 12]
96+
]
97+
98+
for n in notes:
99+
print(n)
100+
synth.press((n,))
101+
yield 120
102+
synth.release_all()
103+
yield 36
104+
105+
87106
def chain(*args):
88107
for a in args:
89108
yield from a
@@ -94,7 +113,7 @@ def chain(*args):
94113
f.setnchannels(1)
95114
f.setsampwidth(2)
96115
f.setframerate(48000)
97-
for n in chain(synthesize2(synth), synthesize3(synth), synthesize4(synth)):
116+
for n in chain(synthesize5(synth)):
98117
for i in range(n):
99118
result, data = audiocore.get_buffer(synth)
100119
f.writeframes(data)

tests/circuitpython/miditrack.py.exp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
(0, 1, 512, 1)
2-
1 [-16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16382, 16382]
2+
1 [-16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383]
33
(0, 1, 512, 1)
4-
1 [0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16382, 16382, 16382, 16382, 16382, 16382, 0, 0]
4+
1 [0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0, 0, 0, 0, 0, -16383, -16383, -16383, -16383, -16383, -16383, 0, 0, 0, 0, 0, 0, 16383, 16383, 16383, 16383, 16383, 16383, 0, 0]

tests/circuitpython/synthesizer.py.exp

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
()
22
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
33
(80,)
4-
[-16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383, 16382, 16382, 16382, 16382, 16382, -16383, -16383, -16383, -16383, -16383]
4+
[-16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383, 16383, 16383, 16383, 16383, 16383, -16383, -16383, -16383, -16383, -16383]
55
(80, 91)
6-
[0, 0, 16382, 16382, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0]
6+
[0, 0, 28045, 28045, 0, -28046, -28046, 0, 28045, 28045, 0, 0, 28045, 0, 0, -28046, -28046, 0, 28045, 28045, 0, 0, 28045, 0]
77
(91,)
8-
[-16382, 0, 0, 16382, 0, 0, 16382, 16382, 0, -16382, -16382, 0, 16382, 16382, 0, 0, 16382, 0, 0, -16382, -16382, -16382, 16382, 16382]
9-
(-5242, 5241)
10-
(-10484, 10484)
11-
(-15727, 15726)
12-
(-16383, 16382)
13-
(-14286, 14285)
14-
(-13106, 13105)
15-
(-13106, 13105)
16-
(-13106, 13105)
17-
(-13106, 13105)
18-
(-13106, 13105)
19-
(-13106, 13105)
20-
(-13106, 13105)
21-
(-13106, 13105)
22-
(-11009, 11008)
23-
(-8912, 8911)
24-
(-6815, 6814)
25-
(-4718, 4717)
26-
(-2621, 2620)
27-
(-524, 523)
8+
[-28046, 0, 0, 28045, 0, 0, 28045, 28045, 0, -28046, -28046, 0, 28045, 28045, 0, 0, 28045, 0, 0, -28046, -28046, -28046, 28045, 28045]
9+
(-5242, 5242)
10+
(-10485, 10484)
11+
(-15727, 15727)
12+
(-16383, 16383)
13+
(-14286, 14286)
14+
(-13106, 13106)
15+
(-13106, 13106)
16+
(-13106, 13106)
17+
(-13106, 13106)
18+
(-13106, 13106)
19+
(-13106, 13106)
20+
(-13106, 13106)
21+
(-13106, 13106)
22+
(-11009, 11009)
23+
(-8912, 8912)
24+
(-6815, 6815)
25+
(-4718, 4718)
26+
(-2621, 2621)
27+
(-524, 524)
2828
(0, 0)
2929
(0, 0)
3030
(0, 0)

0 commit comments

Comments
 (0)