[rec.music.synth] Programmable sequencers

tjt@cbnewsh.att.com (timothy.j.thompson) (11/26/90)

(This continues a conversation that began several weeks ago.)

In article <98@generic.UUCP>, taob@pnet91.cts.com (Brian Tao) writes:
> tjt@cbnewsh.att.com (timothy.j.thompson) writes:
> >Rather than aim for that ultimately impossible goal, why not make
> >the sequencer programmable
> the chance.  When you say "programmable", do mean hooks and entry points into
> the application for various "modules"? 

That's one way to do it, demonstrated by the Soundscape and Bars&Pipes
software for the Amiga.  The way I prefer is to embed a music-specific
programming language within the sequencer.  This makes it easier for
non-programmers to use it (since a separate compiler isn't required, and
because an application-specific language can make it easier to learn).
It also makes it more portable and easier for people to share things,
since the same language can be provided on different machines.
An example of this approach is Cakewalk 3.0, which has a language
named CAL that allows you to write editing transformations that can
be invoked from within their sequencer.

My preference is to make the language a much more tightly integrated
part of the sequencer, so that users can create new features that
are completely equivalent to the 'normal' features of the sequencer (e.g.
being able to combine mouse action, graphics, console input and MIDI I/O).
In fact, if the language is suitable enough, you can even use it to implement
all of the 'normal' features of the sequencer.  Besides being extremely
convenient for development, this allows users to change existing features
of the sequencer to suit their own taste.

As you might guess, I've implemented such a system, call Keynote.
(See Computing Systems, Volume 3 Number 2, Spring 1990, for a description.)
It takes a bit of up-front effort to build such a language, but
I can testify to the fact that it's a huge win after you do it (right).
As an example of what you can do with it, I've enclosed below the
complete Keynote code that implements a graphical drum-pattern editor
(which is quite different from the piano-roll-sequencer-style-editor
that Keynote was originally developed to support).  The initial program
was written in under 100 lines of code, and after a few convenience
features it reached 150 or so. 

    ...Tim Thompson...AT&T Bell Labs/Holmdel/NJ...tjt@twitch.att.com...

====================================================================

# This code implements a drum-pattern editor.  To specify the MIDI
# channel and pitches of your drum machine, create a file that looks like:
#
#		chan 10
#		bass 35
#		snare 38
#		closed_hat 42
#		open_hat 46
#
# Then, invoke:
#
#		kboom(nsteps,filename)
#
# where nsteps is the number of steps you want in the drum pattern.
# The step size is 1b/8 (a 32nd) by default, but you can set it (Stepsize)
# to whatever you want.
#
# The screen will show a matrix (drums on vertical axis, beats on
# horizontal axis).  The drum pattern will be playing constantly -
# click mouse button 1 wherever you want to add/delete drum hits.
# Press 's' to start and stop the pattern, 'q' to quit.

Nsteps = 0
Stepsize = 1b/8

function kboom(nsteps,mapfile) {
	readdrums(mapfile)
	arrayinit(Dbeat)
	Nsteps = nsteps
	for ( n=0; n<Nsteps; n++ )
		Dbeat[n] = ''
	Showstart = Showlow = Showbar = 0
	Showhigh = 128
	Kbcount = -1
	Kbon = 1
	Kbxinc = (Nsteps*Stepsize)/50
	Kbxinit = Kbxinc * 12
	Kbyinc = Showhigh / (Kbdrums+1)
	Showleng = Kbxinit + Nsteps * Stepsize
	Grid = ''

	# set up actions for triggering drums and user interaction
	sched(Dbeat[0],0)	# scheduled only once
	sched(kbtrig,Stepsize/2,Stepsize)	# repeatedly scheduled
	temposet(1)		# allows tempo control from console
	action(BUTTON1DOWN,kbmouse)
	interrupt(kbcmd,CONSOLE)

	print "Press 's' to toggle sound, 'q' to quit."
	kbdrawall()

	realtime()	# realtime loop during which everything happens

	# Construct a single phrase containing completed pattern.
	Kbpattern = ''
	for ( n=0; n<Nsteps; n++ ) {
		ph = Dbeat[n]
		ph.time = n * Stepsize
		Kbpattern |= ph
	}
	Kbpattern.length = Nsteps * Stepsize
	print "Result is in Kbpattern."
}

