3D Printed Sound Bites

20K16233

Intro: 3D Printed Sound Bites

After I posted my 3D printed record project, I received an interesting comment from Instructables author rimar2000.  He described an idea he's had for a while about a strip of material that has audio information encoded on it so that it can be played back by quickly sliding a fingernail, credit card, or piece of paper along its surface.  This same basic idea has been implemented in various ways already, here are a few:

talking strip birthday card:

It's even possible to tune rumble strips on a road so that your car turns into a musical instrument as it drives over.  Here is a musical road near MT Fuji, Japan:

another musical road - Lancaster, CA:


The reason rimar2000 suggested this idea to me is because I'd already found a way to convert audio signals into a 3D printable model in my record project.  Using 3D printing it is theoretically possible to convert any piece of audio into a "sound strip," allowing anyone to encode customized messages into this physical format.

I recently found some time to put the code together and print out a few test strips, and unfortunately, I think my process needs a little more refining until it is ready for mass production, but I figured I'd post everything I've started here in case anyone else is interested.  The video below shows the closest I've come so far to reproducing my own voice saying "amanda."  If you know what it's supposed to say, you can kind of make out what's going on here, there are three distinct sections where the sound gets louder - corresponding to the three syllables in the word.



I made this on an Objet Connex500 UV cured resin printer at 300 dpi with 30 micron layers.  It's possible that printing at the max resolution (600dpi with 16 micron layers) might help a little bit, but I haven't had a chance to do it.  It's also possible that other materials or dimensions might help.  I was thinking that tilting the bumps on the surface of the strip so that they are pointing in a 45 degree angle instead of straight up might work better with the angle that the card is hitting them.  If you have any other suggestions please feel free to leave them in the comments.

STEP 1: The Code

The main idea behind this project is to take an audio waveform and use it to modulate the height of the surface of a linear strip.  this way, any object passing across the surface will vibrate up and down in the hills and valley of the wave, these vibrations will cause vibrations in the air, which cause us to hear sound.  You will find comments in my code that talk about the specifics of how it works, follow these steps to create your own 3d models:

1.  Download Audacity.

2.  Open an audio file of your choice with Audacity.  Use Effect>Normalize to amplify the signal as much as you can without clipping.  Trim any excess blank space off the beginning an end of the clip, you will want to keep the audio as short as possible.  File>Export this file and save it as a WAV in a folder called "soundBites". 

3.  Download Python 2.5.4.

4.  Copy the Python code below and save it in the soundBites folder, this code converts the information stored in a wav file (audio) into a series of numbers inside a text file (txt).
//3d printed sound bites - wav to txt
//by Amanda Ghassaei
//May 2013
//https://www.instructables.com/id/3D-Printed-Sound-Bites/

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
*/


import wave
import math
import struct

bitDepth = 8#target bitDepth
frate = 44100#target frame rate

fileName = "your_file_name_here.wav";#file to be imported (change this)

#read file and get data
w = wave.open(fileName, 'r')
numframes = w.getnframes()

frame = w.readframes(numframes)#w.getnframes()

frameInt = map(ord, list(frame))#turn into array

#separate left and right channels and merge bytes
frameOneChannel = [0]*numframes#initialize list of one channel of wave
for i in range(numframes):
    frameOneChannel[i] = frameInt[4*i+1]*2**8+frameInt[4*i]#separate channels and store one channel in new list
    if frameOneChannel[i] > 2**15:
        frameOneChannel[i] = (frameOneChannel[i]-2**16)
    elif frameOneChannel[i] == 2**15:
        frameOneChannel[i] = 0
    else:
        frameOneChannel[i] = frameOneChannel[i]

#convert to string
audioStr = ''
for i in range(numframes):
    audioStr += str(frameOneChannel[i])
    audioStr += ","#separate elements with comma

fileName = fileName[:-3]#remove .wav extension
text_file = open(fileName+"txt", "w")
text_file.write("%s"%audioStr)
text_file.close()

Copy the file name of the audio file you just saved and paste it into the following line in Python:

             fileName = "your_file_name_here.wav"

Hit Run>RunModule, after a minute or two you will have a .txt file saved in the soundBites folder.

