shithub: pplay

Download patch

ref: a8402254addaa203ed5e6f175677b971274a2f86
parent: 3f2ec921691046ec1738aeeeb494be9bfef21120
author: qwx <[email protected]>
date: Mon Apr 24 16:05:45 EDT 2023

reimplement chunking; near-infinite undo/redo; fixes; regressive commit

no longer ever modify chunks, only replace them.

- as before, chunks define "chains", ie.  circular lists of chunks
- whatever edit op is performed, all overlapping chunks are *replaced*
by new ones representing the new content of the interval + sliced
borders of the two terminal chunks on each side:

	-[ a ]-[ b ] ..  [ c ]-[ d ]-
	  replace ^^^^^^^^^^
	-[ a ]-[L][x] ..  [y][R]-[ d ]-
	  new undo frame: [ b ]-...-[ c ], [L]-[x]-..-[y]-[R]

	-[ a ]-[ b ] ..  [ c ]-[ d ]-
	  cut ^^^^^^^^^^
	-[ a ]-[L]-[R]-[ d ]-
	  new undo frame: [ b ]-...-[ c ], [L]-[R]

	-[ a ]-[ b ] ..  [ c ]-[ d ]-
	  insert ^
	-[ a ]-[L]-[ i ]-[R]-[ c ]-[ d ]-
	  new undo frame: [ b ], [L]-[ i ]-[R]

	same with insert/replace/crop/copy/etc.

- bordering overlapping chunks are sliced into two; the outermost
slices are named L and R resp.  for the left and right border and just
their truncated unmodified prefix/suffix; the entire overlapping chain
is unlinked and replaced by a new chain terminated by L and R; the op
is pushed to an undo stack by just saving the pointers to the "parent"
unsliced left and right border, and pointers to L and R.
- always replacing entire overlapping chunks means that all pointers
in between are always valid and never need repair, that undo/redo
merely substitute one chain for another, and that all operations are
in constant time; the cost in terms of chunks is dwarfed by the audio
buffer size either way regardless of the multiplication of the number
of chunks, and this dispenses with locks and other complicated logic
for snarfing buffers; it also dispenses with the need to ever memcpy
anything, only pointers are passed around.

this resembles or replicates umbraticus' own method in pcmed(1).

also:
- "forget" ops if undoing then rewriting history from an earlier point
- just allocate new undo slots as needed instead of even limiting
them, who cares, negligeable in practice
- synchronous edit after read proc is done reading in new data
- lower i/o buffer size; don't memcpy anything...  for now.
- ui: there is always a valid range selected; upon any left click,
we set an insertion point; upon any right click we set a replacement
range and unset any insertion point; simpler than prior approach
- display total time duration of range

not thoroughly tested and there are regressions -- all much easier
to debug.

--- a/chunk.c
+++ b/chunk.c
@@ -12,6 +12,25 @@
 };
 static Chunk *norris;
 
+// FIXME: crazy idea, multisnarf with addressable elements; $n registers; fork pplay to display them → ?
+
+typedef struct Op Op;
+struct Op{
+	Chunk *p1;
+	Chunk *p2;
+	Chunk *l;
+	Chunk *r;
+};
+static Op *opbuf, *ophead, *opend;
+static usize opbufsz;
+
+static struct{
+	Chunk *from;
+	usize foff;
+	Chunk *to;
+	usize toff;
+} hold;
+
 int
 Δfmt(Fmt *fmt)
 {
@@ -20,8 +39,8 @@
 	d = va_arg(fmt->args, Dot*);
 	if(d == nil)
 		return fmtstrcpy(fmt, "[??:??:??:??]");
-	return fmtprint(fmt, "[from=%08zux cur=%08zux at=%08zux to=%08zux]",
-		d->from, d->pos, d->at, d->to);
+	return fmtprint(fmt, "[from=%08zux cur=%08zux to=%08zux]",
+		d->from, d->pos, d->to);
 }
 
 int
@@ -76,7 +95,7 @@
 	c->left = c;
 	c->right = c;
 	c->b = b;
-	c->off = 0;
+	c->boff = 0;
 	c->len = b->bufsz;
 	incref(&b->Ref);
 	return c;
@@ -117,12 +136,12 @@
 
 	assert(c != nil && c->b != nil);
 	nc = newchunk(c->b);
-	nc->off = c->off;
+	nc->boff = c->boff;
 	nc->len = c->len;
 	incref(c->b);
 	return nc;
 }
-Chunk *
+static Chunk *
 clone(Chunk *left, Chunk *right)
 {
 	Chunk *cl, *c, *nc;
@@ -137,7 +156,20 @@
 	}
 	return cl;
 }
+static Chunk *
+splitchunk(Chunk *p, usize from, usize to)
+{
+	Chunk *c;
 
+	assert(from < p->len);
+	assert(to > 0 && to - from <= p->len);
+	c = clonechunk(p);
+	c->boff += from;
+	c->len = to - from;
+	c->left = c->right = c;
+	return c;
+}
+
 static void
 freebuf(Buf *b)
 {
@@ -170,21 +202,8 @@
 	freechunk(c);
 }
 
