commit 436441120de6e062654ee883f9f671a832b665a9
parent ae120b537f186f631aaf681bc68ea25377c45545
Author: rpa <rpa@laika>
Date: Wed, 4 Jan 2023 19:35:40 +0000
src/wave: bits and pieces of wavetable synth
Diffstat:
10 files changed, 887 insertions(+), 0 deletions(-)
diff --git a/src/wave/draw.c b/src/wave/draw.c
@@ -0,0 +1,371 @@
+#include <u.h>
+#include <libc.h>
+#include <draw.h>
+#include <thread.h>
+#include <mouse.h>
+#include <keyboard.h>
+
+#include "util.h"
+
+#define DefLength 169
+/*
+ * This DefLength is equal to standard sample rate of 44100
+ * divided by C4 note from chromatic scale frequency of 261.626
+ */
+
+char *path;
+int autosaveflag, dirtyflag;
+u32int pcm[MaxLength];
+usize length;
+Mousectl *mctl;
+Keyboardctl *kctl;
+Image *bg, *lo, *hi;
+
+int readpcm(void);
+int writepcm(void);
+void keyboard(Rune);
+void mouse(Mouse);
+void resize(int *);
+void redraw(u32int *, u32int *);
+
+void sine(void);
+void noise(void);
+
+void save(void);
+
+
+void (*menucmd[])(void) = {
+ sine, noise
+};
+
+char *menuitems[] = {
+ "sine",
+ "noise",
+ nil,
+};
+
+Menu menu = {menuitems, nil, 0};
+
+void
+usage(void)
+{
+ fprint(2, "usage: [-a] %s [file]\n", argv0);
+ threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+ ARGBEGIN{
+ case 'a':
+ autosaveflag = 1;
+ break;
+ default:
+ usage();
+ }ARGEND
+
+ if (argc > 1) usage();
+ else if (argc == 0) {
+ path = strdup("");
+ length = DefLength;
+ sine();
+ } else {
+ path = strdup(argv[0]);
+ if (readpcm() != 0) {
+ fprint(2, "failed to open file %s: %r\n", argv[0]);
+ threadexitsall("failed to open file");
+ }
+ }
+
+ if (initdraw(nil, nil, "pcm/draw") < 0) sysfatal("initdraw: %r");
+ if ((mctl = initmouse(nil, screen)) == nil) sysfatal("initmouse: %r");
+ if ((kctl = initkeyboard(nil)) == nil) sysfatal("initkeyboard: %r");
+
+ bg = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DBlack);
+ lo = allocimagemix(display, DRed, DBlack);
+ hi = allocimage(display, Rect(0,0,1,1), screen->chan, 1, DRed);
+
+ draw(screen, screen->r, bg, nil, ZP);
+ redraw(nil, nil);
+ flushimage(display, 1);
+
+ Rune kv;
+ Mouse mv;
+ int rv[2];
+
+ Alt alts[] = {
+ {kctl->c, &kv, CHANRCV},
+ {mctl->c, &mv, CHANRCV},
+ {mctl->resizec, rv, CHANRCV},
+ {nil, nil, CHANEND},
+ };
+
+ for (;;) {
+ switch (alt(alts)) {
+ case 0:
+ keyboard(kv);
+ break;
+ case 1:
+ mouse(mv);
+ break;
+ case 2:
+ resize(rv);
+ break;
+ }
+ }
+}
+
+int
+readpcm(void)
+{
+ int fd;
+ if ((fd = open(path, OREAD)) < 0) {
+ fprint(2, "failed to open %s: %r\n", path);
+ return 1;
+ }
+ length = MaxLength;
+ length = read(fd, pcm, MaxLength * 4) / 4;
+ close(fd);
+ return 0;
+}
+
+int
+writepcm(void)
+{
+ int fd;
+ if ((fd = create(path, OWRITE, 0666)) < 0) {
+ fprint(2, "failed to open %s: %r\n", path);
+ return 1;
+ }
+ write(fd, pcm, length * 4);
+ close(fd);
+ return 0;
+}
+
+void
+keyboard(Rune rv)
+{
+ usize newlength;
+ char *rpt, ebuf[1024];
+
+ switch (rv) {
+ case 'r':
+ case 'w':
+ snprint(ebuf, sizeof(ebuf), "%C%s", rv, path);
+ break;
+ case 'l':
+ snprint(ebuf, sizeof(ebuf), "%C%ulld", rv, length);
+ break;
+ default:
+ snprint(ebuf, sizeof(ebuf), "%C", rv);
+ }
+
+ if (enter("% ", ebuf, sizeof(ebuf), mctl, kctl, nil) < 0) return;
+
+ switch(ebuf[0]) {
+ case 'q':
+ threadexitsall(nil);
+ break;
+ case 'w':
+ free(path);
+ path = strdup(ebuf + 1);
+ writepcm();
+ break;
+ case 'r':
+ free(path);
+ path = strdup(ebuf + 1);
+ readpcm();
+ redraw(nil, nil);
+ flushimage(display, 1);
+ break;
+ case 'l':
+ newlength = strtoull(ebuf + 1, &rpt, 0);
+ if ((rpt != ebuf + 1) && (newlength < MaxLength)) {
+ usize oldlength = length;
+ length = newlength;
+ int i;
+ if (oldlength != 0) {
+ for (i = oldlength; i < newlength; i++) pcm[i] = pcm[i%oldlength];
+ }
+ if (oldlength < newlength) {
+ redraw(pcm + oldlength, pcm + newlength);
+ } else {
+ draw(screen, rectaddpt(screen->r, Pt(newlength, 0)), bg, nil, ZP);
+ }
+ flushimage(display, 1);
+ }
+ }
+}
+
+void
+mouse(Mouse mv)
+{
+ u32int *buf;
+ Frame n;
+ int x, i, bsize, dir, h;
+ static Point oldpt;
+ Point pt = subpt(mv.xy, screen->r.min);
+ double d, val;
+
+ h = Dy(screen->r);
+ val = 1 - 2 * (double)pt.y / (double)h;
+
+ if (val < -1) val = -1;
+ if (val > 1) val = 1;
+ pt.y = val * (double)0x7fff;
+
+ switch (mv.buttons) {
+ case 0:
+ save();
+ oldpt = pt;
+ break;
+ case 1:
+ /* draw line in pcm from oldpt to pt) */
+ bsize = pt.x - oldpt.x;
+ dir = 1;
+ if (bsize < 0) bsize = -bsize, dir = -1;
+ for (
+ i = 0, x = oldpt.x;
+ x != pt.x;
+ i++, x += dir) {
+ d = (double)i / (double)bsize;
+ n.c1 = (pt.y * d + oldpt.y * (1 - d));
+ n.c2 = n.c1;
+ if ((x >= 0) && (x < length)) {
+ Frame *fr = (Frame *)&pcm[x];
+ fr->c1 = n.c1;
+ fr->c2 = n.c2;
+ }
+ }
+ if ((pt.x >= 0) && (pt.x < length)) {
+ Frame *fr = (Frame *)&pcm[pt.x];
+ fr->c1 = pt.y;
+ fr->c2 = pt.y;
+ }
+
+ if (dir > 0) {
+ redraw(pcm + oldpt.x, pcm + pt.x + 2);
+ } else {
+ redraw(pcm + pt.x, pcm + oldpt.x + 2);
+ }
+ flushimage(display, 1);
+
+ oldpt = pt;
+ dirtyflag = 1;
+ break;
+ case 2:
+ /* shift pcm data left/right */
+ bsize = oldpt.x - pt.x;
+ while (bsize < 0) bsize += length;
+ bsize = (length == 0) ? 0 : bsize % length;
+
+ buf = malloc(length * 4);
+
+ memcpy(buf, pcm + bsize, (length - bsize) * 4);
+ memcpy(buf + length - bsize, pcm, (bsize) * 4);
+ memcpy(pcm, buf, length * 4);
+ free(buf);
+
+ redraw(nil, nil);
+ flushimage(display, 1);
+
+ oldpt = pt;
+ dirtyflag = 1;
+ break;
+ case 4:
+ /* menu */
+ x = menuhit(3, mctl, &menu, nil);
+ if (x >= 0) {
+ menucmd[x]();
+ redraw(nil, nil);
+ flushimage(display, 1);
+ dirtyflag = 1;
+ }
+ break;
+ }
+}
+
+void
+resize(int *)
+{
+ if (getwindow(display, Refnone) < 0) sysfatal("resize: %r");
+ draw(screen, screen->r, bg, nil, ZP);
+ redraw(nil, nil);
+ flushimage(display, 1);
+}
+
+void
+redraw(u32int *start, u32int *end)
+{
+ Frame *fr;
+ int h = Dy(screen->r);
+ int c = screen->r.min.y + h / 2;
+ int x, yv, yp = c;
+
+ if (start == nil) start = pcm;
+ if (end == nil) end = &pcm[length];
+ if (start > end) {
+ void *b;
+ b = end, end = start, start = b;
+ }
+ if (start > pcm) {
+ fr = (Frame *)(start - 1);
+ yp = c - (double)h * ((double)(fr->c1) / (double)0xffff);
+ }
+ if (start < pcm) start = pcm;
+ if (end > pcm + length) end = pcm + length;
+
+ u32int *p;
+ for (p = start, x = screen->r.min.x + (p - pcm); p < end; p++, x++) {
+ if (x > screen->r.max.x) break;
+
+ fr = (Frame *)p;
+ yv = c - (double)h * ((double)(fr->c1) / (double)0xffff);
+
+ Rectangle rlo, rhi;
+
+ rlo = canonrect(Rect(x, yv, x + 1, c));
+ rhi = canonrect(Rect(x, yv, x + 1, yp));
+ if (rhi.min.y == rhi.max.y) rhi.min.y--, rhi.max.y++;
+
+ draw(screen, Rect(x, screen->r.min.y, x + 1, screen->r.max.y), bg, nil, ZP);
+ draw(screen, rlo, lo, nil, ZP);
+ draw(screen, rhi, hi, nil, ZP);
+
+ yp = yv;
+ }
+}
+
+void
+sine(void)
+{
+ int i;
+ Frame *fr;
+ for (i = 0; i < length; i++) {
+ fr = (Frame *)&pcm[i];
+ fr->c1 = 0x7fff * sin((double)i / (double)length * PI * 2.0);
+ fr->c2 = fr->c1;
+ }
+}
+
+void
+noise(void)
+{
+ int i;
+ Frame *fr;
+ srand(time(0) + lrand());
+ for (i = 0; i < length; i++) {
+ fr = (Frame *)&pcm[i];
+ fr->c1 = lrand() & 0xFFFF;
+ fr->c2 = fr->c1;
+ }
+}
+
+void
+save(void)
+{
+ if ((autosaveflag > 0) && (dirtyflag > 0)) {
+ writepcm();
+ dirtyflag = 0;
+ }
+}
diff --git a/src/wave/fade.c b/src/wave/fade.c
@@ -0,0 +1,111 @@
+#include <u.h>
+#include <libc.h>
+
+#include "util.h"
+
+#define DefLength 8957
+/*
+ * Given 44100 samplerate, this DefLength should result
+ * in roughly 0.2 ms long output, which should be around
+ * the point where changes in timbre start to be perceptable
+ * by human ear.
+ *
+ * Probably.
+ *
+ * It is also a whole number multiple of draw's DefLength,
+ * which should give us product that loops nicely.
+ */
+
+
+Aud *files, output, envelope;
+int fcount;
+
+void load(Aud *aud, char *path);
+double env(double);
+double slope(double);
+double slope(double);
+void apply(Aud *, Aud *, double);
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s [-e file] [-n count] wavefile1 wavefile2 ...\n", argv0);
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ output.n = DefLength;
+
+ ARGBEGIN {
+ case 'n':
+ output.n = strtol(EARGF(usage()), nil, 0);
+ break;
+ default:
+ usage();
+ } ARGEND
+ if (argc < 2) usage();
+
+ fcount = argc;
+ files = malloc(sizeof(Aud) * fcount);
+
+ int i;
+ for (i = 0; i < fcount; i++) {
+ load(files + i, argv[i]);
+ apply(&output, &files[i], - (double)i);
+ }
+ write(1, output.p, output.n * 4);
+}
+
+void
+load(Aud *aud, char *path)
+{
+ int fd;
+ if ((fd = open(path, OREAD)) < 0) sysfatal("load: %r");
+ if ((aud->n = read(fd, aud->p, sizeof(aud->p))) < 0) sysfatal("load: %r");
+ aud->n = aud->n / 4;
+ close(fd);
+}
+
+void
+apply(Aud *dst, Aud *src, double dx)
+{
+ int i, j;
+ Frame *fs, *fd;
+ for (i = 0; i < output.n; i++) {
+
+ double p = ((double)fcount - 1) * (double)i / (double)output.n;
+ double v = slope(p + dx);
+
+ j = i % src->n;
+
+ fd = (Frame *)&dst->p[i];
+ fs = (Frame *)&src->p[j];
+ fd->c[0] += fs->c[0] * v;
+ fd->c[1] += fs->c[1] * v;
+ }
+}
+
+double
+slope(double x)
+{
+ /* TODO: maybe I should use sine-shaped slope instead of triangle one */
+ if (x < 0) x = -x;
+ if (x > 1) x = 1;
+ return 1 - x;
+}
+
+double
+env(double I)
+{
+ return I;
+
+ /*int i;
+ double delta, a ,b;
+ i = (int)(I * tolen(envelope.n)) & (-2);
+ delta = (I * tolen(envelope.n)) - (double)i;
+ a = (double)(envelope.p[i] + envelope.p[i + 1]) / 4.0 / (double)0x7fff + 0.5;
+ b = (double)(envelope.p[i + 2] + envelope.p[i + 3]) / 4.0 / (double)0x7fff + 0.5;
+ return a * delta + b * (1 - delta);*/
+}
diff --git a/src/wave/harm.c b/src/wave/harm.c
@@ -0,0 +1,79 @@
+/*
+Input is a list of floats.
+Output is a PCM audio of sum of harmonics with corresponding amplitudes.
+*/
+
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+
+#include "util.h"
+
+long baselen = 0, outlen = 8363; // samplerate of FastTracker samples
+double amp = 1;
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s [-a amplitude] [-b length] [-l length]\n", argv0);
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ ARGBEGIN{
+ case 'a':
+ amp = strtod(EARGF(usage()), nil);
+ break;
+ case 'b':
+ baselen = strtol(EARGF(usage()), nil, 0);
+ if (baselen <= 0) {
+ fprint(2, "invalid baselen\n");
+ exits("wrong args");
+ }
+ break;
+ case 'l':
+ outlen = strtol(EARGF(usage()), nil, 0);
+ if (outlen <= 0) {
+ fprint(2, "invalid outlen\n");
+ exits("wrong args");
+ }
+ break;
+ default:
+ usage();
+ } ARGEND
+ if (argc != 0) usage();
+
+ if (baselen == 0) baselen = outlen;
+
+ double *buf = mallocz(sizeof(double) * outlen, 1);
+ Biobuf *bp = Bfdopen(0, OREAD);
+ char *s;
+ double h = 1;
+ int i;
+ while((s = Brdstr(bp, '\n', 1)) != nil) {
+ double d = strtod(s, nil);
+ for (i = 0; i < outlen; i++) {
+ buf[i] += sin(2.0 * PI * ((double)i / (double)baselen) * h) * d;
+ }
+ h = h + 1.0;
+ if (h > baselen / 2) break; // no point to freqs bigger than what audio resolution would give us
+ }
+
+ u32int *out = malloc(sizeof(u32int) * outlen);
+
+ for (i = 0; i < outlen; i++) {
+ double d = buf[i] * amp;
+ if (d > 1.0) d = 1;
+ if (d < -1.0) d = -1;
+ Frame *f;
+ f = (Frame *)(&out[i]);
+ f->c1 = d * 0x7fff;
+ f->c2 = f->c1;
+ }
+
+ write(1, out, outlen * sizeof(u32int));
+
+ exits(0);
+}
diff --git a/src/wave/loop.c b/src/wave/loop.c
@@ -0,0 +1,63 @@
+#include <u.h>
+#include <libc.h>
+
+#include "util.h"
+
+enum {
+ MSample,
+ MFrame,
+};
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s [-f] count < file\n", argv0);
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ int mode;
+ s16int in[MaxLength * 2], *out;
+ long i, inlen, outlen, arg;
+ char *rptr;
+
+ mode = MSample;
+ outlen = 0;
+
+ ARGBEGIN {
+ case 'f':
+ mode = MFrame;
+ break;
+ default:
+ usage();
+ } ARGEND
+
+ if (argc != 1) usage();
+ arg = strtoul(argv[0], &rptr, 0);
+ if (rptr == argv[0]) usage();
+
+ inlen = read(0, in, sizeof(s16int) * MaxLength * 2);
+ if (inlen < 0) sysfatal("%r");
+ inlen /= sizeof(s16int);
+
+ switch (mode) {
+ case MSample:
+ outlen = inlen * arg;
+ break;
+ case MFrame:
+ outlen = arg * 2;
+ break;
+ }
+
+ out = malloc(outlen * sizeof(s16int));
+
+ for (i = 0; i < outlen; i += 2) {
+ out[i] = in[i % inlen];
+ out[i + 1] = in[(i + 1) % inlen];
+ }
+
+ outlen = write(1, out, outlen * sizeof(s16int));
+ if (outlen < 0) sysfatal("%r");
+}
diff --git a/src/wave/mkfile b/src/wave/mkfile
@@ -0,0 +1,7 @@
+</$objtype/mkfile
+
+TARG=draw loop fade piano sampler harm
+BIN=/$objtype/bin
+HFILES=util.h
+
+</sys/src/cmd/mkmany
diff --git a/src/wave/piano.c b/src/wave/piano.c
@@ -0,0 +1,106 @@
+/*
+ * lets user play samples from PCM files
+ * using computer keyboard
+ */
+
+#include <u.h>
+#include <libc.h>
+
+#define BSIZE 4096
+
+Rune *keys = L"zsxdcvgbhnjmq2w3er5t6y7ui9o0p[";
+Rune *notes = L"CCDDEFFGGAAB";
+Rune *semi = L"-#-#--#-#-#-";
+
+/*
+ * Frequencies for equal-tempered scale
+ * from https://pages.mtu.edu/~suits/notefreqs.html
+ */
+
+double freq[] = {
+ 261.63, // C-4
+ 277.18,
+ 293.66,
+ 311.13,
+ 329.63,
+ 349.23,
+ 369.99,
+ 392.00,
+ 415.30,
+ 440.00, // A-4
+ 466.16,
+ 493.88,
+ 523.25, // C-5
+ 554.37,
+ 587.33,
+ 622.25,
+ 659.25,
+ 698.46,
+ 739.99,
+ 783.99,
+ 830.61,
+ 880.00, // A-5
+ 932.33,
+ 987.77,
+ 1046.50,
+ 1108.73,
+ 1174.66,
+ 1244.51,
+ 1318.51,
+ 1396.91 // F-6
+};
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s\n", argv0);
+ exits("usage");
+}
+
+void
+main(int argc, char **argv)
+{
+ int kbd;
+ long n;
+ char buf[BSIZE];
+ Rune last;
+
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+ if (argc != 0) usage();
+
+ kbd = open("/dev/kbd", OREAD);
+ last = 0;
+ while((n = read(kbd, buf, BSIZE)) > 0) {
+ char *bp;
+ for (bp = buf; bp < buf + n; bp += strlen(bp) + 1) {
+ Rune r[BSIZE], *rp;
+ runesnprint(r, BSIZE, "%s", bp);
+ switch(r[0]) {
+ case 'c':
+ if (r[1] == 0x7f) exits(nil);
+ break;
+ case 'k':
+ last = r[runestrlen(r) - 1];
+ rp = runestrchr(keys, last);
+ if (rp != nil) {
+ int k = (rp - keys) % 12;
+ int o = (rp - keys) / 12;
+ fprint(2, "%C%C%d, %f ", notes[k], semi[k], o + 4, freq[rp-keys]);
+ fprint(1, "%f\n", freq[rp-keys]);
+ } else last = 0;
+ break;
+ case 'K':
+ if ((last > 0) && (last != r[runestrlen(r) - 1])) {
+ fprint(2, "off\n");
+ fprint(1, "0\n");
+ last = 0;
+ }
+ break;
+ }
+ }
+ }
+}
+
diff --git a/src/wave/sampler.c b/src/wave/sampler.c
@@ -0,0 +1,103 @@
+#include <u.h>
+#include <libc.h>
+#include <bio.h>
+#include <thread.h>
+
+#include "util.h"
+
+#define BufSize 256
+#define BufSizeInBytes (BufSize * 2 * sizeof(s16int))
+#define BaseFreq 523.25
+
+Aud sample;
+
+s16int buf[BufSize * 2];
+int audio, fid;
+Channel *ctl;
+
+void synth(void *);
+Frame * lookup(void *, double);
+
+void
+usage(void)
+{
+ fprint(2, "usage: %s file\n", argv0);
+ threadexitsall("usage");
+}
+
+void
+threadmain(int argc, char **argv)
+{
+ double in;
+ ARGBEGIN {
+ default:
+ usage();
+ } ARGEND
+ if (argc != 1) usage();
+
+ fid = open(argv[0], OREAD);
+ if (fid <= 0) sysfatal("%r");
+ sample.n = read(fid, sample.p, MaxLength * sizeof(u32int)) / sizeof(u32int);
+ close(fid);
+
+ audio = open("/dev/audio", OWRITE);
+ if (audio <= 0) sysfatal("%r");
+
+ ctl = chancreate(sizeof(double), 8);
+ proccreate(synth, nil, 64 * 1024);
+ Biobuf *bp;
+ bp = Bfdopen(0, OREAD);
+ for (;;) {
+ // TODO: use Brdstr or Brdline and extract double from it's output instead of this.
+ Bgetd(bp, &in);
+ send(ctl, &in);
+ if (Bgetc(bp) == Beof) break;
+ }
+ chanclose(ctl);
+ threadexitsall(nil);
+}
+
+void
+synth(void *)
+{
+ int n;
+ double t, freq, v;
+ freq = 0;
+ t = 0;
+ for(n = 0; n >= 0; n = nbrecv(ctl, &v)){
+ if (n > 0) {
+ freq = v;
+ if (freq == 0) t = 0;
+ }
+ int i;
+ for (i = 0; i < BufSize; i++) {
+ Frame *fr = lookup(&sample, t);
+ buf[i * 2] = fr->c1;
+ buf[1 + i * 2] = fr->c2;
+ t += freq / BaseFreq;
+ }
+ if (write(audio, buf, BufSizeInBytes) < BufSizeInBytes) {
+ sysfatal("synth: write to /dev/audio failed, %r");
+ }
+ }
+}
+
+/*
+s16int
+lookup(void *, double t)
+{
+ return 0x7fff * sin(t / 44100.0 * BaseFreq * PI * 2.0);
+}
+*/
+
+Frame *
+lookup(void *v, double t)
+{
+ Aud *aud;
+ Frame *fr;
+ long T;
+ aud = (Aud *)v;
+ T = t;
+ fr = (Frame *)(&aud->p[T%aud->n]);
+ return fr;
+}
diff --git a/src/wave/unconv b/src/wave/unconv
@@ -0,0 +1,4 @@
+#!/bin/rc
+# convert to fasttracker samplerate
+
+audio/pcmconv -i 's16c1r44100' -o 's8c1r8363'
diff --git a/src/wave/util.h b/src/wave/util.h
@@ -0,0 +1,23 @@
+#define MaxLength (44100 * 10)
+/*
+ * This is 10 seconds in standard PCM samplerate
+ * TODO: MaxLength should be equal to max size of 9p message
+ */
+
+typedef struct Frame Frame;
+typedef struct Aud Aud;
+
+struct Frame {
+ union {
+ s16int c[2];
+ struct {
+ s16int c1;
+ s16int c2;
+ };
+ };
+};
+
+struct Aud{
+ u32int p[MaxLength];
+ long n;
+};
diff --git a/src/wave/waveform b/src/wave/waveform
@@ -0,0 +1,20 @@
+#!/bin/rc
+
+rfork e
+
+dir=/tmp/wave
+
+mkdir -p $dir
+touch $dir'/'^(f1 f2 f3 f4 fade)
+
+for (f in $dir'/'^(f1 f2 f3 f4)) {
+ dd -if /dev/zero -of $f -bs 169 -count 4 -quiet 1
+}
+dd -if /dev/zero -of $dir/fade -bs 169 -count 16 -quiet 1
+
+window -m -r 0 0 200 200 6.draw -a $dir/f1
+window -m -r 200 0 400 200 6.draw -a $dir/f2
+window -m -r 400 0 600 200 6.draw -a $dir/f3
+window -m -r 600 0 800 200 6.draw -a $dir/f4
+
+window -m -r 0 200 800 400 6.draw $dir/fade