5.  Download Processing.

6.  To export STL from Processing, I used the ModelBuilder Library by Marius Watz.  Download the ModelBuilder library here, I used version 0007a03.

7. Unzip the Modelbuilder library .zip and copy the folder inside called "modelbuilder".  Unzip the processing .zip and go to Processing>modes>java>libraries and paste the "modelbuilder" folder in the "libraries" folder.

8.  Copy the processing sketch below and save it as "soundBites.pde" in the soundBites folder.  You can adjust many parameters in this code: the x/y/z resolution of the printer, the speed that you want to playback, the amplitude of the waves, the thickness of the strip, and more... you can even invert the wave to make it recessed within the strip.
//3d printed sound bites
//by Amanda Ghassaei
//May 2013
//https://www.instructables.com/id/3D-Printed-Sound-Bites/

/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
*/

import processing.opengl.*;
import unlekker.util.*;
import unlekker.modelbuilder.*;
import ec.util.*;

String filename = "amanda.txt";

UGeometry geo;//storage for stl geometry
UVertexList lastEdge, currentEdge, end1, end2;//storage for conecting one groove to the next

//parameters
float samplingRate = 44100;//(44.1khz audio initially)
int rateDivisor;//ensures we pack only as much data into this stl as the resolution of the printer can handle
float speed = 26;//inches/sec that the data will be "read" from the surface
boolean invertGroove = true;//false = grove is below surface, true = groove extends above surface
float dpi = 300;//objet printer prints at 600 dpi
byte micronsPerLayer = 16;//microns per vertical print layer

//variable parameters
float amplitude = 50;//amplitude of signal (in z layers)
float sideWidth = 0;//amount of extra space on each side (in pixels)
float bevel = 25;//bevel width (bevel on either side of groove (in pixels)
float grooveWidth = 450;//in pixels
float depth = 1;//measured in z layers, depth of tops of wave in groove from uppermost surface of record
float zHeight = 0.075;//thickness in inches

void setup(){
  
  //set up storage
  geo = new UGeometry();//place to store geometery of vertices
  lastEdge = new UVertexList();
  currentEdge = new UVertexList();
  end1 = new UVertexList();
  end2 = new UVertexList();
  
  setUpVariables();//convert units, initialize etc
  drawGrooves(processAudioData());//draw in grooves using imported audio data
  
  //change extension of file name
  String name = filename;
  int dotPos = filename.lastIndexOf(".");
  if (dotPos > 0)
    name = filename.substring(0, dotPos);
  geo.writeSTL(this, name + ".stl");//write stl file from geomtery
  
  exit();
}

float[] processAudioData(){
  
  //get data out of txt file
  String rawData[] = loadStrings(filename);
  String rawDataString = rawData[0];
  float audioData[] = float(split(rawDataString,','));//separated by commas
  
  //normalize audio data to given bitdepth
  //first find max val
  float maxval = 0;
  for(int i=0;i<audioData.length;i++){
    if (abs(audioData[i])>maxval){
      maxval = abs(audioData[i]);
    }
  }
  //normalize amplitude to max val
  for(int i=0;i<audioData.length;i++){
    audioData[i]*=amplitude/maxval;
  }
  
  return audioData;
}

void setUpVariables(){
  
  //convert everything to inches
  float micronsPerInch = 25400;//scalingfactor
  
  amplitude = amplitude*micronsPerLayer/micronsPerInch;
  depth = depth*micronsPerLayer/micronsPerInch;
  grooveWidth /= dpi;
  sideWidth /= dpi;
  bevel /= dpi;
  
  rateDivisor = int((samplingRate/speed)/600);
  if (rateDivisor == 0){
    rateDivisor = 1;
  }
}

