Audio QR Codes

Published Mar 18, 2013 by Pillow Computing Consortium at /blog/2013-03-17-audio-qr-codes/

…are the way of the future.

Everyone’s familiar with the almost-ubiquitous QR codes. They appear no matter where you go - cereal advertisements, fliers for prom, even terrorist communications:

QRCode

As a side note, I would like to thank Kaywa for their free QR code generator. Without their continuing support I would have to use something else, or else (heaven forbid) generate my own QR codes.

Now, QR codes are cool and all, but they’re a little bit commonplace now. They used to be the hip thing. Now everyone who has a smartphone has a QR code reader. I was thinking the other day about my work with SSTV (Slow Scan TeleVision) - essentially, images transferred over sound waves.

Then I saw SpectroTyper, which is very cool oh-by-the-way. It essentially builds an audio file where the spectrograph displays some human readable text.

So I said to myself: I can do that.

And I wrote a little script that will convert an arbitrary QR code into an audio file. You give it data, it spits out a wave file. Here’s an example audio file:

qrcode

I’ll be the first to admit that it doesn’t sound like much. But before you scoff at me too much, pull it into Audacity and look at the spectrograph:

Does that look like something we all know? Scan it if you don’t believe me.

The backend script to generate that isn’t terribly complex. Essentially, it reads an image, and wherever the image is black it adds a cosine wave of the appropriate frequency to that time bucket. Then it normalizes everything and we’re finished.

Feel free to play around with the QR code generator - however, bear in mind that it’s generating each QR code audio bit from scratch on a Pentium 4. So when you submit a job I’ve found it’s usually taken about a minute or two, but jobs are processed linearly so don’t abuse it.

Also, I believe I promised to release the source code for the HDR. Here’s the code:

import Image, ImageOps, ImageChops, math, argparse, os, ImageFilter, ImageDraw 
 
# Computes F(t): 
# - a is the lower asymptote, k is the upper one. 
# - b is the max. growth rate. 
# - v is a symetry constant. Keep at 0.5. 
def logistic(t, a, k, b, v, m): 
    swapped = False 
    if k<a: 
        tmp = k 
        k = a 
        a = tmp 
        swapped = True 
    retval=(k-a) / math.pow(1.0 + v * math.pow(2.7182818, -b*(t-m)), 1.0/v) 
    if swapped: 
        retval = (-k+a)+retval 
    retval += a 
    return retval 
 
def parabola(t, coef, cx, cy): 
    return cy + coef*(t-cx)*(t-cx) 
 
def drawHistogram(histogram, log=False): 
    maximum = 0 
    for h in histogram: 
        if h > maximum: 
            maximum = h 
    img = Image.new("RGBA", (len(histogram), len(histogram))) 
    draw = ImageDraw.Draw(img) 
    cnt = 0 
    for h in histogram: 
        if h == 0: 
            y = 0 
        else: 
            if log: 
                y = math.log(float(h)) / math.log(maximum) 
            else: 
                y = float(h) / maximum 
        #print h, maximum, y 
        draw.line([(cnt, len(histogram)), (cnt, (1.0-y)*len(histogram))]) 
        cnt += 1 
        pass 
    return img 
 
# Assumes the domain of rn is the 0<x<width of image 
def overlayFunction(img, fn): 
    draw = ImageDraw.Draw(img) 
    last_v = (0, 256-fn(0)) 
    for x in range(img.size[0]): 
        new_v = (x, 256-fn(x)) 
        draw.line([last_v, new_v], (255,0,0)) 
        last_v = new_v 
        pass 
    return img 
 
