Close

Oscilloscope Sing-Along: Rotating Cube

A project log for Microhacks

A collection of small ideas

ted-yapoTed Yapo 02/04/2018 at 20:211 Comment

I read about Neil Fraser's audio-based oscilloscope art on the blog, and I got hooked.  This time it's a rotating cube done in python.  I've made a video of the result, and synchronized the wav file as the soundtrack, so you can hook an oscilloscope to your headphone jack and "sing-along" at home.

The cube is a bit jumpy because the low-frequency response of my soundcard doesn't extend down to DC, and the youtube audio compression takes its toll as well (odd wiggles), but it still works.

The output is best viewed on an analog scope, but you may have luck with some digital models, too.  For instance, I found that on a Rigol DS1054Z, setting the Mem Depth (Acquire menu) to 6k, and turning the horizontal to 1ms works well.

Here's the python code, for those interested.  The ScopeDisplay class is probably the most interesting part.  The rest of the code just draws the cube, using backface culling to avoid drawing the hidden lines, but that's easily removed if you want to see them.  I really enjoyed remembering all this stuff.  Kids today are spoiled with their fancy depth-buffers!

If x- and y- seem reversed, just swap them on the scope.  You've got a 50-50 shot of getting it right the first time :-)

#!/usr/bin/env python

# this code is in the public domain

import wave
import struct
import math

class ScopeDisplay():
  def __init__(self, sample_rate, filename):
    self.sample_rate = sample_rate
    self.wav = wave.open(filename, 'w')
    self.wav.setnchannels(2)
    self.wav.setsampwidth(1)
    self.wav.setframerate(sample_rate)
    self.wav.setnframes(1)
    self.data = []
  def point(self, x, y):
    left = int(min(max(128+127*x, 0), 255))
    right = int(min(max(128+127*y, 0), 255))
    self.data.append(struct.pack('B', left))
    self.data.append(struct.pack('B', right))
  def line(self, x0, y0, x1, y1, step = 1):
    d = math.sqrt((x0-x1)*(x0-x1) + (y0-y1)*(y0-y1))
    n_pts = max(2, int(step * d))
    for i in range(0, n_pts):
      x = x0 + (x1 - x0) * i / (n_pts-1)
      y = y0 + (y1 - y0) * i / (n_pts-1)
      self.point(x, y)
  def close(self):
    self.wav.writeframes(''.join(self.data))
    self.wav.close()


display = ScopeDisplay(48000, 'oscilloscope_cube.wav')

# cube points
x = [-1, -1, -1, -1, +1, +1, +1, +1]
y = [-1, -1, +1, +1, -1, -1, +1, +1]
z = [-1, +1, +1, -1, -1, +1, +1, -1]

# cube faces (indexes into points)
faces = [[1,2,6,5],[0,4,7,3],[4,7,6,5],[0,1,2,3],[2,6,7,3],[0,1,5,4]]

# face normals (for hidden line-removal)
nx = [0, 0, 1,-1, 0, 0]
ny = [0, 0, 0, 0, 1,-1]
nz = [1,-1, 0, 0, 0, 0]

# viewpoint pe{x,y,z}
pex = 0
pey = 0
pez = 3

# cube size
scale = 1/math.sqrt(2)

# rotation speeds
x_speed = 0.002
y_speed = 0.0001
z_speed = 0.00003

for i in range(0, 100000):
  # draw cube
  for f_idx in range(0, len(faces)):
    face = faces[f_idx]
    nx0 = nx[f_idx]
    ny0 = ny[f_idx]
    nz0 = nz[f_idx]
    for pair in [[face[0], face[1]],
                 [face[1], face[2]],
                 [face[2], face[3]],
                 [face[3], face[0]]]:
      x0 = scale*x[pair[0]]
      y0 = scale*y[pair[0]]
      z0 = scale*z[pair[0]]
      x1 = scale*x[pair[1]]
      y1 = scale*y[pair[1]]
      z1 = scale*z[pair[1]]

      # rotate cube around y-axis
      theta = 2*math.pi * i * y_speed
      cs = math.cos(theta)
      sn = math.sin(theta)
      xq0 = x0 * cs - z0 * sn
      yq0 = y0
      zq0 = x0 * sn + z0 * cs 
      xq1 = x1 * cs - z1 * sn
      yq1 = y1
      zq1 = x1 * sn + z1 * cs
      nx1 = nx0 * cs - nz0 * sn
      ny1 = ny0
      nz1 = nx0 * sn + nz0 * cs

      # rotate cube around x-axis
      theta = 2*math.pi * i * x_speed
      cs = math.cos(theta)
      sn = math.sin(theta)
      xw0 = xq0
      yw0 = yq0 * cs - zq0 * sn
      zw0 = yq0 * sn + zq0 * cs 
      xw1 = xq1
      yw1 = yq1 * cs - zq1 * sn
      zw1 = yq1 * sn + zq1 * cs
      nx2 = nx1
      ny2 = ny1 * cs - nz1 * sn
      nz2 = ny1 * sn + nz1 * cs

      # rotate cube around z-axis
      theta = 2*math.pi * i * z_speed
      cs = math.cos(theta)
      sn = math.sin(theta)
      xr0 = xw0 * cs - yw0 * sn
      yr0 = xw0 * sn + yw0 * cs
      zr0 = zw0 
      xr1 = xw1 * cs - yw1 * sn
      yr1 = xw1 * sn + yw1 * cs
      zr1 = zw1 
      nx3 = nx2 * cs - ny2 * sn
      ny3 = nx2 * sn + ny2 * cs
      nz3 = nz2 

      # project points into screen space
      d0 = (1 - pez) / (zr0 - pez)
      xs0 = pex + d0*(xr0 - pex)
      ys0 = pey + d0*(yr0 - pey)
      d1 = (1 - pez) / (zr1 - pez)
      xs1 = pex + d1*(xr1 - pex)
      ys1 = pey + d1*(yr1 - pey)

      # cull back-faces based
      #   on dot(normal, view vector)
      dot0 = nx3*(xr0 - pex) + ny3*(yr0 - pey) + nz3*(zr0 - pez)
      dot1 = nx3*(xr1 - pex) + ny3*(yr1 - pey) + nz3*(zr1 - pez)
      if dot0 < 0 and dot1 < 0:
        display.line(xs0, ys0, xs1, ys1, 20)

display.close()

Discussions

kjw wrote 07/22/2024 at 22:36 point

See also "Oscilloscope Music Kickstarter (June 2015): https://www.youtube.com/watch?v=qnL40CbuodU There are many more like this.

  Are you sure? yes | no