diff options
Diffstat (limited to 'image.py')
-rwxr-xr-x | image.py | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/image.py b/image.py new file mode 100755 index 0000000..d1b1fc0 --- /dev/null +++ b/image.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 + +import subprocess as subp +import tempfile +import wave +import os + +from PIL import Image, ImageDraw, ImageFont +from .workspace import Show, Function, SHOW + +BLUE = (0,0,255) +RED = (255,0,0) +WHITE = (255,255,255) +GRAY = (127,127,127) +GREEN = (0, 255, 0) +BLACK = (0,0,0) +YELLOW = (255,255,0) +BAMBER = (255,127,0) +CBLUE = (127,127,255) +NOGEL = (255,255,127) +PURPLE = (255,0,255) + +def get_wave(fname): + """Load an audio file into a wave.""" + with tempfile.NamedTemporaryFile(delete=False,suffix=".wav") as f: + tname = f.name + subp.call(["ffmpeg", "-i", fname, "-acodec", "pcm_s16le", "-y", tname], stdout=subp.DEVNULL, stderr=subp.DEVNULL) + return tname,wave.open(tname,mode="rb") + +def chunks(l, n): + """Split the iterable l into chunks of at most n elements.""" + for i in range(0, len(l), n): + yield l[i:i+n] + +def s16letoi(lsb, msb): + """Convert S16LE (2's complement) to an integer.""" + val = (msb << 8) + lsb + if msb&0b1000000 > 0: ## Negative + return (-1)*((1 << 16) - val) + return val + +def render_image(f:Function): + """Render a QLC+ function. + + This function delegates appropriately, based on the type of function. + """ + if f.type == SHOW: + return render_image_show(f) + else: + raise ValueError("Don't know how to render %s" % f.type) + +def render_image_show(s:Show): + """Render a show to a PIL image.""" + vals, acues = s.render_all() + vals = list(vals) + + values = {} + for t,cv in vals: + for c, v in cv: + if c not in values: + values[c] = 0 + + cheight = 270 + offset = 200 + numheight = 100 + + width = int(vals[-1][0]/10 + 1)+offset + height = (len(values)+1)*cheight+numheight + + ah = lambda y: height-1-y + + font = ImageFont.truetype(font="arial.ttf", size=int(offset*0.8)) + nfont = ImageFont.truetype(font="arial.ttf", size=int(numheight*0.8)) + + colormap = {7:RED, 8:RED, 9:RED, 19: BAMBER, 12: CBLUE, 24: NOGEL, 4: BLUE, 5: BLUE, 6: BLUE, + 25: GREEN, 26: GREEN, 27: GREEN, 13: YELLOW, 14: YELLOW, 15: YELLOW, 16: YELLOW, + 18: PURPLE, 22: PURPLE, 21: PURPLE} + + order = [7,8,9,25,26,27,4,5,6,12,24,19,13,14,15,16,18,22,21] + + im = Image.new("RGB", (width, height), color=WHITE) + draw = ImageDraw.Draw(im) + channels = [i for i in order if i in values]+["A"] + for c in range(len(values)+1): + draw.line(((0, ah(c*cheight)), (width-1,ah(c*cheight))), fill=GRAY, width=1) + draw.text((0,ah(c*cheight + 200)), text=str(channels[c]), font=font, fill=GRAY) + channels = channels[:-1] + + draw.line(((offset-1,0),(offset-1,height-1)), fill=GRAY, width=1) + draw.line(((0,numheight-1), (width-1, numheight-1)), fill=GRAY, width=3) + + atime = [] + for a in acues: + tname, wave = get_wave(a[1]) + nchannels, sampwidth, framerate, nframes, *_ = wave.getparams() + if sampwidth != 2: + raise ValueError("Only 16-bit wave is supported") + skip = framerate//100 + for n,t in enumerate(range(0,nframes, skip)): + asum = 0 + count = 0 + for i in range(skip//10): + for c in chunks(wave.readframes(10)[:nchannels*sampwidth],sampwidth): + asum += abs(s16letoi(*c)) + count += 1 + if skip%10 > 0: + wave.readframes(skip%10) + if not count: + break + if len(atime) > n: + atime[n] = min(atime[n]+asum/count, 32767) + else: + atime.append(asum/count) + + wave.close() + os.remove(tname) + break + + t = 0 + while vals: + #old = {c: v for c,v in values.items()} + while vals and vals[0][0] < t: + _, cv = vals.pop(0) + values.update(cv) + curx = t//10 + offset + if t % 500 == 0: + draw.line(((curx+(t%2000==0),0), (curx+(t%2000==0), height-1)), fill=GRAY, width = 1 + 3*(t % 2000 == 0)) + if t % 2000 == 0: + draw.text((curx+1, 5), text=str(t//1000), font=nfont, fill=GRAY) + for n,c in enumerate(channels): + v = values[c] + cury = ah(n*cheight+1+v) + fill = colormap[c] + draw.point((curx, cury), fill=fill) + draw.line(((curx,ah(n*cheight+1)),(curx, cury)), fill=fill, width=1) + if atime: + aval = abs(int(atime.pop(0)/256)) + draw.line([(curx, ah(len(values)*cheight+128-aval)),(curx, ah(len(values)*cheight+128+aval))], fill=BLACK, width=1) + t += 10 + + return im + +if __name__ == "__main__": + import argparse + + from .workspace import Workspace + + parser = argparse.ArgumentParser(description="Render a show to an image.") + parser.add_argument("workspace", type=str, help="Workspace file") + parser.add_argument("show_id", type=int, help="Show ID") + parser.add_argument("output", type=str, help="Output file") + args = parser.parse_args() + + print("Loading workspace...") + w = Workspace.load(args.workspace) + + s = w.functions[args.show_id] + + print("Rendering show \"%s\"..." % s.name) + render_image(s).save(args.output) + print("Done!") |