{{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 }