Page 1 of 1

Image to sound conversion, almost there...?

Posted: Sat Oct 19, 2013 2:48 pm
by Joakim Christiansen
First check out this program, they have a good example of this:
A nice video showing the effect: http://www.youtube.com/watch?v=W8MCAXhEsy4
Their website: http://photosounder.com/

I was like "hey, this can't be so hard..." and I've gotten some progress but I thought... why not just share with you guys to see if you have any good ideas(code)? Because I'm a bit stuck at the moment, I've mostly been testing all kinds of different stuff (hence the messy code) and now I'm open for ideas :)

This is the picture you will need to use with my test code (save it as test.png):
http://i.imgur.com/NOHRuGb.png

When the code is executed it plays a PCM wave file it created and it also stores this as "test.wav" in the same directory. This is so it can easily be analyzed with Audacity, it's the perfect tool for debugging the output! It can also refresh it's data automatically when the new file is generated!

My code is not cleaned up, there is a lot of variables and stuff that could be removed, but which for now is kept for easy testing. Feel free to try it out and post improvements! But turn the volume down before you run it!

Explanation of code:
The X of the image equals time while Y equals the frequency.
In my code the lower frequencies are near the top and the highest are near the bottom, the frequency range can be modified with these variables "frequencyFrom" and "frequencyTo".
You can also modify how many milliseconds the image is supposed to play with "wantedWidthInMilliseconds".

Since there are 44100 samples per second it is obvious that each pixel has to consist of several samples, that is why "For i=0 To samplesPerPixel-1" is done for every pixel. And the result of all the samples per X unit are averaged to produce the final "wave position" used to create the wave mixed of the different frequencies.

My first idea was kinda that I had a wave for each Y pixel oscillating at its own frequency and then just add all the used waves together to produce the final output wave. But this doesn't really work correctly, when mixing them their different "sine offset" will easily fuck things up, a more intelligent processing has to be done. That's when I eventually tried to calculate the latest "sine offset" using this weird procedure "getWaveAngle(pSample,sample,amplitude.f)" which by analyzing the two latest samples and the average amplitude of all the frequencies used to make those it tries to determine the offset in "angle". This was tested in the "drawWave()" procedure and seemed to work. But it still produces some noise in my example and my code doesn't seem to handle more advanced images correctly at all.

Code: Select all

EnableExplicit: Define.l

UseJPEGImageDecoder()
UsePNGImageDecoder()

#ToRad = 0.017453 

Global format.WAVEFORMATEX

Procedure setWaveFormat(bitsPerSample=16,samplesPerSec=44100)
  format\wFormatTag = #WAVE_FORMAT_PCM
  format\nChannels = 1 ;mono
  format\wBitsPerSample = bitsPerSample ;8/16
  format\nSamplesPerSec =  samplesPerSec ;8000 Hz, 11025 Hz, 22050 Hz, and 44100 Hz
  format\nBlockAlign = (format\nChannels * format\wBitsPerSample) / 8 ;equal to the product of nChannels and wBitsPerSample divided by 8 (bits per byte).
  format\nAvgBytesPerSec = format\nSamplesPerSec * format\nBlockAlign ;equal to the product of nSamplesPerSec and nBlockAlign
EndProcedure
Procedure addWaveHeader(*address,dataSize,channels,samplesPerSec,blockAlign,bitsPerSample)
  ; RIFF Chunk
  PokeL(*address+ 0, 'FFIR')
  PokeL(*address+ 4, dataSize + 36)
  PokeL(*address+ 8, 'EVAW')
  ; FORMAT Chunk
  PokeL(*address+ 12, ' tmf')
  PokeL(*address+ 16, $0010)
  PokeW(*address+ 20, $01)
  PokeW(*address+ 22, channels)
  PokeL(*address+ 24, samplesPerSec)
  PokeL(*address+ 28, samplesPerSec * channels * (bitsPerSample / 8) )
  PokeW(*address+ 32, blockAlign)
  PokeW(*address+ 34, bitsPerSample)
  ; DATA Chunk
  PokeL(*address+ 36, 'atad')
  PokeL(*address+ 40, dataSize)
