Table of Contents

Dienstag 13. September 2016: Erstellen einer WAV-Datei

Digital Audio: Prinzipien

Schall: Schwankungen des Luftdrucks, erzeugt z.B. durch die Bewegung einer Lautsprechermembran, aufgenommen z.B. vom menschlichen Ohr.

Der Luftdruck (bzw. Auslenkung einer Membran) wird viele Male pro Sekunde gemessen (z.B. CD: 44100Hz). Diese Messpunkte (typischerweise vorzeichenbehaftete 16-Bit Ganzzahlen) werden gespeichert. Diese Messwerte werden Samples genannt. Die Umwandlung wird auch Sampling genannt.

Beim Abspielen, wird entsprechend dieser Werte eine Spannung erzeugt (und die Kurve zwischen Punkten geglättet).

WAV-Format

Das WAV-Format kann ziemlich komplex sein. Wir werden uns auf ein ganz einfaches Format beschränken:

Wir gehen von folgender Beschreibung des WAV-Formats aus: https://de.wikipedia.org/wiki/RIFF_WAVE#Beispiel_eines_allgemein_lesbaren_WAVE-PCM-Formats

Ruby Details

String manipulation

Die Daten zuerst in einem String geschrieben. Damit das effizient ist, wird ein String der korrekten Länge angelegt:

  filesize = ????
  wav = 0.chr*filesize  # String mit ASCII 0 gefüllt.

Der String kann dann direkt veränder werden, z.B. mit

  wav[8..11]="WAVE"  # Ersetze die Bytes 8 bis 11 mit den 4 Bytes der ASCII-Codes von "WAVE"

Little-Endian Kodierung

Da wir viele Werte Little-Endian kodieren werden müssen, wird eine Funktion definiert:

def to_le(wert, bytes=2)  # Wird kein zweiter Parameter angegeben, ist dieser automatisch 2
  res = 0.chr*bytes       # String vorbereiten
  bytes.times{|i|         # Bytes durchgehen
    res[i] = (wert & 0xff).chr    # Wert in 8-Bit-ASCII umwandeln (nicht zwingend ein Symbol!)
    wert >>=8             # Kurzform für:  wert = wert >> 8
  }
  return res              # Resultat der Funktion
end
#Zum Testen:
puts to_le(1819242339, 4)

Konstanten des Programms

Wir definieren zuerst die Eckdaten unserer Wav-Datei:

duration = 2
 
format = 0x0001      # Siehe Dokumentation
channels = 1         # Mono
rate = 44100         # 44100Hz Samplingrate
bytes_per_second = rate*2
blockalign = 2
bitssample = 16
 
filesize = ?????
 
wav = 0.chr*filesize
 
#
#  Hier die Header-Daten schreiben
#  Und dann Audio-Daten erzeugen
#

Sinus

Um eine Schwingung darzustellen, kann die Sinus-Funktion verwendet werden (andere periodische Funktionen sind aber auch möglich, z.B. Sägezahn, Dreieck oder Rechteck).

  sq32 = Math.sin(30/180.0*Math::PI)

Ausgabe in eine Datei

Die Datei wird in dem Verzeichnis erzeugt (bzw. überschrieben), wo das Programm gestartet wird.

File.open("output.wav","w"){|o|
  o.write(wav)
} 

Lösungsvorschlag

Code anzeigen

Code anzeigen

wavgenerator.rb
# to_le wandelt einen Zahlwert (wert) in ein Folge von einer gegbenen (bytes) Bytes um.
# Z.B. wird mit to_le(0x1020,4) die Folge 0x20,0x10,0x00,0x00 von 4 Bytes erzeugt.
 
def to_le(wert, bytes=2)  # Wird kein zweiter Parameter angegeben, ist dieser automatisch 2
  res = 0.chr*bytes       # String vorbereiten
  bytes.times{|i|         # Bytes durchgehen
    res[i] = (wert & 0xff).chr    # Wert in 8-Bit-ASCII umwandeln (nicht zwingend ein Symbol!)
    wert >>=8             # Kurzform für:  wert = wert >> 8
  }
  return res              # Resultat der Funktion
end
 
duration = 2
 
format = 0x0001      # Siehe Dokumentation
channels = 1         # Mono
rate = 44100         # 44100Hz Samplingrate
bytes_per_second = rate*2
blockalign = 2
bitssample = 16
 
filesize = 44 + duration*bytes_per_second
 
wav = 0.chr*filesize                                                                                                                                                                                              
 
wav[0..3] = "RIFF"                                                                                                                                                                                                
wav[4..7] = to_le(filesize-8, 4)                                                                                                                                                                                  
wav[8..11]="WAVE"                                                                                                                                                                                                 
 
wav[12..15] = "fmt "                                                                                                                                                                                              
wav[16..19] = to_le(16,4)                                                                                                                                                                                         
wav[20..21] = to_le(format,2)                                                                                                                                                                                     
wav[22..23] = to_le(channels,2)                                                                                                                                                                                   
wav[24..27] = to_le(rate,4)                                                                                                                                                                                       
wav[28..31] = to_le(bytes_per_second,4)                                                                                                                                                                           
wav[32..33] = to_le(blockalign,2)                                                                                                                                                                                 
wav[34..35] = to_le(bitssample,2)                                                                                                                                                                                 
 
wav[36..39] = "data"                                                                                                                                                                                              
wav[40..43] = to_le(filesize-44,4)                                                                                                                                                                                
 
offset = 44                                                                                                                                                                                                       
 
# Samples durchzählen
# s ist die Sample Nummer
for s in 0...(duration*rate)
   # Entsprechende Zeit in Sekunden
   t = s.to_f/rate
   # Wert zum Zeitpunkt t
   v = Math.sin(t*440*2*Math::PI)*30000
   # Wert in die "WAV-Datei" schreiben
   wav[(s*2+offset)..(s*2+offset+1)] = to_le(v.to_i,2)
end
 
# Wav-Datei effektiv abspeichern
File.open("output.wav","w"){|o|
  o.write(wav)
}

Weitere Aufgaben

Melodien codieren

Z.B. Alle meine Entchen ist dann

"2cdef4gg2aaaa8g2aaaa8g2ffff4ee2dddd8c"

Oder dieses hier:

"2ebeef4ea2ebeef4eba2ebeefe+d-h#gfed8c2a#ggf4a+d2-agfe4a+c2c-hah#d#faa+c-ha#ge#gh+e-2ebeef4ea2ebeef4eba2ebeefe+d-h#gfed8c2a#ggf4a+d2-agfe4a+c2c-hahe#gh+edc-h8a"

Schreiben Sie ein Programm, das die Melodie zum String erzeugt.

Anzeigen

Anzeigen

synthi.rb
def get_frequency(ton, pm, oktave)
    tabelle = {'a'=>0,
               'h'=>2,
               'c'=>-9,
               'd'=>-7,
               'e'=>-5,
               'f'=>-4,
               "g"=>-2}
 
    return 0.0 if ton=='p'
 
    halbton = tabelle[ton]+pm+12*oktave
    lambda = 2.0**(1.0/12.0)
    return 440.0*(lambda**halbton);
end
 
# Schwingungsfunktion zum Zeitpunkt t mit Frequenz freq
# Werte zwischen -1 und 1
def get_function(freq, t) 
   return Math.sin(freq*2*t*Math::PI)*(0.5**(t/0.1)); 
end
 
# Liefert ein Array mit Ganzzahlen zurück
# (zwischen min -32000 und max 32000)
def get_samples(freq, dauer, rate=44100)
    anzahl = (dauer*rate).to_i
    return Array.new(anzahl) {|i|
       (get_function(freq, i.to_f/rate)*20000).to_i                
    }
end
 
# to_le wandelt einen Zahlwert (wert) in ein Folge von einer gegbenen (bytes) Bytes um.
# Z.B. wird mit to_le(0x1020,4) die Folge 0x20,0x10,0x00,0x00 von 4 Bytes erzeugt.
 
def to_le(wert, bytes=2)  # Wird kein zweiter Parameter angegeben, ist dieser automatisch 2
  res = 0.chr*bytes       # String vorbereiten
  bytes.times{|i|         # Bytes durchgehen
    res[i] = (wert & 0xff).chr    # Wert in 8-Bit-ASCII umwandeln (nicht zwingend ein Symbol!)
    wert >>=8             # Kurzform für:  wert = wert >> 8
  }
  return res              # Resultat der Funktion
end
 
# lied: String
# Ausgabe: Array mit Ganzzahlen
def play(lied, dauer=0.2, rate=44100)
   achtel = 2
   pm = 0
   oktave = 0
   samples = []
   lied.each_char{|c|
        case c
        when 'b'
            pm=-1
        when '#'
            pm=1
        when '+'
            oktave+=1
        when '-'
            oktave-=1
        when '1'..'9'
            achtel = c.to_i
        else
            freq = get_frequency(c, pm, oktave);
            samples+=get_samples(freq, achtel*dauer, rate);
        end
   }
   return samples
end
 
 
format = 0x0001      # Siehe Dokumentation
channels = 1         # Mono
rate = 44100         # 44100Hz Samplingrate
bytes_per_second = rate*2
blockalign = 2
bitssample = 16
 
lied = "2cdef4gg2aaaa8g2aaaa8g2ffff4ee2dddd8c"
samples = play(lied,0.1)
 
# Uwandeln, zusammenfügen als String
samples = samples.map{|s| to_le(s,2)}.join("")
 
 
filesize = 44 + samples.size
 
wav = 0.chr*44                                                                                                                                                                                              
 
wav[0..3] = "RIFF"                                                                                                                                                                                                
wav[4..7] = to_le(filesize-8, 4)                                                                                                                                                                                  
wav[8..11]="WAVE"                                                                                                                                                                                                 
 
wav[12..15] = "fmt "                                                                                                                                                                                              
wav[16..19] = to_le(16,4)                                                                                                                                                                                         
wav[20..21] = to_le(format,2)                                                                                                                                                                                     
wav[22..23] = to_le(channels,2)                                                                                                                                                                                   
wav[24..27] = to_le(rate,4)                                                                                                                                                                                       
wav[28..31] = to_le(bytes_per_second,4)                                                                                                                                                                           
wav[32..33] = to_le(blockalign,2)                                                                                                                                                                                 
wav[34..35] = to_le(bitssample,2)                                                                                                                                                                                 
 
wav[36..39] = "data"                                                                                                                                                                                              
wav[40..43] = to_le(filesize-44,4)                                                                                                                                                                                
 
 
 
# Wav-Datei effektiv abspeichern
File.open("output.wav","w"){|o|
  o.write(wav) # Header
  o.write(samples)  # PCM-Daten
}