Umwandeln von WAV-Dateien in 8KHz Files

Code-Stücke können hier veröffentlicht werden.
Antworten
snakeseven
User
Beiträge: 408
Registriert: Freitag 7. Oktober 2005, 14:37
Wohnort: Berlin
Kontaktdaten:

Hallo,
manchmal braucht man das, z.B. für Internettelefonie. Das folgende Script wandelt einen Ordner mit 44k WAV-Dateien in 8k WAV-Dateien und speichert sie in einem neuen Ordner ab.

Code: Alles auswählen

import audioop,struct,os
    
wavdir44 = 'Pfad Quell-Files 44k'
wavdir8 = 'Pfad Destination-Files 8k'

def DownSample(wave):
    newrate = audioop.ratecv(wave[44:],2,1,44100,8000,None,1,1)[0]     #Audiodaten beginnen ab Position 44. Bis 44 Header
    newlength = len(newrate)
    newheader = wave[:24] + struct.pack('l',8000)+ wave[28:40] + struct.pack('l',newlength)   #Samplerate  24-28, size Audiodaten 40-44
    result = newheader + newrate
    return result

allewavs = os.listdir(wavdir44)
for name in allewavs:
    if name[-4:] == '.WAV' or name[-4:] == '.wav':
        sound1=open(wavdir44 + name, "rb").read()
        sound2 = DownSample(sound1)
        open(wavdir8 + name, "wb").write(sound2) 
        print wavdir8 + name     #Kontrollausgabe
Muss noch optimiert werden. So sollte für eine Reduzierung des Aliasing bei 4Khz noch ein Low-Pass Filter vorgeschaltet werden.
Wird noch kommen. Das hier tuts aber auch schon ganz gut.

Gruss, Seven

Edit: Habe die optionalen Parameter weightA und weightB noch dazu genommen (1,1). Laut der Dokumentation Parameter für einen einfachen digitalen Filter (was für einer steht leider nicht dabei). Das Ergebnis ist aber schon deutlich besser, als ohne Filter (offenbar ein Low-Pass).
snakeseven
User
Beiträge: 408
Registriert: Freitag 7. Oktober 2005, 14:37
Wohnort: Berlin
Kontaktdaten:

So, hier noch der fehlende Low-Pass Filter. Ist auf 4KHz Eckfrequenz eingestellt und sollte der Downsampling-Routine auf jeden Fall vorgeschaltet werden. Dann allerdings ohne die optionalen Parameter weightA und weightB, also:

Code: Alles auswählen

newrate = audioop.ratecv(wave[44:],2,1,44100,8000,None)[0]
Ist zwar nicht der Schnellste, klingt aber dafür gut.
Wer andere Filterparameter will, kann sie sich hier berechnen lassen.
http://www-users.cs.york.ac.uk/~fisher/ ... /trad.html

Code: Alles auswählen

def LP_filter(wave):
    import struct,psyco
    #filtertype  =  Chebyshev  
    #passtype  =  Lowpass  
    #ripple  =  -6  
    #order  =  4  
    #samplerate  =  44100  
    #corner1  =  4000  
   
    psyco.log()       # psyco bringt eine deutlich höhere (!) Performance
    psyco.profile()  # http://psyco.sourceforge.net/

    rawdfiltered = []
    lenraw = len(wave[44:])/2
    rawd = struct.unpack(str(lenraw)+'h',wave[44:])   
    xv = [0,0,0,0,0]
    yv = [0,0,0,0,0]
    
    for sample in rawd:
        del xv[0]; del yv[0]
        xv.append(sample / 1.132343779e+03)
        yv.append((xv[0] + xv[4]) + 4 * (xv[1] + xv[3]) \
        + 6 * xv[2] + (-0.8143381742 * yv[0]) + (3.1524314324 * yv[1]) \
        + (-4.8358600552 * yv[2]) + (3.4836368126 * yv[3]))
        
        if yv[4] < -32767: rawdfiltered.append(32767)                  #Falls Clippen, begrenzen auf gültigen Wertebereich 
        elif yv[4] > 32766: rawdfiltered.append(32766)
        else: rawdfiltered.append(yv[4])
        
    result = wave[:44] + struct.pack(str(lenraw)+'h',*rawdfiltered)
    return result
Fröhliches Abtastraten-Konvertieren !
Seven
BlackJack

Das lässt sich in Python+Psyco noch ein klein wenig steigern bzw. mit Pyrex erheblich beschleunigen.

Deine Funktion auf eine ca. 40 sekündige WAV Datei angesetzt gab bei mir folgende Zeiten:

23.829282999 (Python)
7.06526303291 (Python+Psyco)

Deine Funktion benötigt aber ziemlich viel Speicher. Zum einen weil unnötige Kopien angelegt werden und weil die Daten in `rawd` und im Ergebnis zusätzlich in ein Tupel und eine Liste mit `int` bzw. `float` Objekten kopiert wird. Da werden pro Sample nicht 2-Byte sondern wesentlich mehr belegt. Das sind 12 Bytes für jedes `int` und 16 Bytes für jedes `float` plus dem Zeiger in der Liste auf das jeweilige Objekt. Aus meiner 4MB Testdatei werden da im Speicher mindestens 75MB.

Und bei der Berechnung von `lenraw` wird der gesamte Sampleteil der Daten einmal unnötig kopiert. Besser wäre es von der Länge der Wave-Daten 44 abzuziehen und das dann durch 2 zu teilen.

Man kann eine Menge Speicher sparen, wenn man die Samples einzeln umwandelt. Dazu ist das Modul `array` besser geeignet als `struct`.

Code: Alles auswählen

from __future__ import division
from array import array

def lp_filter2(wave):
    result = array('h')
    result.fromstring(wave[:44])
    
    rawd = array('h')
    rawd.fromstring(wave)
    del rawd[:22]
    
    xv = [0,0,0,0,0]
    yv = [0,0,0,0,0]
    
    for sample in rawd:
        del xv[0]
        del yv[0]
        xv.append(sample / 1.132343779e+03)
        yv.append(  xv[0]
                  + xv[4]
                  + 4 * (xv[1] + xv[3])
                  + 6 * xv[2]
                  + -0.8143381742 * yv[0]
                  +  3.1524314324 * yv[1]
                  + -4.8358600552 * yv[2]
                  +  3.4836368126 * yv[3])
        
        tmp = int(yv[4])
        if tmp < -32767:
            result.append(-32767)
        elif tmp > 32766:
            result.append(32766)
        else:
            result.append(tmp)

    return result
Der Speicherverbrauch ist wesentlich niedriger und schwankt nicht so stark wie bei der ursprünglichen Version. Ohne Psyco läuft es leider ein ganz klein wenig langsamer, mit Psyco aber etwas schneller als das Orignal:

24.1125090122 (Python)
5.48418307304 (Python+Psyco)

Die Rechnung finde ich so übrigens etwas übersichtlicher formatiert. Die ``\`` zum Fortsetzen von einer Zeile braucht man nicht solange noch Klammern geöffnet sind.

Erheblich schneller wird es wenn man Pyrex benutzt:

Code: Alles auswählen

from array import array

def lp_filter3(wave):
    cdef signed int i, j, size, tmp
    cdef signed short *data, sample
    cdef double xv[5], yv[5]
    
    wave_array = array('h')
    wave_array.fromstring(wave)
    
    for i from 0 <= i < 5:
        xv[i] = yv[i] = 0
    
    info = wave_array.buffer_info()
    tmp = info[0]
    data = <signed short*> tmp
    size = info[1]
    
    for i from 22 <= i < size:
        sample = data[i]
        
        for j from 0 <= j < 4:
            xv[j] = xv[j + 1]
            yv[j] = yv[j + 1]
        
        xv[4] = sample / 1.132343779e+03
        yv[4] = (  xv[0]
                 + xv[4]
                 + 4 * (xv[1] + xv[3])
                 + 6 * xv[2]
                 + -0.8143381742 * yv[0]
                 +  3.1524314324 * yv[1]
                 + -4.8358600552 * yv[2]
                 +  3.4836368126 * yv[3])

        if yv[4] < -32767:
            sample = -32767
        elif yv[4] > 32766:
            sample = 32766
        else:
            sample = <signed int> yv[4]
        
        data[i] = sample
    
    return wave_array
Damit kommt man unter eine Sekunde und braucht noch weniger Speicher weil hier nur ein `array` Objekt "in place" verändert wird.

0.28432393074 (Pyrex)
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

...alten thread rauskram...

Genau das möchte ich auch machen. 44.1KHz wave dateien runter samplen... Allerdings möchte ich dabei nicht die WAV Datei direkt öffnen, sondern besser über das WAVE modul gehen. Doch das will nicht:

Code: Alles auswählen

#!/usr/bin/env python2

import wave
import audioop
import struct


WAVE_RESAMPLE = 8000 # Downsample wave file to this sample rate

WAVE_READ_SIZE = 16 * 1024 # How many frames should be read from WAVE file at once?
WAV_UNPACK_STR = {
    1: "<%db", #  8-bit wave file
    2: "<%dh", # 16-bit wave file
    4: "<%dl", # 32-bit wave file
}

def iter_wave_values(wavefile):
    nchannels = wavefile.getnchannels() # typically 1 for mono, 2 for stereo
    samplewidth = wavefile.getsampwidth() # 1 for 8-bit, 2 for 16-bit, 4 for 32-bit samples

    struct_unpack_str = WAV_UNPACK_STR[samplewidth]

    framerate = wavefile.getframerate() # samples / second

    outrate = None
    if framerate > WAVE_RESAMPLE:
        outrate = WAVE_RESAMPLE
        print "resample from %iHz/sec. to %sHz/sec" % (framerate, outrate)
        framerate = WAVE_RESAMPLE

    ratecv_state = None
    while True:
        frames = wavefile.readframes(WAVE_READ_SIZE)
        if not frames:
            break

        if outrate is not None:
            # downsample the wave file
            print "before: %sFrames %sHz/sec." % (len(frames), outrate)
            frames, ratecv_state = audioop.ratecv(
                frames, samplewidth, nchannels, framerate, outrate, ratecv_state,
            )
            print "after: %sFrames %sHz/sec." % (len(frames), outrate)

        frame_count = len(frames) / samplewidth
        frames = struct.unpack(struct_unpack_str % frame_count, frames)
        for frame in frames:
            yield frame


if __name__ == "__main__":
    FILENAME = "HelloWorld1 origin.wav"
    wavefile = wave.open(FILENAME, "rb")

    max = 20
    for frame_no, value in enumerate(iter_wave_values(wavefile)):
        if frame_no > max:
            break
        print value
Ausgaben sind z.B.:

Code: Alles auswählen

resample from 44100Hz/sec. to 8000Hz/sec
before: 32768Frames 8000Hz/sec.
after: 32768Frames 8000Hz/sec.
956
695
702
974
676
750
1072
1775
3057
4815
6643
8630
10309
10973
11405
10632
9054
7612
5257
2741
1054
Dabei hätte ich erwartet, das bei "after:" weniger Frames stehen würden. Ist aber die selbe Anzahl. Somit macht audioop.ratecv irgendwie garnichts?!?!

Wenn ich WAVE_RESAMPLE auf andere Werte stelle, sind die Ausgaben die selben.

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Antworten