EndProcedure
Procedure.l sineWave(position,amplitude,frequency) 
  Protected result, angle.f
  angle = (360 / format\nSamplesPerSec) * position * frequency
  result = Sin(Radian(angle))*amplitude
  ProcedureReturn result
EndProcedure
Procedure.l sineWave2(NA,amplitude,frequency) ;correct if called format\nSamplesPerSec per second
  Protected result, multiplier.f
  Static angle.f
  multiplier = (360 / format\nSamplesPerSec)  * frequency
  angle + multiplier
  If angle > 360.0
    angle - 360.0
  EndIf
  result = Sin(Radian(angle))*amplitude
  ProcedureReturn result
EndProcedure
Procedure.f getWaveAngle(pSample,sample,amplitude.f) ;possible buggy, made in a hurry to try it
  Protected waveAngle.f; = 0
  If pSample > sample
    waveAngle = (-90 * (sample / amplitude)) + 180
  Else
    waveAngle = ( 90 * (sample / amplitude)) + 360
    If waveAngle > 360
      waveAngle - 360
    EndIf
  EndIf
  ProcedureReturn waveAngle
EndProcedure
Procedure.l test()
  Protected *memory, id, size, duration
  duration = 1000
  
  size = Int((format\nSamplesPerSec/1000)*duration) * (format\wBitsPerSample / 8)
  *memory = AllocateMemory(44+size)
  
  Protected x,y
  For x=0 To format\nSamplesPerSec-1 ;1 second
    y = sineWave(x,30000,450)
    PokeW(*memory+44+(x*2),y)
  Next
  
  addWaveHeader(*memory,size,format\nChannels,format\nSamplesPerSec,format\nBlockAlign,format\wBitsPerSample)
  id = CatchSound(#PB_Any,*memory)
  FreeMemory(*memory)
  ProcedureReturn id
EndProcedure
Procedure.l generateFromImage(image$)
  Protected height,width,x,y,color, frequency, fMultiplier.f, amplitude
  Protected frequencyFrom,frequencyTo
  Protected totalSamples, samplesPerPixel, wantedWidthInMilliseconds = 4000, actualWidthInMilliseconds
  Protected image, i, sample, differentFrequencies, mSize, mPos, *memory
  Protected result, angleMultiplier.f, waveAngle.f, avgSample, lastWaveAngle.f, avgAmplitude.f, pSampleWritten
  frequencyFrom = 60
  frequencyTo = 5000 ;17000 ;http://www.youtube.com/watch?v=h5l4Rt4Ol7M
  
  image = LoadImage(#PB_Any,image$)
  If image
    width = ImageWidth(image)
    height = ImageHeight(image)
    ;Debug height
    fMultiplier = (frequencyTo-frequencyFrom) / height
    
    samplesPerPixel = Round((format\nSamplesPerSec*(wantedWidthInMilliseconds*0.001))/width,#PB_Round_Up)
    totalSamples = samplesPerPixel * width
    actualWidthInMilliseconds = Int((totalSamples / format\nSamplesPerSec) * 1000)
    
    mSize = (totalSamples * (format\wBitsPerSample / 8)) + 44
    *memory = AllocateMemory(mSize)
    mPos = 44
    
    Dim sampleTotal(samplesPerPixel-1)
    Dim angleMultiplier.f(height-1)
    Dim waveAngle.f(height-1)
    Dim waveAngleTotal.f(samplesPerPixel-1)
    Dim amplitudeTotal.f(samplesPerPixel-1)
    
    For y=0 To height-1
      frequency = (y * fMultiplier) + frequencyFrom
      angleMultiplier(y) = (360 / format\nSamplesPerSec) * frequency
    Next
    
    StartDrawing(ImageOutput(image))
    For x=0 To width-1
      For y=0 To height-1
        color = Point(x,y)
        If color <> #Black
          differentFrequencies + 1
          amplitude = Int(Red(color)*(32767/255))
          waveAngle(y) = getWaveAngle(pSampleWritten,avgSample,avgAmplitude);lastWaveAngle ;last average one
          For i=0 To samplesPerPixel-1
            waveAngle(y) + angleMultiplier(y)
            If waveAngle(y) > 360.0
              waveAngle(y) - 360.0
            EndIf
            
            sample = Sin(Radian(waveAngle(y)))*amplitude
            sampleTotal(i) + sample ;all y pixels adds their samples so an average can be calculated
            waveAngleTotal(i) + waveAngle(y)
            amplitudeTotal(i) + amplitude
          Next
        Else ;still change their wave pos
          If 0 ;0 means turned off for now
            ;waveAngle(y) = lastWaveAngle ;last average one
            For i=0 To samplesPerPixel-1
              waveAngle(y) + angleMultiplier(y)
              If waveAngle(y) > 360.0
                waveAngle(y) - 360.0
              EndIf
            Next
          EndIf
        EndIf
      Next
      
      For i=0 To samplesPerPixel-1
        pSampleWritten = avgSample
        avgSample = Int(sampleTotal(i)/differentFrequencies)
        waveAngle = waveAngleTotal(i)/differentFrequencies
        avgAmplitude = amplitudeTotal(i)/differentFrequencies
        
        lastWaveAngle = waveAngle
        sampleTotal(i) = 0 ;reset
        waveAngleTotal(i) = 0 ;reset
        amplitudeTotal(i) = 0
        PokeW(*memory+mPos,avgSample)
        mPos + 2
      Next
      differentFrequencies = 0
      
      
    Next
    StopDrawing()
    
    addWaveHeader(*memory,mSize-44,format\nChannels,format\nSamplesPerSec,format\nBlockAlign,format\wBitsPerSample)
    If CreateFile(0,"test.wav")
      WriteData(0,*memory,mSize)
      CloseFile(0)
    EndIf
    result = CatchSound(#PB_Any,*memory)
    FreeMemory(*memory)
  Else
    Debug "error"
  EndIf
  
  ProcedureReturn result
EndProcedure
Procedure drawWave()
  Protected window, imageGadet, i, signal, image
  window = OpenWindow(#PB_Any,0,0,1200,400,"",#PB_Window_ScreenCentered|
                                              #PB_Window_SystemMenu|
                                              #PB_Window_TitleBar)
  imageGadet = ImageGadget(#PB_Any,0,0,1200,400,0)
  image = CreateImage(#PB_Any,1200,400,24,#Black)
  StartDrawing(ImageOutput(image))
  Line(0,100,1200,1,#Gray)
  Line(0,200,1200,1,#Gray)
  Line(0,300,1200,1,#Gray)
  Protected waveAngle.f, angleMultiplier.f, frequency, counter, pSignal
  For i=0 To 1200-1
    frequency = 100
    angleMultiplier = (360 / format\nSamplesPerSec) * frequency
      
    waveAngle + angleMultiplier
    If waveAngle > 360.0
      waveAngle - 360.0
    EndIf
    
    signal = Sin(Radian(waveAngle))*100
    
    counter+1
    If counter = 20
      counter = 0
      DrawText(i,signal+200,Str(getWaveAngle(pSignal,signal,100)),#Green) ;debugging the predicted angle
    EndIf
    pSignal = signal
  
    ;signal = sineWave(i,100,50)
    Plot(i,signal+200,#Green)
    ;signal = sineWave2(i,100,50)
    ;Plot(i,signal+200,#Red)
  Next
  StopDrawing()
  SetGadgetState(imageGadet,ImageID(image))
  
  Repeat
  
  Until WaitWindowEvent() = #PB_Event_CloseWindow

EndProcedure

InitSound()
setWaveFormat(16,44100)

Define sound

Select 0
  Case 0
    sound = generateFromImage("test.png")
    PlaySound(sound)
    Delay(4200)
  Case 1
    drawWave()
  Case 2
    sound = test()
    PlaySound(sound)
    Delay(2000)
EndSelect

Re: Image to sound conversion, almost there...?

Posted: Wed Oct 23, 2013 9:20 pm
by doctornash
paint2sound source:
http://www.purebasic.fr/english/viewtop ... 14&t=49641
paint2sound video:
http://www.youtube.com/watch?v=GqAPurzHLXU
other wacky PB apps featuring various audio DSP techniques you may find useful (source for all is available):
http://www.flexibeatz.weebly.com

Re: Image to sound conversion, almost there...?

Posted: Thu Oct 24, 2013 1:43 pm
by Joakim Christiansen
Thanks, I will study and learn! :D
Great website, I have actually seen it before since you have much knowledge on the area.