function kbcmd(c) {			# handle keyboard commands
	if ( c == "q" )
		stop()
	else if ( c == "s" ) {		# toggle sound
		Kbon = 1 - Kbon
		if ( Kbon )
			Kbcount = -1	# reset to beginning of pattern
	}
}

function drawbeat0() {		# draw flashing marker on beat 0
	y = kbystart(0)+1
	draw(kbxstart(0)+1,y,kbxstart(1)-1,y,XOR)
}

function kbtrig(b) {	 # repeatedly executed every Stepsize beats
	if ( Kbon ) {
		b = (Kbcount++)%Nsteps
		sched(Dbeat[b],Now+Stepsize/2)
		if ( b == 0 ) {
			# make beat 0 obvious with a flashing line
			sched(drawbeat0,Now+3*Stepsize/8)
			sched(drawbeat0,Now+11*Stepsize/8)
		}
	}
}

function kbxstart(n) {		# return x value of beat n in matrix display
	return Kbxinit+n*Stepsize;
}

function kbystart(n) {		# return y value of drum n in matrix display
	return Showhigh-(n+1)*Kbyinc;
}

function kbmouse() {	# handle mouse button presses, adding/deleting drums
	kb = kd = -1
	# figure out which beat (b) and drum (d) is being chosen
	for ( b=0; b<Nsteps; b++ ) {
		if ( Mouseclick > kbxstart(b) && Mouseclick < (kbxstart(b+1))){
			kb = b;
			break;
		}
	}
	for ( d=0; d<Kbdrums; d++ ) {
		if ( Mousepitch < kbystart(d) && Mousepitch > (kbystart(d+1))){
			kd = d;
			break;
		}
	}
	if ( kb < 0 || kd < 0 )
		return;
	if ( Kbdrumnote[kd] in Dbeat[kb] )
		Dbeat[kb] -= Kbdrumnote[kd]	# if already there, delete it
	else
		Dbeat[kb] |= Kbdrumnote[kd]	# if not there, add it

	# draw (or undraw) an X in the specified box
	draw(kbxstart(kb),kbystart(kd),kbxstart(kb+1),kbystart(kd+1),XOR)
	draw(kbxstart(kb),kbystart(kd+1),kbxstart(kb+1),kbystart(kd),XOR)
}

function kbdrawall() {			# draw matrix and labels
	redraw()			# clear screen
	for ( b=0; b<Nsteps; b++ ) {
		x = kbxstart(b)
		draw(x,Showlow,x,Showhigh)	# draw line
		# show beat number at top
		draw(string(b+1),x+3*Stepsize/8,Showhigh-Kbyinc/2)
	}
	for ( d=0; d<Kbdrums; d++ ) {
		y = kbystart(d)
		draw(0,y,Showleng,y)		# draw line
		# show drum label on left
		draw(Kbdrum[d],Kbxinc,y-5*Kbyinc/8)
	}
}

function readdrums(mapfile, arr) {		# read a drum map file
	arrayinit(arr)
	Kbdrums = 0
	Kbchan = 1
	while ( read ln < mapfile ) {
		split(ln,arr)
		if ( arr[0] == "chan" ) {
			Kbchan = 0 + arr[1]
			continue
		}
		Kbdrum[Kbdrums] = arr[0]
		Kbdrumnote[Kbdrums] = makenote(0+arr[1],Stepsize,'a'.vol,Kbchan)
		Kbdrums++
	}
	close mapfile
}