void drawGrooves(float[] audioData){

  int totalSampleNum = audioData.length -1;
  float xPosition;
  float zPosition;
  
  //start at 0
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      lastEdge.add(xPosition,0,0);
    }
  }
  
  
  //draw outer upper edge of groove
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      currentEdge.add(xPosition,sideWidth,0);
    }
  }
  
  
  connectVertices();
  
  //draw outer lower edge of groove
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      zPosition = -depth-amplitude+audioData[sampleNum];
      if (invertGroove){
        currentEdge.add(xPosition,sideWidth+bevel,-zPosition);
      } else {
        currentEdge.add(xPosition,sideWidth+bevel,zPosition);
      }
    }
  } 
  
  
  connectVertices();
  
  
  int lastSampleIndex = 0;
  float finalZPosition = 0;
  //draw inner lower edge of groove
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      zPosition = -depth-amplitude+audioData[sampleNum];
      if (invertGroove){
        currentEdge.add(xPosition,sideWidth+bevel+grooveWidth,-zPosition);
        finalZPosition = -zPosition;
      } else {
        currentEdge.add(xPosition,sideWidth+bevel+grooveWidth,zPosition);
        finalZPosition = zPosition;
      }
      lastSampleIndex = sampleNum;
    }
  }
  
  
  connectVertices();
  
  //draw outer upper edge of groove
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      currentEdge.add(xPosition,sideWidth+2*bevel+grooveWidth,0);
    }
  }
  
  
  connectVertices();
  
  //draw extra upper surface
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      currentEdge.add(xPosition,2*sideWidth+2*bevel+grooveWidth,0);
    }
  }
  
  
  connectVertices();
  
  //bottom edge
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      currentEdge.add(xPosition,2*sideWidth+2*bevel+grooveWidth,-zHeight);
    }
  }
  
  
  connectVertices();
  
  //bottom edge
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      currentEdge.add(xPosition,0,-zHeight);
    }
  }
  
  
  connectVertices();
  
  //end at 0
  for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
    if (sampleNum%rateDivisor == 0){
      xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
      currentEdge.add(xPosition,0,0);
    }
  }
  
  connectVertices(); 
  
  drawEndCaps(audioData, lastSampleIndex, finalZPosition);
  
  print("total length: ");
  print(speed/samplingRate*totalSampleNum);
  println(" inches");
}

void drawEndCaps(float[] audioData, int lastIndex, float finalZ){
  
  float z0Position = -depth-amplitude+audioData[0];
  float xFinalPosition = speed/samplingRate*(lastIndex);
 
  end1.add(xFinalPosition,0,-zHeight);
  end2.add(xFinalPosition,2*sideWidth+2*bevel+grooveWidth,-zHeight);
  end1.add(xFinalPosition,0,0);
  end2.add(xFinalPosition,2*sideWidth+2*bevel+grooveWidth,0);
  end1.add(xFinalPosition,sideWidth,0);
  end2.add(xFinalPosition,sideWidth+2*bevel+grooveWidth,0);
  end1.add(xFinalPosition,sideWidth+bevel,finalZ);
  end2.add(xFinalPosition,sideWidth+bevel+grooveWidth,finalZ);

  geo.quadStrip(end2,end1);
  end1.reset();
  end2.reset();
  
  end1.add(0,0,-zHeight);
  end2.add(0,2*sideWidth+2*bevel+grooveWidth,-zHeight);
  end1.add(0,0,0);
  end2.add(0,2*sideWidth+2*bevel+grooveWidth,0);
  end1.add(0,sideWidth,0);
  end2.add(0,sideWidth+2*bevel+grooveWidth,0);
  if (invertGroove){
    end1.add(0,sideWidth+bevel,-z0Position);
    end2.add(0,sideWidth+bevel+grooveWidth,-z0Position);
  } else {
    end1.add(0,sideWidth+bevel,z0Position);
    end2.add(0,sideWidth+bevel+grooveWidth,z0Position);
  }
  geo.quadStrip(end1,end2);
}

void connectVertices(){
  //connect edges with triangles
  geo.quadStrip(lastEdge,currentEdge);
  
  //set new last edge
  lastEdge.reset();//clear old data
  lastEdge.add(currentEdge);//save current edge
  currentEdge.reset();//clear old data
}

9.  Change the name of the import file in the Processing sketch to match your txt file name:

             String filename = "your_file_name_here.txt";

10.  Hit "Run" in Processing, you should see a file appear in the soundBites folder called "your_file_name.stl", you are now ready for 3d printing.

31 Comments

