smallpond

Unnamed repository; edit this file 'description' to name the repository.
git clone git://git.nihaljere.xyz/smallpond
Log | Files | Refs | README | LICENSE

smallpond.lua (31272B)


      1 -- parse
      2 local em = 8
      3 
      4 local Glyph = {
      5 	["noteheadWhole"] = 0xE0A2,
      6 	["noteheadHalf"] = 0xE0A3,
      7 	["noteheadBlack"] = 0xE0A4,
      8 	["flag8thDown"] = 0xE241,
      9 	["flag8thUp"] = 0xE240,
     10 	["accidentalFlat"] = 0xE260,
     11 	["accidentalNatural"] = 0xE261,
     12 	["accidentalSharp"] = 0xE262,
     13 	["gClef"] = 0xE050,
     14 	["fClef"] = 0xE062,
     15 }
     16 
     17 local Clef = {
     18 	["treble"] = {
     19 		glyph = Glyph.gClef,
     20 		yoff = 3*em,
     21 		defoctave = 4,
     22 		place = function(char, octave)
     23 			local defoctave = 4 -- TODO: how do we use the value above?
     24 			local NOTES = "abcdefg"
     25 			local s, _ = string.find(NOTES, char)
     26 			return (octave - defoctave) * 7 + 2 - s
     27 		end
     28 	},
     29 	["bass"] = {
     30 		glyph = Glyph.fClef,
     31 		yoff = em,
     32 		defoctave = 3,
     33 		place = function(char, octave)
     34 			local defoctave = 3 -- TODO: how do we use the value above?
     35 			local NOTES = "abcdefg"
     36 			local s, _ = string.find(NOTES, char)
     37 			return (octave - defoctave) * 7 + 4 - s
     38 		end
     39 	}
     40 }
     41 
     42 local numerals = {
     43 	['0'] = 0xE080,
     44 	['1'] = 0xE081,
     45 	['2'] = 0xE082,
     46 	['3'] = 0xE083,
     47 	['4'] = 0xE084,
     48 	['5'] = 0xE085,
     49 	['6'] = 0xE086,
     50 	['7'] = 0xE087,
     51 	['8'] = 0xE088,
     52 	['9'] = 0xE089
     53 }
     54 
     55 voice_commands = {
     56 	staff = {
     57 		parse = function(text, start)
     58 			-- move past "\staff "
     59 			start = start + 7
     60 			local text = string.match(text, "(%a+)", start)
     61 			return 7 + #text, {command="changestaff", name=text}
     62 		end
     63 	},
     64 	clef = {
     65 		parse = function(text, start)
     66 			-- move past "\clef "
     67 			start = start + 6
     68 			if string.match(text, "^treble", start) then
     69 				return #"\\clef treble", {command="changeclef", kind="treble"}
     70 			elseif string.match(text, "^bass", start) then
     71 				return #"\\clef bass", {command="changeclef", kind="bass"}
     72 			else
     73 				error(string.format("unknown clef %s", string.sub(text, start)))
     74 			end
     75 		end
     76 	},
     77 	time = {
     78 		parse = function(text, start)
     79 			-- move past "\time "
     80 			start = start + 6
     81 			local num, denom = string.match(text, "^(%d+)/(%d+)", start)
     82 			if num == nil or denom == nil then
     83 				error(string.format("bad time signature format"))
     84 			end
     85 			return #"\\time " + #num + #denom + 1, {command="changetime", num=num, denom=denom}
     86 		end
     87 	}
     88 }
     89 
     90 local voices = {}
     91 local stafforder = {}
     92 
     93 local commands = {
     94 	voice = function (text, start)
     95 		local parsenotecolumn = function(text, start)
     96 			local s, e, flags, count, dot, beam = string.find(text, "^([v^]?)(%d*)(%.?)([%[%]]?)", start)
     97 			local out = {}
     98 
     99 			if string.find(flags, "v", 1, true) then
    100 				out.stemdir = 1
    101 			elseif string.find(flags, "^", 1, true) then
    102 				out.stemdir = -1
    103 			end
    104 
    105 			if beam == '[' then
    106 				out.beam = 1
    107 			elseif beam == ']' then
    108 				out.beam = -1
    109 			end
    110 
    111 			-- make sure that count is a power of 2
    112 			if #count ~= 0 then
    113 				assert(math.ceil(math.log(count)/math.log(2)) == math.floor(math.log(count)/math.log(2)), "note count is not a power of 2")
    114 			end
    115 			out.count = tonumber(count)
    116 			out.dot = #dot == 1
    117 
    118 			return start + e - s + 1, out
    119 		end
    120 		local parsenote = function(text, start)
    121 			-- TODO: should we be more strict about accidentals and stem orientations on rests?
    122 			local s, e, time, note, acc, shift, tie = string.find(text, "^(%d*%.?%d*)([abcdefgs])([fns]?)([,']*)(~?)", start)
    123 			if note then
    124 				local out
    125 				if note == 's' then
    126 					out = {command='srest'}
    127 				else
    128 					out = {command="note", time=tonumber(time), note=note, acc=acc}
    129 				end
    130 
    131 				local _, down = string.gsub(shift, ',', '')
    132 				local _, up = string.gsub(shift, "'", '')
    133 				out.shift = up - down
    134 
    135 				if #tie > 0 then out.tie = true end
    136 				return start + e - s + 1, out
    137 			end
    138 
    139 			error("unknown token")
    140 		end
    141 		local i = start
    142 
    143 		voice = {}
    144 		while true do
    145 			::start::
    146 			i = i + #(string.match(text, "^%s*", i) or "")
    147 			if i >= #text then return i end
    148 			local cmd = string.match(text, "^\\(%a+)", i)
    149 			if cmd == "end" then
    150 				i = i + 4
    151 				break
    152 			end
    153 			if cmd then
    154 				local size, data = voice_commands[cmd].parse(text, i)
    155 				i = i + size
    156 				table.insert(voice, data)
    157 				goto start
    158 			end
    159 
    160 			-- barline
    161 			local s, e = string.find(text, "^|", i)
    162 			if s then
    163 				i = i + e - s + 1
    164 				table.insert(voice, {command="barline"})
    165 				goto start
    166 			end
    167 
    168 			-- grouping (grace or tuplet)
    169 			local s, e, f = string.find(text, "^(%g*)%b{}", i)
    170 			if s then
    171 				local notes = {}
    172 				local grace = false
    173 
    174 				if string.find(f, 'g', 1, true) then
    175 					grace = true
    176 				end
    177 
    178 				local tn, td = string.match(f, 't(%d+)/(%d+)', 1)
    179 				assert(not not tn == not not td)
    180 				if not tn then
    181 					tn = 1
    182 					td = 1
    183 				end
    184 
    185 				-- TODO: deal with notegroups
    186 				i = i + #f + 1
    187 				while i <= e - 2 do
    188 					i = i + #(string.match(text, "^%s*", i) or "")
    189 					if i >= #text then return i end
    190 					i, note = parsenote(text, i)
    191 					i, col = parsenotecolumn(text, i)
    192 					table.insert(voice, {command="newnotegroup", count=col.count, stemdir=col.stemdir, beam=col.beam, dot=col.dot, tuplet=Q.new(td)/tn, grace=grace, notes={[1] = note}})
    193 				end
    194 				i = e + 1
    195 				goto start
    196 			end
    197 
    198 			-- note column
    199 			local s, e = string.find(text, "^%b<>", i)
    200 			if s then
    201 				i = i + 1
    202 				local group = {command="newnotegroup", notes = {}}
    203 				while i <= e - 1 do
    204 					i = i + #(string.match(text, "^%s*", i) or "")
    205 					if i >= #text then return i end
    206 					i, out = parsenote(text, i)
    207 					table.insert(group.notes, out)
    208 				end
    209 				i = e + 1
    210 				i, out = parsenotecolumn(text, i)
    211 				group.count = out.count
    212 				group.stemdir = out.stemdir
    213 				group.beam = out.beam
    214 				group.dot = out.dot
    215 				table.insert(voice, group)
    216 				goto start
    217 			end
    218 
    219 			i, note = parsenote(text, i)
    220 			i, col = parsenotecolumn(text, i)
    221 
    222 			if note.command == 'srest' then
    223 				table.insert(voice, {command='srest', count=col.count})
    224 			else
    225 				table.insert(voice, {command="newnotegroup", count=col.count, stemdir=col.stemdir, beam=col.beam, dot=col.dot, notes={[1] = note}})
    226 			end
    227 		end
    228 
    229 		voices[#voices + 1] = voice
    230 		return i
    231 	end,
    232 	layout = function (text, start)
    233 		local i = start
    234 		while true do
    235 			::start::
    236 			i = i + #(string.match(text, "^%s*", i) or "")
    237 			if i >= #text then return i end
    238 			local cmd = string.match(text, "^\\(%a+)", i)
    239 			if cmd == 'end' then
    240 				i = i + 4
    241 				break
    242 			end
    243 
    244 			if cmd == 'staff' then
    245 				i = i + 7
    246 				local name = string.match(text, '^(%a+)', i)
    247 				table.insert(stafforder, name)
    248 				i = i + #name
    249 				goto start
    250 			end
    251 
    252 
    253 			error('unknown token')
    254 		end
    255 
    256 		return i
    257 	end
    258 }
    259 
    260 function parse(text)
    261 	local i = 1
    262 
    263 	while true do
    264 		i = i + #(string.match(text, "^%s*", i) or "")
    265 		if i >= #text then return nil end
    266 		local cmd = string.match(text, "^\\(%a+)", i)
    267 		if cmd then
    268 			i = i + #cmd + 1
    269 			i = commands[cmd](text, i)
    270 		end
    271 	end
    272 end
    273 
    274 f = assert(io.open("score.sp"))
    275 parse(f:read("*a"))
    276 
    277 local time = Q.new(0)
    278 local octave = 0
    279 local clef = Clef.treble
    280 local lastnote = nil
    281 local staff1 = {}
    282 local points = {}
    283 local pointindices = {}
    284 
    285 function point(t)
    286 	for k, v in pairs(pointindices) do
    287 		if t == k then
    288 			return v
    289 		end
    290 	end
    291 
    292 	table.insert(points, t)
    293 	pointindices[t] = #points
    294 	return pointindices[t]
    295 end
    296 
    297 local timings = {}
    298 local curname
    299 local inbeam = false
    300 local beam
    301 local beams = {}
    302 local beamednotes
    303 local unterminated_ties = {}
    304 local ties = {}
    305 local lastbarline
    306 -- first-order placement
    307 local dispatch1 = {
    308 	newnotegroup = function(data)
    309 		local heads = {}
    310 		local realbeamcount = math.log(data.count) / math.log(2) - 2
    311 		local beamcount
    312 		if inbeam then
    313 			beamcount = math.min(realbeamcount, beamednotes[#beamednotes].realcount)
    314 		else
    315 			beamcount = realbeamcount
    316 		end
    317 		local maxtime, mintime
    318 		local lasthead
    319 		local flipped = false
    320 		for _, note in ipairs(data.notes) do
    321 			octave = octave - note.shift
    322 			local head = {acc=note.acc, y=clef.place(note.note, octave), time=note.time, flip}
    323 
    324 			-- avoid overlapping heads by "flipping" head across stem
    325 			if lasthead and math.abs(head.y - lasthead.y) == 1 then
    326 				flipped = true
    327 				if head.y % 2 == 1 then
    328 					head.flip = true
    329 				else
    330 					lasthead.flip = true
    331 				end
    332 			end
    333 			lasthead = head
    334 			table.insert(heads, head)
    335 			if note.time and not maxtime then maxtime = note.time end
    336 			if maxtime and note.time and note.time > maxtime then maxtime = note.time end
    337 
    338 			if note.time and not mintime then mintime = note.time end
    339 			if maxtime and note.time and note.time < maxtime then maxtime = note.time end
    340 
    341 			if unterminated_ties[curname] and unterminated_ties[curname].y == head.y then
    342 				table.insert(ties, {staff=curname, start=unterminated_ties[curname], stop=head})
    343 				unterminated_ties[curname] = nil
    344 			end
    345 
    346 			if note.tie then
    347 				assert(unterminated_ties[curname] == nil)
    348 				unterminated_ties[curname] = head
    349 			end
    350 		end
    351 
    352 
    353 		local index = point(time)
    354 		if flipped and maxtime then timings[index].flipped = true end
    355 		if not timings[index].mintime then
    356 			timings[index].mintime = mintime
    357 		else
    358 			timings[index].mintime = math.min(mintime, timings[index].mintime)
    359 		end
    360 
    361 		local incr = Q.new(1) / Q.new(data.count)
    362 		if data.dot then
    363 			incr = 3*incr / 2
    364 		end
    365 		if data.tuplet then incr = incr * data.tuplet end
    366 
    367 		local stemlen
    368 		if data.grace then
    369 			stemlen = 2.5
    370 		else
    371 			stemlen = 3.5
    372 		end
    373 		local note = {kind="notecolumn", stemdir=data.stemdir, stemlen=stemlen, dot=data.dot, grace=data.grace, count=incr, length=data.count, time=maxtime, heads=heads, staff=curname}
    374 		if data.beam == 1 then
    375 			assert(not inbeam)
    376 			beamednotes = {}
    377 			table.insert(beams, beamednotes)
    378 			table.insert(beamednotes, {note=note, count=beamcount, realcount=realbeamcount})
    379 			if data.grace then beamednotes.grace = data.grace end
    380 			beamednotes.maxbeams = beamcount
    381 			note.beamgroup = beamednotes
    382 			inbeam = true
    383 		elseif data.beam == -1 then
    384 			assert(inbeam)
    385 			inbeam = false
    386 			table.insert(beamednotes, {note=note, count=beamcount, realcount=realbeamcount})
    387 			beamednotes.maxbeams = math.max(beamednotes.maxbeams, beamcount)
    388 			note.beamgroup = beamednotes
    389 		elseif inbeam then
    390 			beamednotes.maxbeams = math.max(beamednotes.maxbeams, beamcount)
    391 			table.insert(beamednotes, {note=note, count=beamcount, realcount=realbeamcount})
    392 			note.beamgroup = beamednotes
    393 		end
    394 
    395 		table.insert(staff1[curname], note)
    396 
    397 		local index = point(time)
    398 		if note.grace then
    399 			table.insert(timings[index].staffs[curname].pre, note)
    400 		else
    401 			table.insert(timings[index].staffs[curname].on, note)
    402 			time = time + incr
    403 		end
    404 		lastnote = note
    405 	end,
    406 	changeclef = function(data)
    407 		local class = assert(Clef[data.kind])
    408 		local clefitem = {kind="clef", class=class}
    409 		local index = point(time)
    410 		timings[index].staffs[curname].clef = clefitem
    411 		table.insert(staff1[curname], clefitem)
    412 
    413 		clef = class
    414 		octave = class.defoctave
    415 	end,
    416 	changestaff = function(data)
    417 		if staff1[data.name] == nil then
    418 			staff1[data.name] = {}
    419 		end
    420 		curname = data.name
    421 
    422 		-- mark cross staff beams for special treatment later
    423 		if inbeam then beamednotes.cross = true end
    424 	end,
    425 	changetime = function(data)
    426 		local timesig = {kind="time", num=data.num, denom=data.denom}
    427 		local index = point(time)
    428 		timings[index].staffs[curname].timesig = timesig
    429 		table.insert(staff1[curname], timesig)
    430 	end,
    431 	barline = function(data)
    432 		local index = point(time)
    433 		timings[index].barline = true
    434 		lastbarline = index
    435 		lastnote = nil
    436 	end,
    437 	srest = function(data)
    438 		table.insert(staff1[curname], {kind='srest', length=data.count, time=time})
    439 		time = time + 1 / Q.new(data.count)
    440 	end,
    441 }
    442 
    443 for _, voice in ipairs(voices) do
    444 	time = Q.new(0)
    445 	for _, item in ipairs(voice) do
    446 		local index = point(time)
    447 		if not timings[index] then timings[index] = {staffs={}} end
    448 		if curname and not timings[index].staffs[curname] then timings[index].staffs[curname] = {pre={}, on={}, post={}} end
    449 		assert(dispatch1[item.command])(item)
    450 	end
    451 end
    452 
    453 table.sort(points)
    454 
    455 for _, beam in pairs(beams) do
    456 	-- check which way the stem should point on all the notes in the beam
    457 	local ysum = 0
    458 	for _, entry in ipairs(beam) do
    459 		-- FIXME: note.heads[1].y is wrong
    460 		ysum = ysum + entry.note.heads[1].y
    461 	end
    462 
    463 	local stemdir
    464 	if ysum >= 0 then
    465 		stemdir = -1
    466 	else
    467 		stemdir = 1
    468 	end
    469 
    470 	-- check that stem direction hasn't been set manually
    471 	local unset = true
    472 	for _, entry in ipairs(beam) do
    473 		if entry.note.stemdir then
    474 			unset = false
    475 			break
    476 		end
    477 	end
    478 
    479 	-- update the stem direction
    480 	if unset then
    481 		for _, entry in ipairs(beam) do
    482 			entry.note.stemdir = stemdir
    483 		end
    484 	end
    485 end
    486 
    487 local staff3 = {}
    488 local extra3 = {}
    489 
    490 local x = 10
    491 local lasttime = 0
    492 
    493 for staff, _ in pairs(staff1) do
    494 	staff3[staff] = {}
    495 end
    496 
    497 local staff3ify = function(timing, el, staff)
    498 	local xdiff
    499 	local tindex = point(timing)
    500 	if el.kind == "notecolumn" then
    501 		local glyphsize
    502 		if el.grace then
    503 			glyphsize = 24
    504 		else
    505 			glyphsize = 32
    506 		end
    507 		local rx = x
    508 		xdiff = 10
    509 		rx = rx + xdiff
    510 
    511 		local glyph
    512 		if el.length == 1 then
    513 			glyph = Glyph["noteheadWhole"]
    514 		elseif el.length == 2 then
    515 			glyph = Glyph["noteheadHalf"]
    516 		elseif el.length >= 4 then
    517 			glyph = Glyph["noteheadBlack"]
    518 		end
    519 
    520 		local w, h = glyph_extents(glyph, glyphsize)
    521 
    522 		local preoffset = 0
    523 
    524 		-- TODO: increment on each accidental to reduce overlap
    525 		for _, head in ipairs(el.heads) do
    526 			if #head.acc then
    527 				preoffset = 10
    528 			end
    529 		end
    530 
    531 		-- offset of stem if a head is drawn on opposite side of stem
    532 		local altoffset = 0
    533 		if timings[tindex].flipped then
    534 			altoffset = w - 1.2
    535 		end
    536 
    537 		local heightsum = 0
    538 		local lowheight
    539 		local highheight
    540 		for _, head in ipairs(el.heads) do
    541 			heightsum = heightsum + head.y
    542 			local ry = (em*head.y) / 2 + 2*em
    543 			if not lowheight then lowheight = ry end
    544 			if not highheight then highheight = ry end
    545 			if head.flip then
    546 				head.glyph = {kind="glyph", width=w, size=glyphsize, glyph=glyph, x=preoffset + rx, y=ry, time={start=head.time}}
    547 			else
    548 				head.glyph = {kind="glyph", width=w, size=glyphsize, glyph=glyph, x=preoffset + altoffset + rx, y=ry, time={start=head.time}}
    549 			end
    550 			table.insert(staff3[staff], head.glyph)
    551 
    552 			if el.dot then
    553 				xdiff = xdiff + 5
    554 				table.insert(staff3[staff], {kind="circle", r=1.5, x=preoffset + altoffset + rx + w + 5, y=ry, time={start=head.time}})
    555 			end
    556 			if head.acc == "s" then
    557 				table.insert(staff3[staff], {kind="glyph", size=glyphsize, glyph=Glyph["accidentalSharp"], x=rx, y=ry, time={start=head.time}})
    558 			elseif head.acc == "f" then
    559 				table.insert(staff3[staff], {kind="glyph", size=glyphsize, glyph=Glyph["accidentalFlat"], x=rx, y=ry, time={start=head.time}})
    560 			elseif head.acc == "n" then
    561 				table.insert(staff3[staff], {kind="glyph", size=glyphsize, glyph=Glyph["accidentalNatural"], x=rx, y=ry, time={start=head.time}})
    562 			end
    563 
    564 			lowheight = math.min(lowheight, ry)
    565 			highheight = math.max(highheight, ry)
    566 
    567 			local stoptime
    568 			if el.time then stoptime = el.time + 1 else stoptime = nil end
    569 			-- TODO: only do this once per column
    570 			-- leger lines
    571 			if head.y <= -6 then
    572 				for j = -6, head.y, -2 do
    573 					table.insert(staff3[staff], {kind="line", t=1.2, x1=altoffset + preoffset + rx - .2*em, y1=(em * (j + 4)) / 2, x2=altoffset + preoffset + rx + w + .2*em, y2=(em * (j + 4)) / 2, time={start=el.time, stop=stoptime}})
    574 				end
    575 			end
    576 
    577 			if head.y >= 6 then
    578 				for j = 6, head.y, 2 do
    579 					table.insert(staff3[staff], {kind="line", t=1.2, x1=altoffset + preoffset + rx - .2*em, y1=(em * (j + 4)) / 2, x2=altoffset + preoffset + rx + w + .2*em, y2=(em * (j + 4)) / 2, time={start=el.time, stop=stoptime}})
    580 				end
    581 			end
    582 		end
    583 
    584 		if not el.stemdir and el.length > 1 then
    585 			if heightsum <= 0 then
    586 				el.stemdir = 1
    587 			else
    588 				el.stemdir = -1
    589 			end
    590 		end
    591 
    592 		-- stem
    593 		local stemstoptime
    594 		if el.stemdir then
    595 			if el.time then stemstoptime = el.time + .25 else stemstoptime = nil end
    596 			if el.stemdir == -1 then
    597 				-- stem up
    598 				-- advance width for bravura is 1.18 - .1 for stem width
    599 				el.stemx = w + rx - 1.08 + preoffset + altoffset
    600 				local stem = {kind="line", t=1, x1=el.stemx, y1=highheight - .168*em, x2=el.stemx, y2=lowheight -.168*em - el.stemlen*em, time={start=el.time, stop=stemstoptime}}
    601 				el.stem = stem
    602 				table.insert(staff3[staff], el.stem)
    603 			else
    604 				el.stemx = rx + .5 + preoffset + altoffset
    605 				local stem = {kind="line", t=1, x1=el.stemx, y1=lowheight + .168*em, x2=el.stemx, y2=highheight + el.stemlen*em, time={start=el.time, stop=stemstoptime}}
    606 				el.stem = stem
    607 				table.insert(staff3[staff], stem)
    608 			end
    609 		end
    610 
    611 		-- flag
    612 		if el.length == 8 and not el.beamgroup then
    613 			if el.stemdir == 1 then
    614 				local fx, fy = glyph_extents(Glyph["flag8thDown"], glyphsize)
    615 				table.insert(staff3[staff], {kind="glyph", glyph=Glyph["flag8thDown"], size=glyphsize, x=altoffset + preoffset + rx, y=highheight + 3.5*em, time={start=stemstoptime}})
    616 			else
    617 				-- TODO: move glyph extents to a precalculated table or something
    618 				local fx, fy = glyph_extents(Glyph["flag8thUp"], glyphsize)
    619 				table.insert(staff3[staff], {kind="glyph", glyph=Glyph["flag8thUp"], size=glyphsize, x=altoffset + el.stemx - .48, y=lowheight -.168*em - 3.5*em, time={start=stemstoptime}})
    620 				xdiff = xdiff + fx
    621 			end
    622 		end
    623 		xdiff = xdiff + 100 / el.length + 10
    624 		lasttime = el.time
    625 	elseif el.kind == "srest" then
    626 		xdiff = 0
    627 	elseif el.kind == "clef" then
    628 		table.insert(staff3[staff], {kind="glyph", glyph=el.class.glyph, size=32, x=x, y=el.class.yoff, time={start=timings[tindex].mintime, stop=timings[tindex].mintime + 1}})
    629 		xdiff =  30
    630 	elseif el.kind == "time" then
    631 		-- TODO: draw multidigit time signatures properly
    632 		table.insert(staff3[staff], {kind="glyph", glyph=numerals[el.num], size=32, x=x, y=em, time={start=timings[tindex].mintime, stop=timings[tindex].mintime + 1}})
    633 		table.insert(staff3[staff], {kind="glyph", glyph=numerals[el.denom], size=32, x=x, y=3*em, time={start=timings[tindex].mintime, stop=timings[tindex].mintime + 1}})
    634 		xdiff =  30
    635 	end
    636 
    637 	return xdiff
    638 end
    639 
    640 local rtimings = {}
    641 local snappoints = {}
    642 local curclef = {}
    643 for _, time in ipairs(points) do
    644 	local tindex = point(time)
    645 	local todraw = timings[tindex].staffs
    646 
    647 	-- clef
    648 	local xdiff = 0
    649 	for staff, vals in pairs(todraw) do
    650 		if vals.clef and (vals.clef.class ~= curclef[staff]) then
    651 			local diff = staff3ify(time, vals.clef, staff)
    652 			if diff > xdiff then xdiff = diff end
    653 			curclef[staff] = vals.clef.class
    654 		end
    655 	end
    656 
    657 	x = x + xdiff
    658 	xdiff = 0
    659 
    660 	-- time signature
    661 	local xdiff = 0
    662 	for staff, vals in pairs(todraw) do
    663 		if vals.timesig then
    664 			local diff = staff3ify(time, vals.timesig, staff)
    665 			if diff > xdiff then xdiff = diff end
    666 		end
    667 	end
    668 
    669 	x = x + xdiff
    670 	xdiff = 0
    671 
    672 	if timings[tindex].barline then
    673 		local time = timings[tindex].mintime or 0
    674 		if tindex == lastbarline then
    675 			table.insert(extra3, {kind='barline', x=x+25, last=true, time={start=time - 1, stop=time}})
    676 		else
    677 			table.insert(extra3, {kind='barline', x=x+25, time={start=time - 1, stop=time}})
    678 		end
    679 		x = x + 10
    680 	end
    681 
    682 	-- prebeat
    683 	for staff, vals in pairs(todraw) do
    684 		if #vals.pre == 0 then goto nextstaff end
    685 		for _, el in ipairs(vals.pre) do
    686 			local diff = staff3ify(time, el, staff)
    687 			if el.beamref then staff3ify(time, el.beamref, staff) end
    688 			x = x + diff
    689 		end
    690 		::nextstaff::
    691 	end
    692 	xdiff = 0
    693 
    694 	local maxtime = 0
    695 	-- on beat
    696 	for staff, vals in pairs(todraw) do
    697 		if #vals.on == 0 then goto nextstaff end
    698 		local diff
    699 		for _, el in ipairs(vals.on) do
    700 			-- HACK: don't hardcode staff name
    701 			if staff == "low" and el.time and el.time > maxtime then maxtime = el.time end
    702 			diff = staff3ify(time, el, staff)
    703 			if el.beamref then staff3ify(time, el.beamref, staff) end
    704 		end
    705 		if xdiff < diff then xdiff = diff end
    706 		::nextstaff::
    707 	end
    708 
    709 	x = x + xdiff
    710 	rtimings[maxtime] = x
    711 	if maxtime ~= 0 then
    712 		table.insert(snappoints, maxtime)
    713 	end
    714 end
    715 
    716 -- calculate extents
    717 local extents = {}
    718 
    719 for _, staff in pairs(stafforder) do
    720 	local items = staff3[staff]
    721 	extents[staff] = {xmin=0, ymin=0, xmax=0, ymax=0}
    722 	for i, d in ipairs(items) do
    723 		if d.kind == "glyph" then
    724 			local w, h = glyph_extents(d.glyph, 32)
    725 			if d.x - w < extents[staff].xmin then
    726 				extents[staff].xmin = d.x - w
    727 			elseif d.x + w > extents[staff].xmax then
    728 				extents[staff].xmax = d.x + w
    729 			end
    730 
    731 			if d.y - h < extents[staff].ymin then
    732 				extents[staff].ymin = d.y - h
    733 			elseif d.y + h > extents[staff].ymax then
    734 				extents[staff].ymax = d.y + h
    735 			end
    736 		elseif d.kind == "line" then
    737 			if d.x1 < extents[staff].xmin then
    738 				extents[staff].xmin = d.x1
    739 			elseif d.x1 > extents[staff].xmax then
    740 				extents[staff].xmax = d.x1
    741 			end
    742 
    743 			if d.x2 < extents[staff].xmin then
    744 				extents[staff].xmin = d.x2
    745 			elseif d.x2 > extents[staff].xmax then
    746 				extents[staff].xmax = d.x2
    747 			end
    748 
    749 			if d.y1 < extents[staff].ymin then
    750 				extents[staff].ymin = d.y1
    751 			elseif d.y1 > extents[staff].ymax then
    752 				extents[staff].ymax = d.y1
    753 			end
    754 
    755 			if d.y2 < extents[staff].ymin then
    756 				extents[staff].ymin = d.y2
    757 			elseif d.y2 > extents[staff].ymax then
    758 				extents[staff].ymax = d.y2
    759 			end
    760 		elseif d.kind == "quad" then
    761 			if d.x1 < extents[staff].xmin then
    762 				extents[staff].xmin = d.x1
    763 			elseif d.x1 > extents[staff].xmax then
    764 				extents[staff].xmax = d.x1
    765 			end
    766 
    767 			if d.x2 < extents[staff].xmin then
    768 				extents[staff].xmin = d.x2
    769 			elseif d.x2 > extents[staff].xmax then
    770 				extents[staff].xmax = d.x2
    771 			end
    772 
    773 			if d.y1 < extents[staff].ymin then
    774 				extents[staff].ymin = d.y1
    775 			elseif d.y1 > extents[staff].ymax then
    776 				extents[staff].ymax = d.y1
    777 			end
    778 
    779 			if d.y2 < extents[staff].ymin then
    780 				extents[staff].ymin = d.y2
    781 			elseif d.y2 > extents[staff].ymax then
    782 				extents[staff].ymax = d.y2
    783 			end
    784 
    785 			if d.x3 < extents[staff].xmin then
    786 				extents[staff].xmin = d.x3
    787 			elseif d.x3 > extents[staff].xmax then
    788 				extents[staff].xmax = d.x3
    789 			end
    790 
    791 			if d.x4 < extents[staff].xmin then
    792 				extents[staff].xmin = d.x4
    793 			elseif d.x4 > extents[staff].xmax then
    794 				extents[staff].xmax = d.x4
    795 			end
    796 
    797 			if d.y3 < extents[staff].ymin then
    798 				extents[staff].ymin = d.y3
    799 			elseif d.y3 > extents[staff].ymax then
    800 				extents[staff].ymax = d.y3
    801 			end
    802 
    803 			if d.y4 < extents[staff].ymin then
    804 				extents[staff].ymin = d.y4
    805 			elseif d.y4 > extents[staff].ymax then
    806 				extents[staff].ymax = d.y4
    807 			end
    808 		end
    809 	end
    810 end
    811 
    812 local xmax = 0
    813 local yoff = 0
    814 local firstymin, lastymin
    815 local xmin = 0
    816 for i, staff in pairs(stafforder) do
    817 	local extent = extents[staff]
    818 	if xmin > extent.xmin then
    819 		xmin = extent.xmin
    820 	end
    821 
    822 	if xmax < extent.xmax then
    823 		xmax = extent.xmax
    824 	end
    825 
    826 	if i == 1 then
    827 		firstymin = yoff + extent.ymin
    828 	end
    829 
    830 	if i == #stafforder then
    831 		lastymin = yoff - extent.ymin
    832 	end
    833 
    834 	extent.yoff = yoff
    835 	yoff = yoff + extent.ymax - extent.ymin
    836 end
    837 scale = frameheight / yoff
    838 
    839 for _, tie in pairs(ties) do
    840 	local yoff = extents[tie.staff].yoff - extents[tie.staff].ymin
    841 	table.insert(extra3, {kind="curve", x0=tie.start.glyph.x + tie.start.glyph.width + 10, y0=tie.start.glyph.y + yoff, x2=tie.stop.glyph.x + 10, y2=tie.stop.glyph.y + yoff, time={start=tie.start.glyph.time.start, stop=tie.stop.glyph.time.start}})
    842 end
    843 
    844 -- draw beam (and adjust stems) after all previous notes already have set values
    845 for _, notes in ipairs(beams) do
    846 	local beamheight, beamspace
    847 	if notes.grace then
    848 		beamheight = 3
    849 		beamspace = 5
    850 	else
    851 		beamheight = 5
    852 		beamspace = 7
    853 	end
    854 	local x0 = notes[1].note.stemx + .5
    855 	local y0 = notes[1].note.stem.y2 + extents[notes[1].note.staff].yoff - extents[notes[1].note.staff].ymin
    856 	local y0s = notes[1].note.stem.y2
    857 	local yn = notes[#notes].note.stem.y2 + extents[notes[#notes].note.staff].yoff - extents[notes[#notes].note.staff].ymin
    858 	local m = (yn - y0) / (notes[#notes].note.stemx + .5 - x0)
    859 
    860 	-- THIS IS A HACK: replace with more generic mechanism that detects overlap
    861 	-- this only accounts for stems pointing do
    862 	local stemextension = 0
    863 	if notes[1].count == 3 and notes[1].note.stemdir == -1 then
    864 		stemextension = -10
    865 	end
    866 
    867 	if notes.cross then
    868 		if notes[1].note.stemdir == -1 then
    869 			notes[1].note.stem.y2 = notes[1].note.stem.y2 - beamspace * (notes.maxbeams - 1) + beamheight
    870 		end
    871 		if notes[1].note.stemdir == 1 and notes[2] and notes[2].note.stemdir == -1 then
    872 			notes[1].note.stem.y2 = notes[1].note.stem.y2 + beamspace * notes.maxbeams
    873 		end
    874 	end
    875 
    876 	if notes[1].note.stemdir == 1 then
    877 		notes[1].note.stem.y2 = y0s + 7*(notes.maxbeams - 2) + beamheight + stemextension
    878 	end
    879 
    880 	for i, entry in ipairs(notes) do
    881 		if i == 1 then goto continue end
    882 		local note = notes[i].note
    883 		local n = entry.count
    884 		local x1 = notes[i-1].note.stemx + .5
    885 		local prevymin = extents[notes[i-1].note.staff].ymin
    886 		local x2 = note.stemx + .5
    887 		local extent = extents[note.staff]
    888 
    889 		-- change layout parameters depending on stem up or stem down
    890 		local first, last, inc
    891 		if entry.note.stemdir == 1 then
    892 			first = beamspace*(notes.maxbeams - 2)
    893 			last = beamspace*(notes.maxbeams - n - 1)
    894 			if extents[entry.note.staff].yoff < extents[notes[1].note.staff].yoff then
    895 				entry.note.stem.y2 = y0 + m*(x2 - x0) + 7*(notes.maxbeams - 2) + beamheight + extents[entry.note.staff].ymin - extents[entry.note.staff].yoff + stemextension
    896 			else
    897 				entry.note.stem.y2 = y0s + m*(x2 - x0) + 7*(notes.maxbeams - 2) + beamheight + stemextension
    898 			end
    899 			inc = -beamspace
    900 		else
    901 			if extents[entry.note.staff].yoff > extents[notes[1].note.staff].yoff then
    902 				entry.note.stem.y2 = y0 + m*(x2 - x0) + beamheight - extents[entry.note.staff].yoff + extents[entry.note.staff].ymin + stemextension
    903 			else
    904 				entry.note.stem.y2 = y0s + m*(x2 - x0) + stemextension
    905 			end
    906 			first = 0
    907 			last = beamspace*(n-1)
    908 			inc = beamspace
    909 		end
    910 
    911 		-- draw beams segment by segment
    912 		for yoff=first, last, inc do
    913 			local starttime, stoptime
    914 			if note.time then stoptime = note.time + .25 else stoptime = nil end
    915 			if note.time then starttime = notes[i-1].note.time + .25 else starttime = nil end
    916 			if entry.note.stemdir ~= 1 and notes.cross then
    917 				table.insert(extra3, {kind="beamseg", x1=x1 - 0.5 - extent.xmin, x2=x2 - extent.xmin, y1=y0 + m*(x1 - x0) + yoff + stemextension, y2=y0 + m*(x2 - x0) + yoff + stemextension, h=beamheight, time={start=starttime, stop=stoptime}})
    918 			else
    919 				table.insert(extra3, {kind="beamseg", x1=x1 - 0.5 - extent.xmin, x2=x2 - extent.xmin, y1=y0 + m*(x1 - x0) + yoff + stemextension, y2=y0 + m*(x2 - x0) + yoff + stemextension, h=beamheight, time={start=starttime, stop=stoptime}})
    920 			end
    921 		end
    922 		::continue::
    923 	end
    924 end
    925 
    926 for staff, item in ipairs(extra3) do
    927 	if item.kind == 'barline' then
    928 		if item.x < xmin then
    929 			xmin = item.x
    930 		elseif item.x > xmax then
    931 			if item.last then
    932 				xmax = item.x + 7
    933 			else
    934 				xmax = item.x
    935 			end
    936 		end
    937 	end
    938 end
    939 
    940 local lastpoint = 0
    941 for _, point in ipairs(snappoints) do
    942 	lastpoint = math.max(point, lastpoint)
    943 end
    944 
    945 -- TODO: is there a better way to do this?
    946 snappoints[0] = snappoints[1]
    947 local snapidx = 1
    948 local toff_base = 0
    949 function drawframe(time)
    950 	if snappoints[snapidx + 1] and snappoints[snapidx] < time then
    951 		snapidx = snapidx + 1
    952 		toff_base = -rtimings[snappoints[snapidx - 1]]
    953 	end
    954 	local xdiff = rtimings[snappoints[snapidx]] - rtimings[snappoints[snapidx - 1]]
    955 	local delta = xdiff * (time - snappoints[snapidx - 1]) / (snappoints[snapidx] - snappoints[snapidx - 1])
    956 	local toff = toff_base - delta + framewidth / (2*scale)
    957 
    958 	if time > lastpoint + 10 then
    959 		return true
    960 	end
    961 
    962 	for _, staff in ipairs(stafforder) do
    963 		local extent = extents[staff]
    964 		for i, d in ipairs(staff3[staff]) do
    965 			if not d.time.start then goto continue end
    966 			if d.time.start < time then
    967 				if d.kind == "glyph" then
    968 					draw_glyph(scale*d.size, d.glyph, scale*(toff + d.x - extent.xmin), scale*(d.y - extent.ymin + extent.yoff))
    969 				elseif d.kind == "line" then
    970 					local delta = (time - d.time.start) / (d.time.stop - d.time.start)
    971 					local endx, endy
    972 					if d.x1 < d.x2 then
    973 						endx = math.min(d.x1 + delta*(d.x2 - d.x1), d.x2)
    974 					else
    975 						endx = math.max(d.x1 + delta*(d.x2 - d.x1), d.x2)
    976 					end
    977 					if d.y1 < d.y2 then
    978 						endy = math.min(d.y1 + delta*(d.y2 - d.y1), d.y2)
    979 					else
    980 						endy = math.max(d.y1 + delta*(d.y2 - d.y1), d.y2)
    981 					end
    982 					draw_line(scale*d.t, scale*(toff + d.x1 - extent.xmin), scale*(d.y1 - extent.ymin + extent.yoff), scale*(toff + endx - extent.xmin), scale*(endy - extent.ymin + extent.yoff))
    983 				elseif d.kind == "circle" then
    984 					draw_circle(scale*d.r, scale*(toff + d.x - extent.xmin), scale*(d.y - extent.ymin + extent.yoff))
    985 				elseif d.kind == "vshear" then
    986 					local delta = (time - d.time.start) / (d.time.stop - d.time.start)
    987 					local endx, endy
    988 					if d.x1 < d.x2 then
    989 						endx = math.min(d.x1 + delta*(d.x2 - d.x1), d.x2)
    990 					else
    991 						endx = math.max(d.x1 + delta*(d.x2 - d.x1), d.x2)
    992 					end
    993 					if d.y1 < d.y2 then
    994 						endy = math.min(d.y1 + delta*(d.y2 - d.y1), d.y2)
    995 					else
    996 						endy = math.max(d.y1 + delta*(d.y2 - d.y1), d.y2)
    997 					end
    998 					draw_quad(scale*(toff + d.x1 - extent.xmin), scale(d.y1 - extent.ymin + extent.yoff), scale(toff + endx - extent.xmin), scale*(endy - extent.ymin + extent.yoff), scale*(toff + endx - extent.xmin), scale*(endy + d.h - extent.ymin + extent.yoff), scale*(toff + d.x1 - extent.xmin), scale*(d.y1 + d.h - extent.ymin + extent.yoff))
    999 				elseif d.kind == "quad" then
   1000 					draw_quad(scale*(toff + d.x1 - extent.xmin), scale*(d.y1 - extent.ymin + extent.yoff), scale*(toff + d.x2 - extent.xmin), scale*(d.y2 - extent.ymin + extent.yoff), scale*(toff + d.x3 - extent.xmin), scale*(d.y3 - extent.ymin + extent.yoff), scale*(toff + d.x4 - extent.xmin), scale*(d.y4 - extent.ymin + extent.yoff))
   1001 				end
   1002 			end
   1003 
   1004 			::continue::
   1005 		end
   1006 
   1007 		-- draw staff
   1008 		for y=0,em*4,em do
   1009 			draw_line(scale, scale*(toff + xmin), scale*(y + extent.yoff - extent.ymin), scale*(toff + xmax), scale*(y + extent.yoff - extent.ymin))
   1010 		end
   1011 	end
   1012 
   1013 	-- draw barlines
   1014 	for staff, item in ipairs(extra3) do
   1015 		if item.kind == 'barline' then
   1016 			if item.time.start > time then goto continue end
   1017 			local y1 = -firstymin
   1018 			local y2 = lastymin + 4*em
   1019 			local delta = (time - item.time.start) / (item.time.stop - item.time.start)
   1020 			local endy = math.min(y1 + delta*(y2 - y1), y2)
   1021 
   1022 			draw_line(scale, scale*(toff + item.x), scale*(y1), scale*(toff + item.x), scale*(endy))
   1023 			if item.last then
   1024 			draw_line(scale*4, scale*(5 + toff + item.x), scale*(y1), scale*(5 + toff + item.x), scale*(endy))
   1025 			end
   1026 		elseif item.kind == "curve" then
   1027 			if item.time.start > time then goto continue end
   1028 			local delta
   1029 			if item.time.stop < time then
   1030 				delta = 1
   1031 			else
   1032 				delta = (time - item.time.start) / (item.time.stop - item.time.start)
   1033 			end
   1034 			local endx = item.x0 + delta*(item.x2 - item.x0)
   1035 			draw_curve(delta, scale, scale*(toff + item.x0), scale*(item.y0), scale*(toff + (item.x0 + endx) / 2), scale*((item.y0 + item.y2) / 2 + 20), scale*(toff + endx), scale*(item.y2))
   1036 		elseif item.kind == "beamseg" then
   1037 			if item.time.start > time then goto continue end
   1038 			local delta
   1039 			if item.time.stop == item.time.start then
   1040 				delta = 1
   1041 			else
   1042 				delta = (time - item.time.start) / (item.time.stop - item.time.start)
   1043 			end
   1044 			local endx, endy
   1045 			if item.x1 < item.x2 then
   1046 				endx = math.min(item.x1 + delta*(item.x2 - item.x1), item.x2)
   1047 			else
   1048 				endx = math.max(item.x1 + delta*(item.x2 - item.x1), item.x2)
   1049 			end
   1050 			if item.y1 < item.y2 then
   1051 				endy = math.min(item.y1 + delta*(item.y2 - item.y1), item.y2)
   1052 			else
   1053 				endy = math.max(item.y1 + delta*(item.y2 - item.y1), item.y2)
   1054 			end
   1055 			draw_quad(scale*(toff + item.x1), scale*(item.y1), scale*(toff + endx), scale*(endy), scale*(toff + endx), scale*(endy + item.h), scale*(toff + item.x1), scale*(item.y1 + item.h))
   1056 		end
   1057 		::continue::
   1058 	end
   1059 
   1060 	return false
   1061 end