summaryrefslogtreecommitdiff
path: root/image.py
diff options
context:
space:
mode:
Diffstat (limited to 'image.py')
-rwxr-xr-ximage.py161
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!")