Thank you very much for sharing this Amanda!
I just have a problem when running the module in Python, it tells me there is invalid syntax and does´t save the .txt file, so I don´t know how to continue...
Any tip on how to fix this? thank you :)

Thanks for your great project

I try to play with it, but i got trouble with python like following:

File "sound.py", line 21, in <module>
frameOneChannel[i] = frameInt[4*i+1]*2**8+frameInt[4*i] #separate channels and store one channel in new list
IndexError: list index out of range

python ver 2.5.4

any one had this issue??

Thxxx

It means your file is mono, try using an audio program to add the missing channel, so make your audio file stereo, event if its just the same content on each channel. Here is a way of doing that using audacity which is free:

https://chacadwa.com/foh/mono-to-stereo-in-audacity

Thanks! I hit the same issue.

commercial way?
Best regards, and congratulations on your work!

Giaccomo Fontanot, Industrial Designer.
printing square waves is the way to go. Are you interested in pursuing this goal in a more
Hi amandaghassaei. Your work is impressive. I am an Industrial Designer, very interested on this issue. I am retina to figure a way to replicate the talking strips from evades ago. I Have a CNC router, and a LOT of interest to see this working. I believe

The sound-strip idea was used at least as far back as 1955 - I remember an advertising novelty from the Schwinn bicycle company. A ridged red plastic ribbon/strip about 1/8 inch wide was tied to the center of a bicycle-wheel shaped disc (as a resonator). Running your fingernail along the strip clearly played "I like Schwinn bikes." I wonder how this was made - it sure was not computers encoding the speech.

Hello.

Thank you for this instructables.

I am trying to print some words on different objects and find some applications for signaletic projects. So I want to get the wave of my sound, but without the thickness of the strip.

I have followed all your instructions and the .stl is a 15M file for just one word with 483324 triangles written. to big for my CPU.

Is there a way to just have the curve ?

I have tried to change the thickness and different parameters but I still have a big block with more 400.000 triangles, not just the desired curve.

all the best for your different beautifull project and thank you for your works.

M.

change this function:

void drawGrooves(float[] audioData){

int totalSampleNum = audioData.length -1;
float xPosition;
float zPosition;

//draw outer lower edge of groove
for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
if (sampleNum%rateDivisor == 0){
xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
zPosition = -depth-amplitude+audioData[sampleNum];
if (invertGroove){
lastEdge.add(xPosition,sideWidth+bevel,-zPosition);
} else {
lastEdge.add(xPosition,sideWidth+bevel,zPosition);
}
}
}

int lastSampleIndex = 0;
float finalZPosition = 0;
//draw inner lower edge of groove
for (int sampleNum=0;sampleNum<totalSampleNum;sampleNum++){
if (sampleNum%rateDivisor == 0){
xPosition = speed/samplingRate*sampleNum;//length/sec * sec/samples * sampleNum
zPosition = -depth-amplitude+audioData[sampleNum];
if (invertGroove){
currentEdge.add(xPosition,sideWidth+bevel+grooveWidth,-zPosition);
finalZPosition = -zPosition;
} else {
currentEdge.add(xPosition,sideWidth+bevel+grooveWidth,zPosition);
finalZPosition = zPosition;
}
lastSampleIndex = sampleNum;
}
}

connectVertices();

print("total length: ");
print(speed/samplingRate*totalSampleNum);
println(" inches");
}

Very interesting, maybe the combination of lowest amplitude and something to make resonate and amplify like string or membrane would improve. I
If you still didn't found way to get the audiosample value with Processing without the help of the Python script it is very fast easy with the Minim library (already there with the IDE) :

import ddf.minim.*;

Minim minim;
AudioSample sample;
float[] audioDataLeft;
float[] audioDataRight;

void setup() {
minim = new Minim(this);
sample = minim.loadSample("yourFilePathHere");
audioDataLeft = sample.getChannel(BufferedAudio.LEFT);
audioDataRight = sample.getChannel(BufferedAudio.RIGHT);
//etc ... etc ...
}

etc ...

