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).
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
- 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
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.
- 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
}