# Light_coef is the amount that the brighter image is used, 
# dark_coef is the same but for the darker image. 
def generateHDR(medium, dark, light, dark_coef=2.0, light_coef=2.0): 
 
    # Load the light, dark, and medium images. 
    im_dark = Image.open(dark).convert() 
    im_med = Image.open(medium).convert() 
    im_light = Image.open(light).convert() 
 
    debug = Image.new("RGB", (im_dark.size[0]*4, im_dark.size[1]*6)) 
    draw = ImageDraw.Draw(debug) 
    w = im_dark.size[0] 
    h = im_dark.size[1] 
 
    debug.paste(im_med, (0,0)) 
    debug.paste(im_dark, (w,0)) 
    debug.paste(im_light, (w*2,0)) 
 
    debug.paste(im_med, (0,h)) 
    debug.paste(im_dark, (w,h)) 
    debug.paste(im_light, (w*2,h)) 
 
    # Create the light and dark layer masks 
    im_dark_mask = ImageOps.grayscale(im_dark) 
    im_light_mask = ImageOps.grayscale(im_light) 
    im_med_mask = ImageOps.grayscale(im_med) 
 
    debug.paste(im_med_mask, (0,h*2)) 
    debug.paste(im_dark_mask, (w,h*2)) 
    debug.paste(im_light_mask, (w*2,h*2)) 
 
    steepness = 0.03 
    minimum = 150.0 
    maximum = 256.0 
 
    # Remember: 256 is 100% transparent, 0 is 100% opaque 
    def darkCurve(i): 
        retval = int(256-logistic(float(i)-100.0, minimum-120, maximum-120, -steepness, 0.
5, 0.0)) 
        return retval-120 
 
    def lightCurve(i): 
        retval = int(logistic(float(i)-190.0, minimum, maximum, steepness, 0.5, 0.0)) 
        return 256-retval 
 
    def mediumCurve(i): 
        retval = int(256-parabola(float(i), 0.01/127.0, 127.0, 256.0)) 
        return retval 
 
    debug.paste(overlayFunction(drawHistogram(im_med_mask.histogram()), mediumCurve).resiz
e((w,h)), (0, h*3)) 
    debug.paste(overlayFunction(drawHistogram(im_dark_mask.histogram()), darkCurve).resize
((w,h)), (w*1, h*3)) 
    debug.paste(overlayFunction(drawHistogram(im_light_mask.histogram()), lightCurve).resi
ze((w,h)), (w*2, h*3)) 
 
    im_dark_mask = im_dark_mask.point(darkCurve) 
    im_light_mask = im_light_mask.point(lightCurve) 
    im_med_mask = im_med_mask.point(mediumCurve) 
 
    debug.paste(im_med_mask, (0,h*4)) 
    debug.paste(im_dark_mask, (w,h*4)) 
    debug.paste(im_light_mask, (w*2,h*4)) 
 
    # Create a new result image 
    result = im_med 
    debug.paste(result, (0, h*5)) 
 
    # Add the light image through the mask 
    result = Image.composite(im_dark, result, im_dark_mask.filter(ImageFilter.BLUR)) 
    debug.paste(result, (w, h*5)) 
    result = Image.composite(im_light, result, im_light_mask.filter(ImageFilter.BLUR)) 
    debug.paste(result, (w*2, h*5)) 
    debug.paste(result, (w*3, h*5)) 
    debug.paste(drawHistogram(ImageOps.grayscale(result).histogram()).resize((w,h)), (w*3,
 h*4)) 
    debug.save("debug.jpg") 
    return result 
 
parser = argparse.ArgumentParser() 
parser.add_argument("--file-list", nargs='*', help="Processes the list of files specified.
 Files are interpreted in sets of three, where the first is the medium, the next is the da
rkest, and the final in the set is the brightest.") 
parser.add_argument("--output-dir") 
 
args = parser.parse_args() 
 
if args.file_list: 
    if len(args.file_list) > 0 and len(args.file_list)%3 == 0: 
        output_dir = args.output_dir 
        if not output_dir: 
            output_dir = "result" 
        try: 
            os.mkdir(output_dir) 
        except OSError: 
            pass 
        for i in range(0, len(args.file_list), 3): 
            result = generateHDR(args.file_list[i], args.file_list[i+1], args.file_list[i+
2]) 
            result.save("%s/%s"%(output_dir, args.file_list[i].split("/")[-1])) 
    else: 
        print "You must have more than zero, and a multiple of three files." 
else: 
    print "Send us some arguments! (-h for more)"

 

Lane Kolbly

Story logo

© 2022 Pillow Computing Consortium