You'll get the audio data as array of float value between -1 and 1, if I remember well.
yeah I messed around with that, but it seems to only give you a small bit of buffered data at a time. I wasn't able to find a way to pull the whole song in. It's possible I just wasn't using it right though
This is a great start. I remember having a balloon with a talking strip attached - the surface of the balloon acted as the "cone" of a speaker, and since the surface was big, the sound was bigger as well - similar to the greeting card example in the first clip where the paper is the transducer changing the mechanical vibrations into airborne sound waves. You may have better luck keeping the substrate of the strip as low-mass (easy to vibrate) as possible, then attach to a passive "amplifier" surface. It was a long time ago, but I think the strip on my balloon was almost like Mylar. I don't see how (with current technology/materials) you could print that fine, but you might be able to make a "negative" that could stamp the pattern into a thinner vibration strip.
Perhaps it's my brain filling in the phonemes between the schwa - short a - schwa pattern, but I'm pretty sure I heard "Amanda" when scraped at 3/4 speed. Not bad for plastic! Certainly better than poor Ranjit Bhatnagar's attempt at a similar "scraper"(http://moonmilk.com/2010/02/08/instrument-a-day-6-wave-scraper/). This makes me wonder though; Perhaps this medium isn't ideal for reproducing the relatively complex waveforms of actual speech (a larynx-produced carrier signal modulated by the mouth), but may prefer sounds that emulate primitive speech synthesis circa 1989 (Square wave carrier clunkily modulated to produce phonemes). Maybe it would be worth a try to print a synthesized vowel cycle (aeiou) modulated from a single carrier of uniform frequency and amplitude (pulse or square perhaps?), even with some simple consonants thrown in (carefully placed noise). I see below that others have suggested how these strips usually work using cleverly placed notches of uniform depth rather than waves of variable amplitude. Possibly a simple carrier signal, with uniform peaks and unmodulated amplitude (only freq & harmonics modulated) would be a better start than cutting the bit depth of an existing live recording. With clearly adjusted harmonics, I feel you'd have more to convey pronunciation. I'm not sure, but maybe it would be worth a try. Either way, excellent experiment!
Thanks! That's very generous of you, I wish that this project had come out a little better. You might be right about switching to square waves, but I'm not sure I'll ever have time to try!
Hm, did you run the sound through a low-pass filter before printing the waveforms? I would think that a lot of the higher frequences will just result in noise when converted to geometry.

In fact, if you assume a "read" speed of 26 inch/s and a resolution of 300dpi, that translates to 7800 dots/s, or 3900 grooves/s=3.9kHz. I.e. any frequency higher than 3.9kHz would be impossible to print with just 300dpi. (All that is assuming I remember some fragment of my signal processing course and that I managed to not screw up my numbers anywhere. A tall order, I know...)

Actually, if I were to venture a guess I would think that filtering it down further to just a few distinct frequences using a comb filter may yield even better results.
I think the DPI would represent the sample rate though. If 3.9 k is the sample rate (number of grooves), the higest audio frequency you will be able to reproduce is half that or 1.95 k.
(Okay, now I can't stop thinking about this so I better try to get some more of it out of my system)

My idea of using a comb filter may or may not be usable, but here's my thought process behind suggesting it anyway.

Since I wouldn't expect the size of the bumps of the printed strip to actually produce much difference in sound amplitude when dragging an edge over it (I would actually expect having sporadic larger bump would cause the edge to jump and skip over grooves, degrading the sound quality), the geometry would probably work better if it was more like how the musical roads works. That is, a waveform that is essentially digital (low or high) and the sound is produced by the distance between the grooves rather than the varying amplitude of a proper waveform.

My thinking was that by trimming it down to fewer frequencies, it would be result in a waveform with fewer "overlapping" grooves, leading to a cleaner separation between the grooves. But as I said, it was mostly a guess which may be way off. It might be enough to just remove all frequencies below some limit that is way lower than the theoretical maximum above.
I think this is on the right track, my next tests are going to be lower amp/lower bit depth. Even 1 bit audio can give some good results, this is a 1 bit david bowie edit:
http://www.mixcloud.com/amandaghassaei/1-bit-audio-edit/
I think the biggest issue is that I didn't print at 600dpi, I only printed at 300. The difference between 300 and 600 with the 3d printed records was huge
More Comments