-static void
-shrinkbuf(Chunk *c, usize newsz)
-{
-	Buf *b;
-
-	b = c->b;
-	assert(b != nil);
-	assert(newsz < b->bufsz && newsz > 0);
-	if(c->off + c->len > newsz)
-		c->len = newsz - c->off;
-	b->buf = erealloc(b->buf, newsz, b->bufsz);
-}
-
 usize
-chunklen(Chunk *c)
+chainlen(Chunk *c)
 {
 	usize n;
 	Chunk *cp;
@@ -210,257 +229,271 @@
 		*off = p;
 	return c;
 }
-usize
-c2p(Chunk *tc)
+
+static void
+forgetop(Op *op)
 {
-	Chunk *c;
-	usize p;
+	freechain(op->l);
+}
 
-	for(p=0, c=norris; c!=tc; c=c->right)
-		p += c->len;
-	return p;
+int
+unpop(char *)
+{
+	Op *op;
+
+	if(opend == opbuf || ophead == opend)
+		return 0;
+	op = ophead++;
+	dprint(op->p1, "cmd/unpop dot=%Δ P [%χ][%χ] LR [%χ][%χ]\n",
+		&dot, op->p1, op->p2, op->l, op->r);
+	totalsz += chainlen(op->l);
+	linkchunk(op->p1->left, op->l);
+	unlink(op->p1, op->p2);
+	totalsz -= chainlen(op->p1);
+	if(norris == op->p1)
+		norris = op->l;
+	dot.from = dot.pos = 0;
+	dot.to = totalsz;
+	return 1;
 }
 
-void
-recalcsize(void)
+int
+popop(char *)
 {
-	int n;
+	Op *op;
 
-	n = c2p(norris->left) + norris->left->len;
-	if(dot.to == totalsz || dot.to > n)
-		dot.to = n;
-	if(dot.pos < dot.from || dot.pos > dot.to)
-		dot.pos = dot.from;
-	dot.at = dot.from;
-	dprint(nil, "final %Δ\n", &dot);
-	totalsz = n;
+	if(ophead == opbuf)
+		return 0;
+	op = --ophead;
+	dprint(op->l, "cmd/pop dot=%Δ P [%χ][%χ] LR [%χ][%χ]\n",
+		&dot, op->p1, op->p2, op->l, op->r);
+	totalsz += chainlen(op->p1);
+	linkchunk(op->l->left, op->p1);
+	unlink(op->l, op->r);
+	totalsz -= chainlen(op->l);
+	if(norris == op->l)
+		norris = op->p1;
+	dot.from = dot.pos = 0;
+	dot.to = totalsz;
+	return 1;
 }
 
