diff --git a/pd/extra/bob~/GNUmakefile.am b/pd/extra/bob~/GNUmakefile.am new file mode 100644 index 0000000000000000000000000000000000000000..a9663c6559cb2371d1cd694ce2d56ead9df7880e --- /dev/null +++ b/pd/extra/bob~/GNUmakefile.am @@ -0,0 +1,30 @@ +## Makefile.am -- Process this file with automake to produce Makefile.in + +NAME=bob~ + +external_LTLIBRARIES = bob~.la +SOURCES = bob~.c +PATCHES = bob~-help.pd output~.pd +OTHERDATA = + +EXTRA_DIST = makefile + +############################### +# you shouldn't need to add anything below here +dist_external_DATA = $(PATCHES) $(OTHERDATA) + +AUTOMAKE_OPTIONS = foreign +AM_CPPFLAGS = -I$(top_srcdir)/src -DPD +AM_CFLAGS = @ARCH_CFLAGS@ +AM_LIBS = $(LIBM) +AM_LDFLAGS = -module -avoid-version -shared @ARCH_LDFLAGS@ -shrext .@EXTERNAL_EXTENSION@ -L$(top_srcdir)/src + +externaldir = $(pkglibdir)/extra/$(NAME) + + +if MINGW +AM_LIBS += -lpd +endif + +libtool: $(LIBTOOL_DEPS) + $(SHELL) ./config.status --recheck diff --git a/pd/extra/bob~/README.txt b/pd/extra/bob~/README.txt new file mode 100644 index 0000000000000000000000000000000000000000..0e5c5b273d0c1c878bf8c32fad220fc8ae1d4338 --- /dev/null +++ b/pd/extra/bob~/README.txt @@ -0,0 +1,35 @@ +The bob~ object. BSD licensed; Copyright notice is in bob~ source code. + +Imitates a Moog resonant filter by Runge-Kutte numerical integration of +a differential equation approximately describing the dynamics of the circuit. + +Useful references: + +Tim Stilson +Analyzing the Moog VCF with Considerations for Digital Implementation +https://ccrma.stanford.edu/~stilti/papers/moogvcf.ps.gz + +(sections 1 and 2 are a reasonably good introduction but the model they use +is highly idealized.) + +Timothy E. Stinchcombe +Analysis of the Moog Transistor Ladder and Derivative Filters + +(long, but a very thorough description of how the filter works including +its nonlinearities) + +Antti Huovilainen +Non-linear digital implementation of the moog ladder filter + +(comes close to giving a differential equation for a reasonably realistic +model of the filter). + +Th differential equations are: + +y1' = k * (S(x - r * y4) - S(y1)) +y2' = k * (S(y1) - S(y2)) +y3' = k * (S(y2) - S(y3)) +y4' = k * (S(y3) - S(y4)) + +where k controls the cutoff frequency, r is feedback (<= 4 for +stability), and S(x) is a saturation function. diff --git a/pd/extra/bob~/bob~-help.pd b/pd/extra/bob~/bob~-help.pd new file mode 100644 index 0000000000000000000000000000000000000000..9a0de974d652aca8619a07b5681d9c5209da4720 --- /dev/null +++ b/pd/extra/bob~/bob~-help.pd @@ -0,0 +1,158 @@ +#N canvas 27 58 1062 722 12; +#X obj 231 347 env~ 8192, f 4; +#X floatatom 230 387 5 0 0 0 - - -, f 5; +#X floatatom 408 193 5 0 200 0 - - -, f 5; +#X obj 39 260 env~ 8192, f 5; +#X floatatom 39 300 5 0 0 0 - - -, f 5; +#X obj 279 317 bob~; +#X obj 408 215 / 25; +#X msg 950 229 print; +#X obj 87 259 output~; +#X floatatom 291 179 5 0 150 0 - - -, f 5; +#X obj 291 201 mtof; +#X obj 292 246 pack 0 50; +#X obj 292 271 line~; +#X msg 886 227 clear; +#X obj 280 349 output~; +#X floatatom 291 224 7 0 0 0 - - -, f 7; +#X floatatom 611 177 5 0 999 0 - - -, f 5; +#X msg 611 223 saturation \$1; +#X obj 611 127 loadbang; +#X obj 611 199 / 100; +#X text 885 183 clear or print; +#X text 889 202 filter state; +#X floatatom 748 197 5 1 10 0 - - -, f 5; +#X text 744 122 oversampling; +#X msg 748 224 oversample \$1; +#X text 419 88 "resonance"; +#X text 418 105 (>4 to oscillate); +#X obj 748 145 loadbang; +#X msg 748 170 2; +#X text 456 211 scaled to 0-8; +#X text 455 193 0-200 control; +#X text 263 86 resonant or cutoff frequency, f 16; +#X text 300 60 ----- filter parameters ----; +#X text 609 59 ------ optimizations / setup params -------; +#X text 899 161 debugging:; +#X text 603 88 saturation point; +#X text 600 105 of "transistors"; +#X msg 611 152 300; +#X obj 408 142 loadbang; +#X msg 408 167 10; +#X text 521 625 "Clear" momentarily shorts out the capacitors in case +the filter has gone unstable and stopped working.; +#X text 523 410 By default bob~ does one step of 4th-order Runge-Kutte +integration per audio sample. This works OK for resonant/cutoff frequencies +up to about 1/2 Nyquist. To improve accuracy and/or to extend the range +of the filter to higher cutoff frequencies you can oversample by any +factor - but note that computation time rises accordingly. At high +cutoff frequencies/resonance values the RK approximation can go unstable. +You can combat this by raising the oversampling factor.; +#X obj 407 243 line~; +#N canvas 743 303 450 300 test 0; +#X obj 102 122 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0 +1; +#X obj 313 127 min~; +#X obj 357 103 -~ 1; +#X obj 357 128 *~ -50; +#X floatatom 102 102 5 0 128 0 - - -, f 5; +#X obj 235 72 mtof; +#X obj 102 141 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 1 +1; +#X text 196 32 test signal; +#X text 147 102 pitch; +#X text 119 140 sawtooth; +#X obj 312 70 phasor~ 220; +#X obj 233 107 osc~ 220; +#X obj 102 160 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0 +1; +#X text 121 120 sine; +#X text 120 159 noise; +#X floatatom 103 181 3 0 100 0 - - -, f 3; +#X obj 62 24 loadbang; +#X msg 62 49 57; +#X msg 48 163 80; +#X text 133 180 dB out; +#X msg 60 111 1; +#X obj 362 189 noise~; +#X obj 204 207 *~ 0; +#X obj 316 184 *~ 0; +#X obj 315 217 *~ 0; +#X obj 208 238 +~; +#X obj 208 263 outlet~; +#X connect 0 0 22 1; +#X connect 1 0 23 0; +#X connect 2 0 3 0; +#X connect 3 0 1 1; +#X connect 4 0 5 0; +#X connect 5 0 10 0; +#X connect 5 0 11 0; +#X connect 6 0 23 1; +#X connect 10 0 1 0; +#X connect 10 0 2 0; +#X connect 11 0 22 0; +#X connect 12 0 24 1; +#X connect 16 0 17 0; +#X connect 16 0 18 0; +#X connect 16 0 20 0; +#X connect 17 0 4 0; +#X connect 18 0 15 0; +#X connect 20 0 6 0; +#X connect 21 0 24 0; +#X connect 22 0 25 0; +#X connect 23 0 25 1; +#X connect 24 0 25 1; +#X connect 25 0 26 0; +#X coords 0 -1 1 1 95 100 2 100 100; +#X restore 101 96 pd test; +#X text 228 410 output monitor; +#X text 35 321 input monitor; +#X obj 291 130 loadbang; +#X msg 291 155 69; +#X text 47 65 ----- test input ----; +#X text 356 366 <--- adjust output "dB" to hear filter output.; +#X text 21 558 The design is based on papers by Tim Stilson \, Timothy +E. Stinchcombe \, and Antti Huovilainen. See README.txt for pointers. +; +#X text 23 459 The three audio inputs are the signal to filter \, the +cutoff/resonant frequency in cycles per second \, and "resonance" (the +sharpness of the filter). Nominally \, a resonance of 4 should be the +limit of stability -- above that \, the filter oscillates.; +#X text 24 10 bob~ - Runge-Kutte numerical simulation of the Moog analog +resonant filter, f 79; +#X text 876 676 updated for Pd 0.47; +#X text 522 565 The saturation parameter determines at what signal +level the "transistors" in the model saturate. The maximum output amplitude +is about 2/3 of that value.; +#X connect 0 0 1 0; +#X connect 2 0 6 0; +#X connect 3 0 4 0; +#X connect 5 0 0 0; +#X connect 5 0 14 0; +#X connect 5 0 14 1; +#X connect 6 0 42 0; +#X connect 7 0 5 0; +#X connect 9 0 10 0; +#X connect 10 0 15 0; +#X connect 11 0 12 0; +#X connect 12 0 5 1; +#X connect 13 0 5 0; +#X connect 15 0 11 0; +#X connect 16 0 19 0; +#X connect 17 0 5 0; +#X connect 18 0 37 0; +#X connect 19 0 17 0; +#X connect 22 0 24 0; +#X connect 24 0 5 0; +#X connect 27 0 28 0; +#X connect 28 0 22 0; +#X connect 37 0 16 0; +#X connect 38 0 39 0; +#X connect 39 0 2 0; +#X connect 42 0 5 2; +#X connect 43 0 8 0; +#X connect 43 0 8 1; +#X connect 43 0 3 0; +#X connect 43 0 5 0; +#X connect 46 0 47 0; +#X connect 47 0 9 0; diff --git a/pd/extra/bob~/bob~.c b/pd/extra/bob~/bob~.c new file mode 100644 index 0000000000000000000000000000000000000000..d28371c51e61219fe757157a083f250acf4ce9df --- /dev/null +++ b/pd/extra/bob~/bob~.c @@ -0,0 +1,255 @@ +/* bob~ - use a differential equation solver to imitate an analogue circuit */ + +/* copyright 2015 Miller Puckette - BSD license */ + +#include "m_pd.h" +#include <math.h> +#define DIM 4 +#define FLOAT double + +/* if CALCERROR is defined we compute an error estaimate to verify +the filter, outputting it from a second outlet on demand. This +doubles the computation time, so it's only compiled in for testing. */ + +/* #define CALCERROR */ + +typedef struct _params +{ + FLOAT p_input; + FLOAT p_cutoff; + FLOAT p_resonance; + FLOAT p_saturation; + FLOAT p_derivativeswere[DIM]; +} t_params; + + /* imitate the (tanh) clipping function of a transistor pair. We + hope/assume the C compiler is smart enough to inline this so use + a function instead of a #define. */ +#if 0 +static FLOAT clip(FLOAT value, FLOAT saturation, FLOAT saturationinverse) +{ + return (saturation * tanh(value * saturationinverse)); +} +#else + /* cheaper way - to 4th order, tanh is x - x*x*x/3; this cubic's + plateaus are at +/- 1 so clip to 1 and evaluate the cubic. + This is pretty coarse - for instance if you clip a sinusoid this way you + can sometimes hear the discontinuity in 4th derivative at the clip point */ +static FLOAT clip(FLOAT value, FLOAT saturation, FLOAT saturationinverse) +{ + float v2 = (value*saturationinverse > 1 ? 1 : + (value*saturationinverse < -1 ? -1: + value*saturationinverse)); + return (saturation * (v2 - (1./3.) * v2 * v2 * v2)); +} +#endif + +static void calc_derivatives(FLOAT *dstate, FLOAT *state, t_params *params) +{ + FLOAT k = ((float)(2*3.14159)) * params->p_cutoff; + FLOAT sat = params->p_saturation, satinv = 1./sat; + FLOAT satstate0 = clip(state[0], sat, satinv); + FLOAT satstate1 = clip(state[1], sat, satinv); + FLOAT satstate2 = clip(state[2], sat, satinv); + dstate[0] = k * + (clip(params->p_input - params->p_resonance * state[3], sat, satinv) + - satstate0); + dstate[1] = k * (satstate0 - satstate1); + dstate[2] = k * (satstate1 - satstate2); + dstate[3] = k * (satstate2 - clip(state[3], sat, satinv)); +} + +static void solver_euler(FLOAT *state, FLOAT *errorestimate, + FLOAT stepsize, t_params *params) +{ + FLOAT cumerror = 0; + int i; + FLOAT derivatives[DIM]; + calc_derivatives(derivatives, state, params); + *errorestimate = 0; + for (i = 0; i < DIM; i++) + { + state[i] += stepsize * derivatives[i]; + *errorestimate += (derivatives[i] > params->p_derivativeswere[i] ? + derivatives[i] - params->p_derivativeswere[i] : + params->p_derivativeswere[i] - derivatives[i]); + } + for (i = 0; i < DIM; i++) + params->p_derivativeswere[i] = derivatives[i]; +} + +static void solver_rungekutte(FLOAT *state, FLOAT *errorestimate, + FLOAT stepsize, t_params *params) +{ + FLOAT cumerror = 0; + int i; + FLOAT deriv1[DIM], deriv2[DIM], deriv3[DIM], deriv4[DIM], tempstate[DIM]; + FLOAT oldstate[DIM], backstate[DIM]; +#if CALCERROR + for (i = 0; i < DIM; i++) + oldstate[i] = state[i]; +#endif + *errorestimate = 0; + calc_derivatives(deriv1, state, params); + for (i = 0; i < DIM; i++) + tempstate[i] = state[i] + 0.5 * stepsize * deriv1[i]; + calc_derivatives(deriv2, tempstate, params); + for (i = 0; i < DIM; i++) + tempstate[i] = state[i] + 0.5 * stepsize * deriv2[i]; + calc_derivatives(deriv3, tempstate, params); + for (i = 0; i < DIM; i++) + tempstate[i] = state[i] + stepsize * deriv3[i]; + calc_derivatives(deriv4, tempstate, params); + for (i = 0; i < DIM; i++) + state[i] += (1./6.) * stepsize * + (deriv1[i] + 2 * deriv2[i] + 2 * deriv3[i] + deriv4[i]); +#if CALCERROR + calc_derivatives(deriv1, state, params); + for (i = 0; i < DIM; i++) + tempstate[i] = state[i] - 0.5 * stepsize * deriv1[i]; + calc_derivatives(deriv2, tempstate, params); + for (i = 0; i < DIM; i++) + tempstate[i] = state[i] - 0.5 * stepsize * deriv2[i]; + calc_derivatives(deriv3, tempstate, params); + for (i = 0; i < DIM; i++) + tempstate[i] = state[i] - stepsize * deriv3[i]; + calc_derivatives(deriv4, tempstate, params); + for (i = 0; i < DIM; i++) + { + backstate[i] = state[i ]- (1./6.) * stepsize * + (deriv1[i] + 2 * deriv2[i] + 2 * deriv3[i] + deriv4[i]); + *errorestimate += (backstate[i] > oldstate[i] ? + backstate[i] - oldstate[i] : oldstate[i] - backstate[i]); + } +#endif +} + +typedef struct _bob +{ + t_object x_obj; + t_float x_f; + t_outlet *x_out1; /* signal output */ +#ifdef CALCERROR + t_outlet *x_out2; /* error estimate */ + FLOAT x_cumerror; +#endif + t_params x_params; + FLOAT x_state[DIM]; + FLOAT x_sr; + int x_oversample; + int x_errorcount; +} t_bob; + +static t_class *bob_class; + +static void bob_saturation(t_bob *x, t_float saturation) +{ + if (saturation <= 1e-3) + saturation = 1e-3; + x->x_params.p_saturation = saturation; +} + +static void bob_oversample(t_bob *x, t_float oversample) +{ + if (oversample <= 1) + oversample = 1; + x->x_oversample = oversample; +} + +static void bob_clear(t_bob *x) +{ + int i; + for (i = 0; i < DIM; i++) + x->x_state[i] = x->x_params.p_derivativeswere[i] = 0; +} + +static void bob_error(t_bob *x) +{ +#ifdef CALCERROR + outlet_float(x->x_out2, + (x->x_errorcount ? x->x_cumerror/x->x_errorcount : 0)); + x->x_cumerror = 0; + x->x_errorcount = 0; +#else + post("error estimate unavailable (not compiled in)"); +#endif +} + +static void bob_print(t_bob *x) +{ + int i; + for (i = 0; i < DIM; i++) + post("state %d: %f", i, x->x_state[i]); + post("saturation %f", x->x_params.p_saturation); + post("oversample %d", x->x_oversample); +} + +static void *bob_new( void) +{ + t_bob *x = (t_bob *)pd_new(bob_class); + x->x_out1 = outlet_new(&x->x_obj, gensym("signal")); + inlet_new(&x->x_obj, &x->x_obj.ob_pd, &s_signal, &s_signal); + inlet_new(&x->x_obj, &x->x_obj.ob_pd, &s_signal, &s_signal); + x->x_f = 0; + bob_clear(x); + bob_saturation(x, 3); + bob_oversample(x, 2); +#ifdef CALCERROR + x->x_cumerror = 0; + x->x_errorcount = 0; + x->x_out2 = outlet_new(&x->x_obj, gensym("float")); +#endif + return (x); +} + +static t_int *bob_perform(t_int *w) +{ + t_bob *x = (t_bob *)(w[1]); + t_float *in1 = (t_float *)(w[2]); + t_float *cutoffin = (t_float *)(w[3]); + t_float *resonancein = (t_float *)(w[4]); + t_float *out = (t_float *)(w[5]); + int n = (int)(w[6]), i, j; + FLOAT stepsize = 1./(x->x_oversample * x->x_sr); + FLOAT errorestimate; + for (i = 0; i < n; i++) + { + x->x_params.p_input = *in1++; + x->x_params.p_cutoff = *cutoffin++; + if ((x->x_params.p_resonance = *resonancein++) < 0) + x->x_params.p_resonance = 0; + for (j = 0; j < x->x_oversample; j++) + solver_rungekutte(x->x_state, &errorestimate, + stepsize, &x->x_params); + *out++ = x->x_state[0]; +#if CALCERROR + x->x_cumerror += errorestimate; + x->x_errorcount++; +#endif + } + return (w+7); +} + +static void bob_dsp(t_bob *x, t_signal **sp) +{ + x->x_sr = sp[0]->s_sr; + dsp_add(bob_perform, 6, x, sp[0]->s_vec, sp[1]->s_vec, + sp[2]->s_vec, sp[3]->s_vec, sp[0]->s_n); +} + +void bob_tilde_setup(void) +{ + int i; + bob_class = class_new(gensym("bob~"), + (t_newmethod)bob_new, 0, sizeof(t_bob), 0, 0); + class_addmethod(bob_class, (t_method)bob_saturation, gensym("saturation"), + A_FLOAT, 0); + class_addmethod(bob_class, (t_method)bob_oversample, gensym("oversample"), + A_FLOAT, 0); + class_addmethod(bob_class, (t_method)bob_clear, gensym("clear"), 0); + class_addmethod(bob_class, (t_method)bob_print, gensym("print"), 0); + class_addmethod(bob_class, (t_method)bob_error, gensym("error"), 0); + + class_addmethod(bob_class, (t_method)bob_dsp, gensym("dsp"), A_CANT, 0); + CLASS_MAINSIGNALIN(bob_class, t_bob, x_f); +} diff --git a/pd/extra/bob~/makefile b/pd/extra/bob~/makefile new file mode 100644 index 0000000000000000000000000000000000000000..77070484c24714d1e7c007939d5f1c77ebeb9f2e --- /dev/null +++ b/pd/extra/bob~/makefile @@ -0,0 +1,4 @@ +NAME=bob~ +CSYM=bob_tilde + +include ../makefile.subdir diff --git a/pd/extra/bob~/output~.pd b/pd/extra/bob~/output~.pd new file mode 100644 index 0000000000000000000000000000000000000000..af13b0712cd959de96b8a16bc5791b175bec5062 --- /dev/null +++ b/pd/extra/bob~/output~.pd @@ -0,0 +1,130 @@ +#N canvas 96 14 463 390 10; +#X obj 12 110 hsl 63 18 0.01 1 1 0 \$0-v \$0-v volume 20 10 1 9 -245500 +-13381 -1 150 0; +#X obj 80 92 tgl 18 0 THIS_IS_HERE_TO_GET_RID_OF_THE_OUTLET \$0-dsp-toggle +dsp 2 9 1 9 -225271 -195568 -33289 1 1; +#N canvas 366 412 482 356 dsp 0; +#X obj 11 7 inlet; +#X obj 92 226 select 0 1; +#X msg 125 248 6; +#X obj 92 57 route dsp; +#X obj 92 36 receive pd; +#X obj 206 138 loadbang; +#X msg 11 220 dsp \$1; +#X obj 11 245 send pd; +#X msg 206 278 set \$1; +#X obj 206 174 value GLOBAL_PDDP_DSP; +#X msg 109 278 color \$1 20 12; +#X obj 180 309 send \$0-dsp-toggle; +#X obj 92 115 change; +#X msg 92 247 0; +#X connect 0 0 6 0; +#X connect 0 0 12 0; +#X connect 1 0 13 0; +#X connect 1 1 2 0; +#X connect 2 0 10 0; +#X connect 3 0 12 0; +#X connect 4 0 3 0; +#X connect 5 0 9 0; +#X connect 6 0 7 0; +#X connect 8 0 11 0; +#X connect 9 0 8 0; +#X connect 9 0 1 0; +#X connect 10 0 11 0; +#X connect 12 0 8 0; +#X connect 12 0 1 0; +#X connect 12 0 9 0; +#X connect 13 0 10 0; +#X restore 112 118 pd dsp logic; +#X obj 315 2 inlet; +#X obj 80 110 bng 18 1000 50 0 THIS_IS_HERE_TO_GET_RID_OF_THE_OUTLET +\$0-MUTE_TOGGLE empty 0 9 2 8 -262144 -258699 -195568; +#X obj 191 2 inlet~; +#X obj 86 273 line~; +#X obj 186 333 *~; +#X obj 206 363 dac~; +#X text 203 22 audio in; +#X obj 254 2 inlet~; +#X obj 248 332 *~; +#X obj 201 73 hip~ 3; +#X obj 263 73 hip~ 3; +#X obj 12 288 send pd; +#X msg 12 267 dsp 1; +#X obj 248 362 outlet~; +#X obj 148 362 outlet~; +#X obj 355 362 outlet; +#X obj 86 252 pack 0 50; +#X text 153 251 <-- make a ramp to avoid clicks or zipper noise; +#X msg 86 217 0; +#X obj 86 194 moses 0.011; +#X text 307 74 filter out DC; +#N canvas 148 311 361 328 mute 0; +#X obj 23 20 inlet; +#X obj 173 20 inlet; +#X obj 222 208 float; +#X obj 265 121 tgl 15 1 empty empty empty 17 7 0 10 -262144 -1 -1 1 +1; +#X obj 222 162 spigot; +#X obj 172 41 trigger bang bang; +#X obj 254 263 outlet; +#X msg 274 208 0; +#X obj 274 163 select 0; +#X obj 127 64 bang; +#X msg 127 85 set 1; +#X obj 65 304 send \$0-MUTE_TOGGLE; +#X msg 65 283 color \$1 13 20; +#X obj 65 235 bang; +#X msg 65 255 0; +#X msg 98 255 3; +#X connect 0 0 2 1; +#X connect 0 0 9 0; +#X connect 1 0 5 0; +#X connect 2 0 6 0; +#X connect 2 0 13 0; +#X connect 3 0 4 1; +#X connect 3 0 8 0; +#X connect 4 0 2 0; +#X connect 5 0 4 0; +#X connect 5 1 3 0; +#X connect 7 0 6 0; +#X connect 8 0 7 0; +#X connect 8 0 15 0; +#X connect 9 0 10 0; +#X connect 9 0 13 0; +#X connect 10 0 3 0; +#X connect 12 0 11 0; +#X connect 13 0 14 0; +#X connect 14 0 12 0; +#X connect 15 0 12 0; +#X restore 86 148 pd mute; +#X obj 315 25 t f f; +#X connect 0 0 15 0; +#X connect 0 0 18 0; +#X connect 0 0 22 0; +#X connect 0 0 24 0; +#X connect 1 0 2 0; +#X connect 3 0 25 0; +#X connect 4 0 24 1; +#X connect 5 0 12 0; +#X connect 6 0 11 0; +#X connect 6 0 7 0; +#X connect 7 0 8 0; +#X connect 7 0 17 0; +#X connect 10 0 13 0; +#X connect 11 0 8 1; +#X connect 11 0 16 0; +#X connect 12 0 7 1; +#X connect 13 0 11 1; +#X connect 15 0 14 0; +#X connect 19 0 6 0; +#X connect 21 0 19 0; +#X connect 22 0 21 0; +#X connect 22 1 19 0; +#X connect 24 0 22 0; +#X connect 24 0 0 0; +#X connect 25 0 24 0; +#X connect 25 0 18 0; +#X connect 25 0 22 0; +#X connect 25 0 15 0; +#X connect 25 1 0 0; +#X coords 0 0 1 1 90 40 1 10 90;