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