-#define ASSERT(x) {if(!(x)) printchunks(norris); assert((x)); }
-void
-paranoia(int exact)
+static void
+pushop(Chunk *p1, Chunk *p2, Chunk *l, Chunk *r)
 {
-	usize n;
-	Chunk *c, *pc;
-	Buf *b;
+	Op *op;
 
-	ASSERT(dot.pos >= dot.from && dot.pos < dot.to);
-	for(pc=norris, n=pc->len, c=pc->right; c!=norris; pc=c, c=c->right){
-		b = c->b;
-		ASSERT(b != nil);
-		ASSERT((b->bufsz & 3) == 0 && b->bufsz >= Sampsz);
-		ASSERT(c->off < b->bufsz);
-		ASSERT(c->len > Sampsz);
-		ASSERT(c->off + c->len <= b->bufsz);
-		ASSERT(c->left == pc);
-		n += c->len;
+	if(ophead == opbuf + opbufsz){
+		opbuf = erealloc(opbuf,
+			(opbufsz + 1024) * sizeof *opbuf,
+			opbufsz * sizeof *opbuf);
+		ophead = opbuf + opbufsz;
+		opend = ophead;
+		opbufsz += 1024;
 	}
-	if(exact){
-		ASSERT(n <= totalsz);
-		ASSERT(dot.to <= totalsz);
+	if(opend > ophead){
+		for(op=ophead; op<opend; op++)
+			forgetop(op);
+		memset(ophead, 0, (opend - ophead) * sizeof *ophead);
 	}
+	*ophead++ = (Op){p1, p2, l, r};
+	opend = ophead;
 }
-#undef ASSERT
 
-/* FIXME: should set .pos as well? or just bounds? s/setdot/setbounds/? */
 void
-setdot(Dot *dot, Chunk *right)
+ccrop(usize from, usize to)
 {
-	dot->from = 0;
-	if(right == nil)
-		dot->to = c2p(norris->left) + norris->left->len;
-	else
-		dot->to = c2p(right);
-	dot->at = dot->from;
-}
+	usize n, off;
+	Chunk *p1, *p2, *l, *r;
 
-void
-fixroot(Chunk *rc, usize off)
-{
-	Chunk *c;
-
-	dprint(rc, "fixroot [%χ] %08zux\n", rc, off);
-	for(c=rc->left; off>0; off-=c->len, c=c->left){
-		if(off - c->len == 0)
-			break;
-		assert(off - c->len < off);
-	}
-	norris = c;
+	assert(from < to && to <= totalsz);
+	p1 = p2c(from, &off);
+	l = splitchunk(p1, off, p1->len);
+	p2 = p2c(to, &off);
+	r = splitchunk(p2, 0, off);
+	linkchunk(p1, l);
+	linkchunk(p2->left, r);
+	unlink(p2, p1);
+	n = chainlen(l);
+	totalsz = n;
+	pushop(p2, p1, r, l);
+	norris = l;
+	dot.pos -= dot.from;
+	dot.from = 0;
+	dot.to = n;
 }
 
-Chunk *
-splitchunk(Chunk *c, usize off)
+static int
+creplace(usize from, usize to, Chunk *c)
 {
-	Chunk *nc;
+	usize n, off;
+	Chunk *p1, *p2, *l, *r;
 
-	dprint(nil, "splitchunk %Δ [%χ] off=%08zux\n", &dot, c, off);
-	if(off == 0 || c == norris->left && off == c->len)
-		return c;
-	assert(off <= c->len);
-	nc = clonechunk(c);
-	nc->off = c->off + off;
-	nc->len = c->len - off;
-	c->len = off;
-	linkchunk(c, nc);
-	return nc;
+	assert(from > 0 && from < to && to <= totalsz);
+	p1 = p2c(from, &off);
+	l = splitchunk(p1, 0, off);
+	p2 = p2c(to, &off);
+	r = splitchunk(p2, off, p2->len);
+	linkchunk(c, r);
+	linkchunk(l, c);
+	n = chainlen(l);
+	totalsz += n;
+	linkchunk(p1->left, l);
+	unlink(p1, p2);
+	totalsz -= chainlen(p1);
+	pushop(p1, p2, l, r);
+	if(p1 == norris)
+		norris = l;
+	dot.to = dot.from + n;
+	dot.pos = from;
+	return 0;
 }
 
-/* c1 [nc … c2] nc */
-int
-splitrange(usize from, usize to, Chunk **left, Chunk **right)
+// FIXME: use a specific Dot (prep for multibuf); generalize
+static int
+cinsert(usize pos, Chunk *c)
 {
-	usize off;
-	Chunk *c;
+	usize n, off;
+	Chunk *p, *l, *r;
 
-	dprint(nil, "splitrange from=%08zux to=%08zux\n", from, to);
-	c = p2c(from, &off);
-	if(off > 0){
-		splitchunk(c, off);
-		*left = c->right;
-	}else
-		*left = c;	/* dangerous in combination with *right */
-	c = p2c(to, &off);
-	if(off < c->len - 1){
-		splitchunk(c, off);
-		*right = c;
-	}else
-		*right = c;
+	assert(pos <= totalsz);
+	p = p2c(pos, &off);
+	l = splitchunk(p, 0, off);
+	r = splitchunk(p, off, p->len);
+	linkchunk(c, r);
+	linkchunk(l, c);
+	n = chainlen(l);
+	totalsz += n;
+	linkchunk(p->left, l);
+	unlink(p, p);
+	totalsz -= chainlen(p);
+	pushop(p, p, l, r);
+	if(p == norris)
+		norris = l;
+	dot.to = dot.from + n;
+	dot.pos = pos;
 	return 0;
 }
 
-Chunk *
-cutrange(usize from, usize to, Chunk **latch)
+int
+cpaste(usize from, usize to)
 {
-	Chunk *c, *left, *right;
+	Chunk *p1, *p2, *l, *r;
 
-	dprint(nil, "cutrange from=%08zux to=%08zux\n", from, to);
-	if(splitrange(from, to, &left, &right) < 0)
-		return nil;
-	c = left->left;
-	if(left == norris)
-		norris = right->right;
-	unlink(left, right);
-	if(latch != nil)
-		*latch = left;
-	return c;
+	if(hold.from == nil || hold.to == nil){
+		werrstr("cpaste: nothing to paste");
+		return -1;
+	}
+	p1 = hold.from;
+	p2 = hold.to;
+	if(p1 == p2)
+		l = splitchunk(p1, hold.foff, hold.toff);
+	else{
+		l = splitchunk(p1, hold.foff, p1->len);
+		r = splitchunk(p2, 0, hold.toff);
+		if(p1->right != p2)
+			linkchunk(l, clone(p1->right, p2->left));
+		linkchunk(l->left, r);
+	}
+	return from == to ? cinsert(from, l) : creplace(from, to, l);
 }
 
-Chunk *
-croprange(usize from, usize to, Chunk **latch)
+void
+ccopy(usize from, usize to)
 {
-	Chunk *cut, *left, *right;
-
-	dprint(nil, "croprange from=%08zux to=%08zux\n", from, to);
-	if(splitrange(from, to, &left, &right) < 0)
-		return nil;
-	norris = left;
-	cut = right->right;
-	if(latch != nil)
-		*latch = cut;
-	unlink(right->right, left->left);
-	return left;
+	hold.from = p2c(from, &hold.foff);
+	hold.to = p2c(to, &hold.toff);
 }
 
-// FIXME: generalized insert(from, to), where from and to not necessarily distinct
-Chunk *
-inserton(usize from, usize to, Chunk *c, Chunk **latch)
+void
+chold(Chunk *c)
 {
-	Chunk *left;
-
-	dprint(c, "inserton from=%08zux to=%08zux\n", from, to);
-	left = cutrange(from, to, latch);
-	linkchunk(left, c);
-	if(from == 0)
-		norris = c;
-	dprint(nil, "done\n");
-	return left;
+	hold.from = hold.to = c;
+	hold.foff = hold.toff = 0;
 }
 
-Chunk *
-insertat(usize pos, Chunk *c)
+void
+ccut(usize from, usize to)
 {
-	usize off;
-	Chunk *left;
+	usize n;
+	Chunk *p1, *p2, *l, *r;
 
-	dprint(c, "insertat cur=%08zux\n", pos);
-	if(pos == 0){
-		left = norris->left;
-		norris = c;
-	}else{
-		left = p2c(pos, &off);
-		splitchunk(left, off);
-	}
-	if(off == 0)
-		left = left->left;
-	linkchunk(left, c);
-	return left;
+	assert(from > 0 && from < to && to <= totalsz);
+	ccopy(from, to);
+	p1 = hold.from;
+	r = splitchunk(p1, 0, hold.foff);
+	p2 = hold.to;
+	l = splitchunk(p2, hold.toff, p2->len);
+	linkchunk(l, r);
+	n = chainlen(l);
+	totalsz += n;
+	linkchunk(p1->left, l);
+	unlink(p1, p2);
+	totalsz -= chainlen(p1);
+	pushop(p1, p2, l, r);
+	if(p1 == norris)
+		norris = l;
+	dot.from = 0;
+	dot.to = n;
+	dot.pos = from;
 }
 
 uchar *
-getslice(Dot *d, usize n, usize *sz)
+getslice(Dot *d, usize want, usize *have)
 {
-	usize Δbuf, Δloop, off;
+	usize n, off;
 	Chunk *c;
 
 	if(d->pos >= totalsz){
 		werrstr("out of bounds");
-		*sz = 0;
+		*have = 0;
 		return nil;
 	}
 	c = p2c(d->pos, &off);
-	Δloop = d->to - d->pos;
-	Δbuf = c->len - off;
-	if(n < Δloop && n < Δbuf){
-		*sz = n;
-		d->pos += n;
-	}else if(Δloop <= Δbuf){
-		*sz = Δloop;
-		d->pos = d->from;
-	}else{
-		*sz = Δbuf;
-		d->pos += Δbuf;
-	}
-	return c->b->buf + c->off + off;
+	n = c->len - off;
+	*have = want > n ? n : want;
+	return c->b->buf + c->boff + off;
 }
 
 Chunk *
-readintochunks(int fd)
+chunkfile(int fd)
 {
 	int n;
-	usize m;
-	Chunk *rc, *c, *nc;
+	Chunk *c;
+	Buf *b;
 
-	for(m=0, rc=c=nil;; m+=n){
-		nc = newbuf(Iochunksz);
-		if(rc == nil)
-			rc = nc;
-		else
-			linkchunk(c, nc);
-		c = nc;
-		if((n = readn(fd, c->b->buf, Iochunksz)) < Iochunksz)
+	c = newbuf(Chunksz);
+	b = c->b;
+	for(;;){
+		if(b->bufsz - c->len < Chunksz){
+			b->buf = erealloc(c->b->buf, 2 * c->b->bufsz, c->b->bufsz);
+			b->bufsz *= 2;
+		}
+		if((n = readn(fd, b->buf + c->len, Chunksz)) < Chunksz)
 			break;
+		c->len += n;
 		yield();
 	}
-	close(fd);
-	if(n < 0)
-		fprint(2, "readintochunks: %r\n");
-	else if(n == 0){
-		if(c != rc)
-			unlink(c, c);
+	if(n < 0){
+		fprint(2, "chunkfile: %r\n");
 		freechunk(c);
-		if(c == rc){
-			werrstr("readintochunks: nothing read");
-			return nil;
-		}
-	}else if(n > 0 && n < Iochunksz)
-		shrinkbuf(c, n);
-	return rc;
+		return nil;
+	}else if(c->len == 0){
+		fprint(2, "chunkfile: nothing read\n");
+		freechunk(c);
+		return nil;
+	}else if(c->len < b->bufsz){
+		b->buf = erealloc(b->buf, c->len, b->bufsz);
+		b->bufsz = c->len;
+	}
+	return c;
 }
 
 void
-graphfrom(Chunk *c)
+initbuf(Chunk *c)
 {
 	norris = c;
-	recalcsize();
-	setdot(&dot, nil);
+	totalsz = chainlen(c);
+	dot.pos = dot.from = 0;
+	dot.to = totalsz;
 }
--- a/cmd.c
+++ b/cmd.c
@@ -8,182 +8,24 @@
 usize totalsz;
 int treadsoftly;
 
-// FIXME: undo/redo as an unbatched series of inserts and deletes
-// FIXME: crazy idea, multisnarf with addressable elements; $n registers; fork pplay to display them → ?
-
-enum{
-	OPins,
-	OPdel,
-	OPcrop,
-
-	Nops = 128,
-};
 static int epfd[2];
 
-typedef struct Op Op;
-struct Op{
-	int type;
-	usize from;
-	usize to;
-	Chunk *c;
-};
-static int ohead, otail;
-static Chunk *hold;
-static Op ops[Nops];
-
-void
-setrange(usize from, usize to)
-{
-	assert((from & 3) == 0);
-	assert((to & 3) == 0);
-	dot.from = from;
-	dot.to = to;
-	if(dot.pos < from || dot.pos >= to)
-		dot.pos = from;
-	dot.at = dot.from;
-}
-
-int
-jump(usize off)
-{
-	if(off < dot.from || off > dot.to){
-		werrstr("cannot jump outside of loop bounds\n");
-		return -1;
-	}
-	dot.pos = off;
-	if(dot.from == 0 && dot.to == totalsz)
-		dot.at = off;
-	return 0;
-}
-
-// FIXME: needs a different way of managing ops
-int
-unpop(char *)
-{
-	return 0;
-}
-
-int
-popop(char *)	// FIXME: u[n]
-{
-	Op *op;
-
-	if(otail == ohead)
-		return 0;
-	ohead = ohead - 1 & nelem(ops) - 1;
-	op = ops + ohead;
-	dprint(op->c, "cmd/pop dot=%Δ type=%d from=%08zux to=%08zux c=%#p\n",
-		&dot, op->type, op->from, op->to, op->c);
-	switch(op->type){
-	case OPdel:
-		if(insertat(op->from, op->c) == nil)
-			return -1;
-		break;
-	case OPins:
-		if(cutrange(op->from, op->to, nil) == nil)
-			return -1;
-		break;
-	case OPcrop:
-		if(insertat(op->to - op->from, op->c) == nil)
-			return -1;
-		dprint(nil, "uncropped with loose root\n");
-		fixroot(op->c, op->from + (op->to - op->from));
-		break;
-	default: werrstr("phase error: unknown op %d\n", op->type); return -1;
-	}
-	memset(ops+ohead, 0, sizeof *ops);
-	return 1;
-}
-
-void
-pushop(int type, usize from, usize to, Chunk *c)
-{
-	freechain(ops[ohead].c);
-	ops[ohead] = (Op){type, from, to, c};
-	ohead = ohead + 1 & nelem(ops) - 1;
-}
-
 static int
-replace(char *, Chunk *c)
+paste(char *)
 {
-	Chunk *left, *latch;
-	usize n;
+	usize from, to;
 
-	if(c == nil){
-		fprint(2, "replace: nothing to paste\n");
-		return -1;
-	}
-	n = chunklen(c);
-	if((left = inserton(dot.from, dot.to, c, &latch)) == nil){
-		fprint(2, "insert: %r\n");
-		return -1;
-	}
-	pushop(OPdel, dot.from, dot.to, latch);
-	pushop(OPins, dot.from, dot.from+n, nil);
-	setdot(&dot, nil);
-	dot.pos = c2p(left->right);
-	return 1;
+	from = dot.from;
+	to = dot.to;
+	if(latchedpos >= 0 && dot.from == 0 && dot.to == totalsz)
+		to = from = latchedpos;
+	return cpaste(from, to) == 0 ? 1 : -1;
 }
 
 static int
-insert(char *, Chunk *c)
-{
-	Chunk *left;
-
-	if(c == nil){
-		fprint(2, "insert: nothing to paste\n");
-		return -1;
-	}
-	if(dot.at == dot.from){
-		fprint(2, "insert: nowhere to paste\n");
-		return -1;
-	}
-	assert(dot.at >= dot.from && dot.at <= dot.to);
-	dprint(nil, "cmd/insert %Δ\n", &dot);
-	dprint(c, "buffered\n");
-	pushop(OPins, dot.at, dot.at+chunklen(c)-1, nil);
-	if((left = insertat(dot.at, c)) == nil){
-		fprint(2, "insert: %r\n");
-		return -1;
-	}
-	setdot(&dot, nil);
-	dot.pos = c2p(left->right);
-	dot.at = dot.from;
-	dprint(nil, "end\n");
-	return 1;
-}
-
-static int
-paste(char *s, Chunk *c)
-{
-	if(c == nil && (c = hold) == nil){
-		werrstr("paste: no buffer");
-		return -1;
-	}
-	c = clone(c, c->left);
-	if(dot.from > 0 || dot.to < totalsz)
-		return replace(s, c);
-	else
-		return insert(s, c);
-}
-
-static void
-snarf(Chunk *c)
-{
-	dprint(hold, "snarf was:\n");
-	freechain(hold);
-	hold = c;
-	dprint(hold, "snarf now:\n");
-}
-
-static int
 copy(char *)
 {
-	Chunk *left, *right;
-
-	dprint(hold, "cmd/copy %Δ\n", &dot);
-	splitrange(dot.from, dot.to, &left, &right);
-	snarf(clone(left, right));
+	ccopy(dot.from, dot.to);
 	return 0;
 }
 
@@ -190,19 +32,12 @@
 static vlong
 cut(char *)
 {
-	Chunk *latch;
-
+	dprint(nil, "cmd/cut %Δ\n", &dot);
 	if(dot.from == 0 && dot.to == totalsz){
-		werrstr("cut: no range selected");
+		werrstr("cut: can't cut entire buffer");
 		return -1;
 	}
-	dprint(nil, "cmd/cut %Δ\n", &dot);
-	cutrange(dot.from, dot.to, &latch);
-	dprint(latch, "latched\n");
-	snarf(clone(latch, latch->left));
-	pushop(OPdel, dot.from, dot.from+chunklen(latch)-1, latch);
-	dot.pos = dot.from;
-	setdot(&dot, nil);
+	ccut(dot.from, dot.to);
 	return 1;
 }
 
@@ -209,87 +44,38 @@
 static int
 crop(char *)
 {
-	Chunk *latch;
-
 	dprint(nil, "cmd/crop %Δ\n", &dot);
-	if(croprange(dot.from, dot.to, &latch) == nil)
-		return -1;
-	dprint(latch, "latched\n");
-	pushop(OPcrop, dot.from, dot.to, latch);
-	setdot(&dot, nil);
-	dot.pos = 0;
+	ccrop(dot.from, dot.to);
 	return 1;
 }
 
-vlong
-getbuf(Dot d, usize n, uchar *buf, usize bufsz)
-{
-	uchar *p, *b;
-	usize sz;
-
-	assert(d.pos < totalsz);
-	assert(n <= bufsz);
-	b = buf;
-	while(n > 0){
-		if((p = getslice(&d, n, &sz)) == nil || sz < Sampsz)
-			return -1;
-		memcpy(b, p, sz);
-		b += sz;
-		n -= sz;
-	}
-	return b - buf;
-}
-
 static int
 writebuf(int fd)
 {
-	static uchar *buf;
-	static usize bufsz;
 	int nio;
-	usize n, m, c, k;
+	uchar *b;
+	usize n, m, k;
 	Dot d;
 
-	d.pos = d.from = dot.from;
-	d.to = dot.to;
+	d = dot;
+	d.pos = d.from;
 	if((nio = iounit(fd)) == 0)
 		nio = 8192;
-	if(bufsz < nio){
-		buf = erealloc(buf, nio, bufsz);
-		bufsz = nio;
-	}
-	for(m=d.to-d.from, c=0; m>0;){
+	for(m=d.to-d.from, b=(uchar*)&d; m>0; m-=n, d.pos+=n){
 		k = nio < m ? nio : m;
-		if(getbuf(d, k, buf, bufsz) < 0){
+		if((b = getslice(&d, k, &k)) == nil || k <= 0){
 			fprint(2, "writebuf: couldn\'t snarf: %r\n");
 			return -1;
 		}
-		if((n = write(fd, buf, k)) != k){
+		if((n = write(fd, b, k)) != k){
 			fprint(2, "writebuf: short write not %zd: %r\n", k);
 			return -1;
 		}
-		m -= n;
-		d.pos += n;
-		c += n;
 	}
-	write(fd, buf, 0);	/* close pipe */
+	write(fd, b, 0);	/* close pipe */
 	return 0;
 }
 
-int
-advance(Dot *d, usize n)
-{
-	usize m, sz;
-
-	m = 0;
-	while(n > 0){
-		if(getslice(d, n, &sz) == nil)
-			return -1;
-		m += sz;
-		n -= sz;
-	}
-	return m;
-}
-
 static void
 rc(void *s)
 {
@@ -311,8 +97,7 @@
 	close(fd);
 	threadexits(nil);
 }
-/* using a thread does slow down reads a bit */
-// FIXME: ugly
+
 static void
 rproc(void *efd)
 {
@@ -321,23 +106,25 @@
 	Chunk *c;
 
 	d = dot;
-	treadsoftly = 1;
+	treadsoftly++;
 	fd = (intptr)efd;
-	if((c = readintochunks(fd)) == nil){
+	if((c = chunkfile(fd)) == nil){
 		treadsoftly = 0;
 		threadexits("failed reading from pipe: %r");
 	}
 	close(fd);
-	dot = d;
-	paste(nil, c);
-	dot.pos = dot.from;
-	setdot(&dot, nil);
-	recalcsize();
+	qlock(&lsync);
+	dot.from = d.from;
+	dot.to = d.to;
+	chold(c);
+	paste(nil);
+	qunlock(&lsync);
+	treadsoftly--;
 	redraw(0);
-	treadsoftly = 0;
 	threadexits(nil);
 }
 
+// FIXME: make sure writes complete even after exit
 static int
 pipeline(char *arg, int rr, int wr)
 {
@@ -346,7 +133,7 @@
 	if(procrfork(rc, arg, mainstacksize, RFFDG|RFNOTEG|RFNAMEG) < 0)
 		sysfatal("procrfork: %r");
 	close(epfd[0]);
-	if(wr && procrfork(wproc, (int*)dup(epfd[1], -1), mainstacksize, RFFDG) < 0){
+	if(wr && procrfork(wproc, (int*)dup(epfd[1], -1), mainstacksize, RFFDG|RFNOTEG|RFNAMEG) < 0){
 		fprint(2, "procrfork: %r\n");
 		return -1;
 	}
@@ -399,9 +186,8 @@
 	return 0;
 }
 
-/* the entire string is treated as the filename, ie.
- * spaces and any other weird characters will be part
- * of it */
+/* the entire string is treated as the filename, ie. spaces
+ * and any other weird characters will be part of it */
 static int
 writeto(char *arg)
 {
@@ -411,7 +197,7 @@
 		werrstr("writeto: %r");
 		return -1;
 	}
-	if(procrfork(wproc, (int*)fd, mainstacksize, RFFDG) < 0){
+	if(procrfork(wproc, (int*)fd, mainstacksize, RFFDG|RFNOTEG|RFNAMEG) < 0){
 		fprint(2, "procrfork: %r\n");
 		return -1;
 	}
@@ -438,8 +224,6 @@
 			break;
 		s += n;
 	}
-	if(debug)
-		paranoia(1);
 	switch(r){
 	case '<': x = pipefrom(s); break;
 	case '^': x = pipethrough(s); break;
@@ -446,30 +230,43 @@
 	case '|': x = pipeto(s); break;
 	case 'c': x = copy(s); break;
 	case 'd': x = cut(s); break;
-	case 'p': x = paste(s, nil); break;
+	case 'p': x = paste(s); break;
 	case 'q': threadexitsall(nil);
 	case 'r': x = readfrom(s); break;
 	case 's': x = replicate(s); break;
-//	case 'U': x = unpop(s); break;
+	case 'U': x = unpop(s); break;
 	case 'u': x = popop(s); break;
 	case 'w': x = writeto(s); break;
 	case 'x': x = crop(s); break;
 	default: werrstr("unknown command %C", r); x = -1; break;
 	}
-	if(debug)
-		paranoia(0);
-	recalcsize();
 	return x;
 }
 
 int
+advance(Dot *d, usize n)
+{
+	usize m;
+
+	assert(d->pos >= d->from && d->pos <= d->to);
+	while(n > 0){
+		m = d->to - d->pos > n ? n : d->to - d->pos;
+		n -= m;
+		d->pos += m;
+		if(d->pos == d->to)
+			d->pos = d->from;
+	}
+	return 0;
+}
+
+int
 loadin(int fd)
 {
 	Chunk *c;
 
-	if((c = readintochunks(fd)) == nil)
+	if((c = chunkfile(fd)) == nil)
 		sysfatal("loadin: %r");
-	graphfrom(c);
+	initbuf(c);
 	return 0;
 }
 
--- a/dat.h
+++ b/dat.h
@@ -8,12 +8,12 @@
 	WriteDelay = Rate / WriteRate,	/* 1764 default delay */
 	Sampsz = 2 * 2,
 	Outsz = WriteDelay * Sampsz,
-	Iochunksz = 4*1024*1024,	/* ≈ 24 sec. at 44.1 kHz */
+	Chunksz = Sampsz * Rate,
 };
 #pragma incomplete Buf
 struct Chunk{
 	Buf *b;
-	usize off;
+	usize boff;
 	usize len;
 	Chunk *left;
 	Chunk *right;
@@ -21,13 +21,15 @@
 extern struct Dot{
 	usize pos;
 	usize from;
-	usize at;
 	usize to;
 };
 extern Dot dot;
+extern vlong latchedpos;
 extern usize totalsz;
 extern int treadsoftly;
 extern int viewdone;
+
+extern QLock lsync;
 
 extern int stereo;
 extern int debug;
--- a/draw.c
+++ b/draw.c
@@ -8,6 +8,7 @@
 QLock lsync;
 int debugdraw;
 int viewdone;
+vlong latchedpos;
 
 enum{
 	Cbg,
@@ -129,12 +130,12 @@
 			lmin = lmax = 0;
 			rmin = rmax = 0;
 			while(n > 0){
-				p = getslice(&d, n, &k);
-				if(p == nil){
+				if((p = getslice(&d, n, &k)) == nil){
 					if(k > 0)
 						fprint(2, "getslice: %r\n");
 					goto end;
 				}
+				d.pos += k;
 				e = p + k;
 				while(p < e){
 					s = (s16int)(p[1] << 8 | p[0]);
@@ -182,11 +183,11 @@
 	seprint(s, s+sizeof s, "T %zd @ %τ", T / Sampsz, dot.pos);
 	p = string(screen, statp, col[Ctext], ZP, font, s);
 	if(dot.from > 0 || dot.to < totalsz){
-		seprint(s, s+sizeof s, " ↺ %τ - %τ", dot.from, dot.to);
+		seprint(s, s+sizeof s, " ↺ %τ - %τ (%τ)", dot.from, dot.to, dot.to - dot.from);
 		p = string(screen, p, col[Cloop], ZP, font, s);
 	}
-	if(dot.at != dot.from && dot.at != dot.to){
-		seprint(s, s+sizeof s, " ‡ %τ", dot.at);
+	if(latchedpos >= 0){
+		seprint(s, s+sizeof s, " ‡ %τ", (usize)latchedpos);
 		p = string(screen, p, col[Cins], ZP, font, s);
 	}
 }
@@ -199,8 +200,8 @@
 		drawchunks();
 	drawpos(dot.from, col[Cloop]);
 	drawpos(dot.to, col[Cloop]);
-	if(dot.at != dot.from && dot.at != dot.to)
-		drawpos(dot.at, col[Cins]);
+	if(latchedpos >= 0)
+		drawpos(latchedpos, col[Cins]);
 }
 
 void
@@ -289,6 +290,31 @@
 }
 
 void
+setrange(usize from, usize to)
+{
+	assert((from & 3) == 0);
+	assert((to & 3) == 0);
+	dot.from = from;
+	dot.to = to;
+	if(dot.pos < from || dot.pos >= to)
+		dot.pos = from;
+	latchedpos = -1;
+}
+
+static int
+setcur(usize off)
+{
+	if(off < dot.from || off > dot.to - Outsz){
+		werrstr("cannot jump outside of loop bounds\n");
+		return -1;
+	}
+	dot.pos = off;
+	latchedpos = off;
+	update();
+	return 0;
+}
+
+void
 setloop(vlong off)
 {
 	off *= T;
@@ -299,15 +325,6 @@
 		setrange(off, dot.to);
 	else
 		setrange(dot.from, off);
-	update();
-}
-
-static void
-setcur(usize off)
-{
-	if(off < dot.from || off > dot.to - Outsz)
-		return;
-	jump(off);
 	update();
 }
 
--- a/fns.h
+++ b/fns.h
@@ -1,35 +1,29 @@
-void	fixroot(Chunk*, usize);
 void	dprint(Chunk*, char*, ...);
 void	freechain(Chunk*);
-usize	chunklen(Chunk*);
-void	recalcsize(void);
-void	paranoia(int);
-void	setdot(Dot*, Chunk*);
-Chunk*	clone(Chunk*, Chunk*);
-int	splitrange(usize, usize, Chunk**, Chunk**);
-void	graphfrom(Chunk*);
-Chunk*	inserton(usize, usize, Chunk*, Chunk**);
-Chunk*	insertat(usize, Chunk*);
-Chunk*	croprange(usize, usize, Chunk**);
-Chunk*	cutrange(usize, usize, Chunk**);
-Chunk*	readintochunks(int);
+int	unpop(char*);
+int	popop(char*);
+int	cpaste(usize, usize);
+void	ccopy(usize, usize);
+void	chold(Chunk*);
+void	ccut(usize, usize);
+void	ccrop(usize, usize);
+Chunk*	chunkfile(int);
+void	initbuf(Chunk*);
 int	cmd(char*);
 void	initcmd(void);
 void	update(void);
 void	setzoom(int, int);
 int	zoominto(vlong, vlong);
-void	setpan(int);
-void	setpage(int);
+void	setrange(usize, usize);
 void	setloop(vlong);
 void	setofs(usize);
+void	setpan(int);
+void	setpage(int);
 void	setjump(usize);
 void	redraw(int);
 void	initdrw(int);
 int	advance(Dot*, usize);
-int	jump(usize);
 Chunk*	p2c(usize, usize*);
-usize	c2p(Chunk*);
-void	setrange(usize, usize);
 int	setpos(usize);
 uchar*	getslice(Dot*, usize, usize*);
 vlong	getbuf(Dot, usize, uchar*, usize);
--- a/pplay.c
+++ b/pplay.c
@@ -16,25 +16,24 @@
 static Mousectl *mc;
 static int cat;
 static int afd = -1;
-static uchar sbuf[Iochunksz];
 
 static void
 athread(void *)
 {
 	int nerr;
-	vlong n;
+	uchar *b;
+	usize n;
 
 	nerr = 0;
 	for(;;){
 		if(afd < 0 || nerr > 10)
 			return;
-		n = getbuf(dot, Outsz, sbuf, sizeof sbuf);
-		if(n < 0){
+		if((b = getslice(&dot, Outsz, &n)) == nil || n <= 0){
 			fprint(2, "athread: %r\n");
 			nerr++;
 			continue;
 		}
-		if(write(afd, sbuf, n) != n){
+		if(write(afd, b, n) != n){
 			fprint(2, "athread write: %r (nerr %d)\n", nerr);
 			break;
 		}
@@ -164,12 +163,12 @@
 			case Kleft: setpage(-1); break;
 			case Kright: setpage(1); break;
 			default:
+				if((p = prompt(r)) == nil || strlen(p) == 0)
+					break;
 				if(treadsoftly){
-					fprint(2, "dropping edit event during ongoing read\n");
+					fprint(2, "dropping edit command during ongoing read\n");
 					break;
 				}
-				if((p = prompt(r)) == nil || strlen(p) == 0)
-					break;
 				qlock(&lsync);
 				switch(cmd(p)){
 				case -1: fprint(2, "cmd \"%s\" failed: %r\n", p); update(); break;