Skip to content

Instantly share code, notes, and snippets.

@dmishin
Created December 24, 2015 14:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dmishin/91dd2553c3d7dfc1478a to your computer and use it in GitHub Desktop.
Save dmishin/91dd2553c3d7dfc1478a to your computer and use it in GitHub Desktop.
#!/usr/bin/env python
#This is Python3 executable
import os.path
from numpy import array, hstack, vstack, clip
from PIL import Image
import numpy as np
import subprocess
import re
#SOX_EXEC = r"C:\Program Files\sox-14-4-2\sox.exe"
SOX_EXEC = r"sox"
def hilbert_indices(N):
m = array([[0]], dtype=np.int32)
for i in range(N):
d = 4**i
m = vstack( (hstack( (m.T, m.T[::-1, ::-1] + 3*d )),
hstack( (m+d, m+2*d) ) ) )
return m
def palette_gray():
return [x for x in range(256) for _chan in range(3) ]
def palette_colored(gamma):
r = [(i/127.0)**gamma for i in range(1,128)]
palette = [(p, p*0.7, 0) for p in r][::-1] + [(0,0,0)] + \
[(0, p*0.7, p) for p in r]
palette.append((0,0.7,1)) #make 256 samples
return [int(p*255) for rgb in palette for p in rgb ]
def data2image(img, palette):
img = Image.fromarray( np.squeeze(img).astype(np.uint8)+127, "P")
img.putpalette(palette)
return img
def soxversion():
return subprocess.check_output([SOX_EXEC, "--version"]).strip().decode('mbcs','ignore')
def soxinfo( filename ):
info_args = [SOX_EXEC,
"--info",
filename]
info = subprocess.check_output(info_args)
channels = int(re.search(br"Channels\s*:\s*(\d+)", info ).group(1))
bits = int(re.search(br"Precision\s*:\s*(\d+)-bit", info ).group(1))
rate = int(re.search(br"Sample Rate\s*:\s*(\d+)", info ).group(1))
samples = int(re.search(br"(\d+)\s+samples").group(1))
return channels, bits, rate, samples
def read_sound( filename, out_byps=1, trim=None ):
"""Read sound file, using SOX"""
cmd = [SOX_EXEC,
filename, # input filename
'-t','raw', # output file type raw
'-e','signed-integer', # output encode as signed ints
'-L', # output little endin
'-b',str(out_byps*8), # output bytes per sample
'-c', '1', # mono sound
'-', # output to stdout
]
if trim is not None:
# only extract requested part
cmd.extend(['trim', str(trim[0])+'s', str(trim[1])+'s'] )
return np.fromstring(subprocess.check_output(cmd),'<i%d'%(out_byps))
def ilog2(x):
log = 0
while x > 1:
log += 1
x = x // 2
return log
def normalize_int8_array( data ):
"""Scale int8 array to span at least +- 127"""
if data.dtype != np.int8:
raise TypeError("Wrong data type, must be int8")
amplitude = max( data.max(), -data.min() )
if amplitude < 127:
return (data.astype(np.float32)*(1.0/amplitude*127)).astype(np.int8)
else:
return data
def data_array2hilbert_image(data, palette):
"""Bend linear data array to a Hilbert curve, apply palette and return image. Data must have type int8"""
size = max(data.shape)
print ("Read sound samples:", size)
n = ilog2(size)//2
print ("Hilbert curve order:", n)
#now n contains hilbert curve order
return data2image( data[hilbert_indices( n )],
palette=palette)
if __name__=="__main__":
import sys
import argparse
PALETTES = {
'gray': lambda: palette_gray(),
'color-bright': lambda: palette_colored(0.5),
'color': lambda: palette_colored(1.0),
'color-dark': lambda: palette_colored(1.5)
}
parser = argparse.ArgumentParser()
parser.add_argument("audiofile",
help="Input audio file, any fomat supported by SOX.")
parser.add_argument("imgfile",
nargs='?',
help="Output image file. If not specified, image is shown. Image format must be PNG or GIF, image will be saved in indexed color mode.")
parser.add_argument("-t", "--trim",
metavar="SAMPLE",
help="Start and end samples to take from audio.", type=int, nargs=2)
parser.add_argument("-p", "--palette",
default="color",
choices=PALETTES.keys(),
help="Name of the palette")
parser.add_argument("--sox", metavar="PATH", help="path to the SOX executable", default=SOX_EXEC)
args = parser.parse_args()
SOX_EXEC = args.sox
try:
print("Detecting SOX version...")
print(soxversion())
except Exception as err:
print("Failed to run {SOX_EXEC}".format(SOX_EXEC=SOX_EXEC))
print("If SOX is installed, edit this script and put correct path. If not, install it.")
print(err)
exit(1)
#read data and convert it to image
image = data_array2hilbert_image\
( normalize_int8_array(read_sound( args.audiofile,
trim = args.trim or None,
out_byps=1 )),
palette=PALETTES[args.palette]())
if args.imgfile is None:
image.show()
else:
image.save(args.imgfile)
################################################################################
# Tests
################################################################################
if __name__!="__main__":
import unittest
class TestHilbertIndex(unittest.TestCase):
def test_hilbert_indices0(self):
m = hilbert_indices(0)
me = array( [[0]], dtype=np.int )
self.assertEqual( m, me )
def test_hilbert_indices1(self):
m = hilbert_indices(1)
me = array( [[0,3],
[1,2]],
dtype=np.int )
self.assertTrue( (m == me).all() )
def test_hilbert_indices2(self):
m = hilbert_indices(2)
me = array( [[0,1,14,15],
[3,2,13,12],
[4,7,8,11],
[5,6,9,10]],
dtype=np.int )
self.assertTrue( (m == me).all(), "returned: \n{m}, expected: \n{me}".format(m=m,me=me) )
def test_hilbert_indicesN(self):
N = 6
m = hilbert_indices(N)
x,y = 0,0
for i in range(4**N-1):
self.assertEqual( m[x,y], i, "m[{x},{y}]=={i}".format(**locals()))
found = False
for dx, dy in [(-1,0),(1,0),(0,1),(0,-1)]:
x1 = x+dx
y1 = y+dy
if x1 < 0 or x1 >= 2**N: continue
if y1 < 0 or y1 >= 2**N: continue
if m[x1,y1] == i+1:
found = True
break
self.assertTrue(found, "not found neighbor {i} for {x}, {y}".format(**locals()))
x,y = x1,y1
@dmishin
Copy link
Author

dmishin commented Dec 24, 2015

Script to convert audio files to images, by wrapping wave profile along Hilbert curve.

Requires

  • Python 3
    • PIL (Pillow) - to manipulate images
  • SOX - to read audio data

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment