{{backlinks>.}}
===== 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:
* Mono (1 Kanal)
* 44100Hz Samplingrate
* unkomprimiert
* 16-Bit signed ints, little-endian
* Erzeugen von 2 Sekunden 440Hz-Ton (Kammer-a)
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 ====
# 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 ====
* Experimentieren Sie mit anderen Funktionen. Fügen Sie z.B. Tremolo oder Vibrato dazu. Erzeugen Sie Rauschen.
* Erzeugen Sie zwei oder mehr Töne gleichzeitig (Funktionen einfach addieren, Aufgepasst auf Überläufe!).
=== Melodien codieren ===
* cdefgah stehen für die jeweiligen Töne. # bzw. b macht den folgenen Ton einen halben höher bzw. tiefer.
* + - wechselt eine Octave höher oder tiefer
* p steht für Pause
* 12345678 steht für die Länge in Achteln der folgenden Töne
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.
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
}