Tutorial: Messungen mit der Soundkarte – Teil II: Amplitude, Frequenz und Phase

Soundkarten-Messung II:
Amplitude, Frequenz und Phase

Druckfassung

von Michael Gaedtke

Letzte Änderungen vom 10. August 2006

1. Vorbereitungen

2. Spitzen- und Effektivwerte der Signalamplitude

3. Frequenzmessung

4. Messung der Phasendifferenz – XOR-Methode

5. Phasenmessung mit flankengesteuerten FlipFlops

6. Offsetspannung kompensieren

7. Aufruf der Auswertefunktionen

8. Downloads

Soundkarten verfügen in aller Regel über Wechselspannungs-Eingänge. Gleichspannungen werden mit einer Kondensator-Kopplung vom Eingang abgetrennt. Deshalb ist es auch nicht möglich, ohne Hilfsmittel mit der Soundkarte Gleichspannungen, -ströme und Gleichstromwiderstände zu messen. Dazu müsste die Gleichspannung zum Beispiel zunächst in eine proportionale Frequenz gewandelt werden. Das setzt eine externe Hardware voraus. In diesem Tutorial soll zunächst gezeigt werden. wie man verschiedene Amplitudenwerte, die Frequenz und die Phasenlage von Wechselspannungen messen kann.

Eine wichtige Warnung vorab: Die Eingangsschaltungen üblicher Soundkarten verkraften nur sehr geringe Wechselspannungen in der Größenordnung von einem Volt. Die vorgestellten Programme sind deshalb ausschließlich dazu geeignet, Experimente bei diesen niedrigen Spannungspegeln zu machen. Wer höhere Spannungen messen will, muss dazu einen aktiven oder passiven Spannungsteiler vor den Eingang seiner Soundkarte schalten, damit nichts kaputt geht. Er tut dies ausdrücklich auf eigene Gefahr. Der Autor kann für Experimente keinerlei Garantie übernehmen!

Der Einsatz zum Beispiel eines Vorschaltverstärkers mit Spannungsteiler und Schutzeinrichtungen empfiehlt sich in vielen Fällen auch wegen des ziemlich niedrigen Eingangswiderstands mancher Soundkarten. Er liegt manchmal nur in der Größenordnung von 10 kOhm, so dass zum Beispiel hochohmige Verstärker- und Filterschaltungen nur schlecht gemessen werden können, ohne dass der Eingang der Soundkarte das Messergebnis maßgeblich beeinflusst.

Die verschiedenen Messroutinen aus diesem Tutorial habe ich in einem kleinen Beispielprogramm zusammengefasst, dem auch die nachfolgenden Quelltexte entnommen sind. Das Programm demonstriert, wie die Messroutinen eingesetzt werden und mit der BASS.DLL zusammen arbeiten. Das Programm und die in Delphi 6 verfassten Quelltexte stehen am Ende dieses Tutorials zum Download bereit.

Top

1. Vorbereitungen

Im ersten Teil dieses Tutorials wurde gezeigt, wie die BASS-Routinen in eigene Programme eingebunden werden. Im Interface-Teil des Programms nimmt man dazu die BASS-Unit in die uses-Klausel auf:

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics,
  Controls, Forms, Dialogs, StdCtrls, ComCtrls,

  mgMath,
  BASS;

Außerdem müssen eine Reihe von Konstanten und Typen deklariert werden, um in übersichtlicher Form auf die gespeicherten Messwerte zugreifen zu können. Da die meisten Messwerte zweikanalig für linken und rechten Kanal anfallen, habe ich dafür den Record-Typ TMeasValue definiert:

const
  Le = 0; // Left Channel
  Ri = 1; // Right Channel

type
  TAudioSample = SmallInt;   // 16 Bit mit Vorzeichen
  TStereoSample = array[Le..Ri] of TAudioSample;
  TStereoBuffer = array[0..44100] of TStereoSample;

  TMeasValue    = record
                    LeftChannel : extended;
                    RightChannel : extended;
                  end; // record

Die wichtigsten Parameter des ausgewerteten Signalabschnitts werden darauf aufbauend in einem TWaveRecord zusammengefasst:

  TWaveRecord = record
                  BufferBlocks : word;
                  StereoBuffer : TStereoBuffer;
                  Averaging : boolean;  // Messungen mitteln?
                  CompOffset : boolean; // Offset kompensieren?
                  Frequency : TMeasValue;
                  Phase : extended;
                  RMSLevel : TMeasValue;
                  dBRMSLevel : TMeasValue;
                  PeakLevel : TMeasValue;
                  dBPeakLevel : TMeasValue;
                end;

var
  Form1: TForm1;
  RecChannel: HRecord;
  WaveRecord : TWaveRecord;

In der Variablen BufferBlocks wird gespeichert, wie viele Samples im StereoBuffer tatsächlich belegt sind. Diese Variable wird jeweils bei der Füllung des StereoBuffers in der Callback-Routine gesetzt. Um kontrollierte Anfangsbedingungen für alle Variablen zu haben, werden die Parameter dieses Records im Implementationsteil des Programms mit Default-Werten initialisiert:

procedure TForm1.InitWaveRecord (var WR : TWaveRecord);
  begin
    with WR do begin
      BufferBlocks := 0;
      Averaging := true;
      CompOffset := true;
      Frequency.LeftChannel := 0.0;
      Frequency.RightChannel := 0.0;
      Phase := 0.0;
      RMSLevel.LeftChannel := 0.0;
      RMSLevel.RightChannel := 0.0;
      dBRMSLevel.LeftChannel := 0.0;
      dBRMSLevel.RightChannel := 0.0;
      PeakLevel.LeftChannel := 0.0;
      PeakLevel.RightChannel := 0.0;
      dBPeakLevel.RightChannel := 0.0;
      dBPeakLevel.RightChannel := 0.0;
    end; // with
  end; // procedure InitWaveRecord;

Mit dem booleschen Parameter Averaging wird gesteuert, ob bei der Auswertung ein gleitender Mittelwert für die Ergebnisse errechnet werden soll. CompOffset legt fest, ob ein eventueller Gleichspannungsanteil im Eingangssignal der Soundkarte kompensiert werden soll. Beide Schalter werden defaultmäßig true gesetzt. Die Prozedur InitWaveRecord wird in der Create-Prozedur des Programms für den globalen WaveRecord aufgerufen:

procedure TForm1.FormCreate(Sender: TObject);
  ...
 
begin
    ...
    // Weitere Initialisierungen:
    InitWaveRecord(WaveRecord);
    ...
  end; // procedure FormCreate

Top

2. Spitzen- und Effektivwerte der Signalamplitude

Nach diesen Vorbereitungen folgen im Quelltext die eigentlichen Auswerte-Funktionen für die Ermittlung verschiedener Pegelwerte der aufgenommenen Sample-Werte, für die Ermittlung der Frequenz des aufgezeichneten Signals (sofern sich ein periodisches Signal erkennen lässt) und des Phasenwinkels zwischen rechtem und linkem Eingangssignal. Die folgenden Messprozeduren sind Beispiele, wie die Auswertung der eingelesenen und in den StereoBuffer-Bereich des WaveRecords kopierten Samplewerte erfolgen kann. Viele andere Möglichkeiten sind denkbar. Besonders einfach ist es, den während der gesampleten Zeitspanne aufgetretenen Spitzenwert zu ermitteln:

procedure MeasurePeakLevels (var LocWaveRecord : TWaveRecord);
  var
    Channel,i : word;
    Peak,Norm : extended;
  begin
    with LocWaveRecord do begin
      for Channel := Le to Ri do begin
        Peak := 0;
        for i := 0 to BufferBlocks-1 do
          if Abs(StereoBuffer[i,Channel]) > Peak
            then Peak := Abs(StereoBuffer[i,Channel]);
          Norm := Peak/cMaxAudio;
          case Channel of
            Le : begin
                   PeakLevel.LeftChannel := Peak;
                   dBPeakLevel.LeftChannel := GainTodB(Norm);
                 end; // case Le
            Ri : begin
                   PeakLevel.RightChannel := Peak;
                   dBPeakLevel.RightChannel := GainTodB(Norm);
                 end; // case Ri
          end; // case
      end; // for Channel
    end; // with LocWaveRecord
  end; // procedure MeasurePeakLevels

Die auszuwertenden Daten werden der Prozedur MeasurePeakLevels als lokale Variable LocWaveRecord übergeben, die in einer for-Schleife für beide Stereokanäle ausgewertet wird. Nacheinander werden in einer weiteren for-Schleife alle Werte im StereoBuffer bis zur Anzahl der aktuell gespeicherten BufferBlocks abgefragt, ob sie größer als der letzte bereits gefundene Spitzenwert sind; in diesem Fall wird der neue Spitzenwert gespeichert. Die Funktion Abs() gibt den Absolutwert des jeweiligen Samples zurück.

Der so gefundene Peak-Level wird anschließend auf den Maximalpegel eines 16-Bit-Wertes mit Vorzeichen (32767) normiert. Eine case-Abfrage sorgt dafür, dass die gefundenen Messwerte für die Stereokanäle in die entsprechenden Variablen des WaveRecords geschrieben werden. Dabei wandelt die Funktion GainTodB() den normierten Messwert nach der Formel

in einen dB-Wert um. Der Bruch Uaus/Uein bezeichnet einen Verstärkungs- oder Abschwächungsfaktor. Die Funktion nutzt dazu die Funktion Log10, die den Brigg’schen Logarithmus zur Basis 10 errechnet. Diese Hilfsfunktionen sind in der Unit mgMath codiert:

function LogB (const x, B : extended) : extended;
  begin
    if (x <> 0) and (B <> 0)
      then LogB := Ln(x)/Ln(B)
      else LogB := 0;
  end; // function LogB

function Log10 (const x : extended) : extended;
  begin
    Log10 := LogB(x,10.0);
  end; // function Log10

function GainTodB (const Gain : extended) : extended;
  begin
    GainTodB := 20*Log10(Gain);
  end; // function GainTodB

Die Spitzenwerte des Eingangssignals sind vor allem dann interessant, wenn es um die Programmierung von Aussteuerungsanzeigen und die Vermeidung von Übersteuerungen der Soundkarte bei der Messung geht. Alle Analog-Digital-Wandler reagieren äußerst empfindlich auf jede Übersteuerung – verwertbare Messergebnisse sind dann nicht mehr zu erwarten.

Auch die Ermittlung des RMS-Effektivwertpegel des Signals ist nicht viel komplizierter:

procedure MeasureRMSLevels (var LocWaveRecord : TWaveRecord);
  var
    Channel,i : word;
    Sum,Mean,Root,Norm : extended;
  begin
    with LocWaveRecord do begin
      for Channel := Le to Ri do begin
        Sum := 0;
        for i := 0 to BufferBlocks-1 do
          Sum := Sum + (StereoBuffer[i,Channel] *
                        StereoBuffer[i,Channel]);
        Mean := Sum/BufferBlocks;
        Root := sqrt(Mean);
        Norm := Root/cMaxAudio;
        case Channel of
          Le : begin
                 RMSLevel.LeftChannel := Root;
                 dBRMSLevel.LeftChannel := GainTodB(Norm);
               end; // case Le
          Ri : begin
                 RMSLevel.RightChannel := Root;
                 dBRMSLevel.RightChannel := GainTodB(Norm);
               end; // case Ri
        end; // case
      end; // for Channel
    end; // with LocWaveRecord
  end; // procedure MeasureRMSLevels

Die englische Bezeichnung Root-Mean-Square für den Effektivwert eines Wechselspannungssignals beschreibt ziemlich präzise, was man mit den einzelnen Samplewerten machen muss, um den Effektivwert des Signals zu errechnen: Die einzelnen Samples jedes Stereokanals werden quadriert – square - (nicht mit der sqr-Funktion, weil die einen Float-Wert als Eingabe erwartet und unser StereoBuffer-Array aus Feldern vom Typ SmallInt besteht), die Quadrate aufsummiert und der Mittelwert – mean – berechnet, aus dem schließlich die Quadratwurzel – root – gezogen wird:

Auch dieser Pegel wird auf den maximalen SmallInt-Wert normiert und in einen dB-Wert umgerechnet. Wenn bekannt ist, welcher Spannungspegel (bei LineIn meist in der Größenordnung von einem Volt) der Vollaussteuerung (full scale, FS) des Soundkarteneingangs entspricht, dann kann im Programm einfach ausgerechnet werden, welcher tatsächliche Eingangspegel zurzeit anliegt. Dazu muss der Messwert mit einem entsprechenden Skalierungsfaktor multipliziert werden.

Top

3. Frequenzmessung

Von großem Interesse ist neben den Amplitudenwerten auch die Frequenz des gesampleten Signals – soweit es sich überhaupt um ein periodisches Signal handelt, dass mit einer spezifischen Frequenz wiederkehrt. Neben ihrer eigentlichen Funktion der Frequenzbestimmung zum Beispiel im Sinne eines Frequenzzählers kann man die Frequenz auch nutzen, um die Signale eines gleichspannungsgesteuerten Oszillators auszuwerten. Mit einem solchen U/f-Umsetzer ist es möglich, über die Soundkarte auch Gleichspannungen, Gleichströme, ohmsche Widerstände und allgemein über spezielle Sensoren auch sehr langsame Zustandsänderungen wie Temperatur und Luftdruck zu messen. Der Kern der Frequenzmessung ist ganz einfach:

        j := 0;
        for i := 1 to BufferBlocks-1 do begin
          if ((StereoBuffer[i-1,Channel] <= 0)
          and (StereoBuffer[i,Channel] > 0))
            then j := Succ(j);
        end; // for i
        Freq := j*cSampleRate/(BufferBlocks);

Alle Samplewerte werden darauf geprüft, ob ein "ansteigender" Nulldurchgang des Signals vorliegt. Dazu wird mit einer if-Abfrage festgestellt, ob zwei benachbarte Samples jeweils kleiner und größer als Null sind. In diesem Fall wird der Zähler j inkrementiert. Aus dem zeitlichen Abstand zweier solcher Nulldurchgänge ergibt sich die aktuelle Frequenz. in der Variablen Freq.

Der zeitliche Abstand zweier Samples ist durch die Konstante cSampleRate (im Programmbeispiel 44100 Hz, also 1/44100 Sekunden) definiert. Folgen die Nulldurchgänge beispielsweise im Abstand von je 44 Samples, so entspricht dies einer Frequenz von 44100 Samples/Sekunde ¸ 44 Samples » 1000 1/Sekunde bzw. Hz. Man sieht hier allerdings auch, worauf es ankommt: Wenn durch den Einfluss von Störungen und Rauschen kein klar definierter Nulldurchgang erkennbar ist und das Signal im Bereich des Nulldurchgangs mehrfach prellt, dann kann die tatsächliche Frequenz nicht gemessen werden. In diesem Fall müsste das Eingangssignal wahrscheinlich gefiltert werden, um den Rauschanteil zu reduzieren. Klar ist auch, dass die Auflösung der Messung von der ausgewerteten Zeit und der Höhe der Samplerate abhängt: Je höher die Abtastrate und je länger der ausgewertete Zeitraum, umso präziser kann die Messung sein.

procedure MeasureFrequency (var LocWaveRecord : TWaveRecord);
  var
    Channel,i,j : word;
    Freq  : extended;
  begin
    with LocWaveRecord do begin

      for Channel := Le to Ri do begin

        j  := 0;
        for i := 1 to BufferBlocks-1 do begin
          if ((StereoBuffer[i-1,Channel] <= 0)
          and (StereoBuffer[i,Channel] > 0))
            then j := Succ(j);
        end; // for i
        Freq := j*cSampleRate/(BufferBlocks);

Wie man sieht, sind ganze sieben Programmzeilen für die Frequenzmessung nötig. Eine Periodizität ist feststellbar, wenn zwei oder mehr Nulldurchgänge erkennbar sind. Der Zähler j hat dann mindestens den Wert 2. Ob das Ergebnis ein sinnvoller Wert ist, kann allerdings kaum automatisch ermittelt werden. Dazu müsste das Signal im Einzelnen "von Hand" beurteilt werden. Die Variable TWaveRecord.Averaging steuert, ob das aktuelle Messergebnis ausgegeben wird oder ein gleitender Mittelwert, bei dem der aktuelle Messwert nur zu jeweils 10 Prozent zum Ergebnis beiträgt.

        if j > 2 then begin
          case Channel of
            Le : begin
                   if Averaging
                     then Frequency.LeftChannel := 0.1*Freq + 0.9*Frequency.LeftChannel
                     else Frequency.LeftChannel := Freq;
                 end; // case Le
            Ri : begin
                   if Averaging
                    then Frequency.RightChannel := 0.1*Freq + 0.9*Frequency.RightChannel
                    else Frequency.RightChannel := Freq;
                 end; // case Ri
          end; // case
        end // if j > 2
       
        else begin
          Frequency.LeftChannel := 0;
          Frequency.RightChannel := 0;
        end; // else

      end; // for Channel
    end; // with LocWaveRecord
  end; // Procedure MeasureFrequency

Natürlich sind auch aufwendigere Mittelungsverfahren möglich. War keine Periodizität erkennbar, dann werden die Ergebnisse für linken und rechten Kanal Null gesetzt.

Top

4. Messung der Phasendifferenz – XOR-Methode

Eine einfache elektrische Schwingung ist durch die Signalform, die Frequenz und die Amplitude beschrieben. Betrachtet man zwei Schwingungen, so kommt in vielen Fällen ein Zeitversatz zwischen den Schwingungen hinzu. Ein solcher Zeitversatz stellt sich zum Beispiel ein, wenn ein elektrisches Signal eine Filterschaltung durchläuft: Das Ausgangssignal hat dann die gleiche Frequenz wie das Eingangssignal, aber möglicherweise eine veränderte Amplitude (Verstärkung, Dämpfung) und es tritt gegenüber dem Eingangssignal zeitlich verzögert auf.

Dieser Zeitversatz wird nicht in einer Zeiteinheit angegeben, sondern mit Bezug auf die Periodendauer des Signals im Winkelmaß. Eine Verzögerung um eine volle Signalperiode – bei 1000 Hz also um eine Millisekunde – entspricht einem Phasenversatz um 360 Grad (im Bogenmaß: 2π). Da sich das Signal nach jeder Periode wiederholt, ist eine Phasenverschiebung um 360 Grad nicht erkennbar. Bei einer Verschiebung um eine halbe Periode (= 180 Grad) ist das Signal genau gegenphasig: Ein Wellenberg des Eingangssignals entspricht einem Wellental des Ausgangssignals.

Errechnet man für einen bestimmten Zeitpunkt t den momentanen Amplitudenwert einer Sinusschwingung, dann taucht der Phasenwinkel im Argument der Sinusfunktion auf:

Der Phasenwinkel j0 wird als Nullphasenwinkel bezeichnet. Haben zwei Schwingungen gleicher Frequenz (w = 2πf) den gleichen Nullphasenwinkel, so sind sie gleichphasig und weisen keine Phasendifferenz oder Phasenverschiebung auf.

Die Messung des Phasenwinkels zweier Signale – zum Beispiel von Ein- und Ausgangssignale einer Schaltung – ist vor allem bei elektrischen Filterschaltungen eine sinnvolle Sache. Bei rückgekoppelten Systemen kann man Aufschlüsse über die Stabilität bekommen. Interessant sind auch Untersuchungen an Schwingkreisen (Lautsprechern), da sich aus der Phasenlage die Resonanzfrequenz (Nulldurchgang der Phase) elegant bestimmen lässt. Das Bild zeigt die Schaltung eines einfachen RC-Hochpasses, der im Grund einen Spannungsteiler aus zwei Widerständen darstellt.

Einer dieser beiden „Widerstände“ weist allerdings einen frequenzabhängigen Wert auf, denn die Impedanz eines Kondensators ist

Als Beispiel ist ein Kondensator mit einer Kapazität von 220 Nanofarad gerechnet. Bei einer Frequenz von 1000 Hz weist dieser Kondensator eine kapazitive Impedanz (man spricht auch vom Blindwiderstand) von 723 Ohm auf. Das ist ein komplexer Wert, deshalb darf man, um die Gesamtimpedanz Z der RC-Kette zu finden, die beiden Widerstandswerte nicht einfach addieren. Man (erinnert sich an Pythagoras und) addiert ihre Beträge:

Als Beispiel wurde ein Widerstand R von 1000 Ohm angenommen. Der Spannungsteiler dämpft das Eingangssignal um den Dämpfungsfaktor D

bzw. 0,81. Gleichzeitig wird die Phase gedreht – und damit sind wir bei dem Punkt, um den es hier eigentlich geht:

Zwischen Ein- und Ausgangssignal tritt eine Phasenverschiebung um 60 Grad auf. Bei einem Filter 1. Ordnung, wie es hier als Beispiel dienst, beträgt die Phasenverschiebung zwischen Ein- und Ausgang bei der Grenzfrequenz (-3 dB) θ = 45 Grad. Man kann die Phasendifferenz daher auch dazu benutzen, die Grenzfrequenz von Filterschaltungen zu bestimmen. Solche Phasenverschiebungen wollen wir messen. Wie wir gesehen haben, lässt sich die Frequenz eines Signals bestimmen, indem man die Zeit zwischen zwei gleichen Zuständen (Phasen) ein und desselben periodischen Signals wie zum Beispiel einer Sinusschwingung misst. Dazu kann man zum Beispiel die Nulldurchgänge von minus nach plus nehmen. Im Prinzip misst man den Phasenversatz zwischen zwei Signalen (im Programmbeispiel zwischen rechtem und linkem Eingangskanal der Soundkarte), indem man den Zeitversatz zwischen gleichen "Phasen" – zum Beispiel von Nulldurchgängen - der Signale bestimmt.

In vielen Fällen wird bei Hardware-Lösungen für Phasendetektoren mit Exklusiv-Oder-Schaltungen (XOR) gearbeitet, weil das eine besonders einfache Methode zur Phasenmessung darstellt. An dieser Methode, die auch im Phasendetektor I des gängigsten Phase-Locked-Loop-ICs (PLL) CD4046 eingesetzt wird, orientiert sich die folgende Delphi-Prozedur:

procedure MeasurePhaseXOR (var LocWaveRecord : TWaveRecord);

  var
    Channel,i : word;
    BoolBuffer : array[0..44100,Le..Ri] of boolean;
    Sum,Phi : extended;
  begin
    with LocWaveRecord do begin

      for Channel := Le to Ri do begin
        for i := 1 to BufferBlocks-1 do begin
          if (StereoBuffer[i,Channel] > 0)
            then BoolBuffer[i,Channel] := true
            else BoolBuffer[i,Channel] := false;
        end; // for i
      end; // for Channel

Die für das Verfahren notwendige boolesche Operation XOR ist in Pascal vordefiniert, so dass wir sie nicht selbst zu kodieren brauchen. Die Eingangsgrößen für die Verknüpfung müssen vom Typ boolean sein, deshalb werden zwei entsprechende Buffer-Arrays definiert. Mit Hilfe eines simplen Komparators wird aus den Samples der beiden Eingangskanäle im LocWaveRecord zwei Rechteckfunktionen gebildet: Wenn der Sample-Wert größer als Null ist, dann wird der boolesche Wert true gespeichert; ist der Sample-Wert kleiner oder gleich Null, dann false. Wichtig für die Funktion von Phasendetektoren nach der XOR-Methode ist, dass die beiden Eingangssignale ein möglichst genaues 50/50-Puls/Pausen-Verhältnis (duty cycle) aufweisen. Bei Hardware-Messgeräten wird das häufig durch eine Teilung des Rechtecksignals durch zwei sichergestellt. Das könnte man im Programm natürlich nachahmen. Da ich meine Experimente mit sauberen Sinussignalen gemacht habe, war diese Vorsichtsmaßnahme nicht nötig. Nach der Vorbereitung der Rechtecksignale folgt die XOR-Verknüpfung der beiden Eingangskanäle:

      Sum := 0;
      for i := 0 to BufferBlocks-1 do begin
        if (BoolBuffer[i,Le] xor BoolBuffer[i,Ri])
          then Sum := Sum + 180;
      end; // for i

Die logische Verknüpfung Exklusiv-Oder bedeutet, dass eine Aussage dann wahr ist, wenn entweder die erste Aussage oder die zweite Aussage wahr ist, aber nicht beide. Als Formelzeichen für diese Verknüpfung wird meist XOR verwendet, das auch in Pascal benutzt wird. Umgangssprachlich entspricht die Aussage "Entweder A oder B" am besten dem Ausdruck "A XOR B". Praktisch entspricht dies der Addition zweier Bits modulo 2- ist die Summe eine gerade Zahl ergibt sich 0, ist sie ungerade 1. Am besten sieht man das an der folgenden Wahrheitstabelle:

XOR-Verknüpfung zweier Bits:
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0

Daraus lässt sich auch ersehen, wie die Phasendetektion mit der XOR-Verknüpftung funktioniert: Sind beide Eingangssamples gleich, bleibt der Ausgang null; sind sie ungleich, wird der Ausgang eins. Das Ausgangssignal ist deshalb eins, wenn beide Signale unterschiedliche Zustände haben. Bei der maximalen Phasenverschiebung um 180° unterscheiden sich die Signale zu jedem Zeitpunkt – das Ausgangssignale ist immer eins (true) – im Programm wird dieser Zustand sofort mit dem Faktor 180 gewichtet, damit die Berechnung den gewünschten Winkel-Wert liefert. Sind die beiden Signale exakt in Phase, dann ist der Ausgang immer null (false) und der Phasenwinkel ist ebenfalls null Grad.

Im Timing-Diagramm ist eine Phasenverschiebung um 90° dargestellt, für die sich am Ausgang eine Rechteckfunktion mit 50/50-Puls-Pausen-Verhältnis ergibt (vorausgesetzt, auch die beiden Eingangsignale sind streng symmetrisch). Der Mittelwert dieser Ausgangsfunktion beträgt gerade ½. Für alle dazwischen liegenden Phasenverschiebungen ergeben sich entsprechende Zwischenwerte.

 

Die Integration in der Summen-Variablen Sum und anschließende Mittelwertbildung der Ausgangsfunktion liefert den Phasenwert. Hardwaremäßig würde das mit einem einfachen RC-Tiefpass erster Ordnung verwirklicht:

      Phi := Sum/Bufferblocks;

      if Averaging
        then Phase := 0.9*Phase + 0.1*Phi
        else Phase := Phi;

    end; // with LocWaveRecord
  end; // procedure MeasurePhaseXOR

Es schließt sich noch eine einfache gleitende Mittelwertbildung an, die bei Bedarf mit der Variablen WaveRecord.Averaging eingeschaltet werden kann.

Top

5. Phasenmessung mit flankengesteuerten FlipFlops

Die Phasenmessung mit der XOR-Funktion hat den Nachteil, dass nur der Betrag der Phase zwischen 0° und 180° gemessen werden kann. Die Information über das Vorzeichen der Phase entfällt, so dass zwischen induktiven und kapazitiven Phasenverschiebungen nicht unterschieden wird. Das ist zumindest dann nachteilig, wenn man den komplexen Impedanzgang eines Schwingkreises – wie zum Beispiel eines dynamischen Lautsprechers – messen will. Deshalb lohnt es sich, noch eine zweite Methode zu Phasenmessung zu betrachten, die Phasendifferenzen für die vollen 360° bestimmen kann und die mit zwei flankengesteuerten (edge-controlled) Kippschaltungen (FlipFlops) arbeitet. In der Prozedur MeasurePhase2 werden die Operationen wieder an der lokalen Variablen LocWaveRecord durchgeführt. Im ersten Schritt werden sämtliche Anstiegsflanken (Nulldurchgänge) der beiden Eingangssignale von minus nach plus gesucht und in das lokale Buffer-Array Buffer1 geschrieben:

procedure MeasurePhaseECFF (var LocWaveRecord : TWaveRecord);
  var
    Channel,i,j,Trig,TrigLe,TrigRi : word;
    Buffer1,Buffer2 : array[0..44100,Le..Ri] of integer;
    Sum,Phi : extended;
    RSFFLe,RSFFRi : boolean;
  begin
    with LocWaveRecord do begin
      for Channel := Le to Ri do begin
        j  := 0;
        Buffer1[0,Channel] := 0;
        for i := 1 to BufferBlocks-1 do begin
          if (StereoBuffer[i-1,Channel] <= 0)
          and (StereoBuffer[i,Channel] > 0)
            then begin
              Buffer1[i,Channel] := 1;
              j := Succ(j);
              if j = 1 then begin
                Trig := i;
                case Channel of
                  Le : TrigLe := i;
                  Ri : TrigRi := i;
                end; // case
              end; // if j
            end // if
            else Buffer1[i,Channel] := 0;
        end; // for i
      end; // for Channel

Alle anderen Werte des Arrays werden Null gesetzt. Im Prinzip arbeitet die Prozedur wie eine Komparator-Schaltung, die den Nulldurchgang erkennt. Jeder Nulldurchgang inkrementiert den Zähler j. Zusätzlich wird für den ersten Nulldurchgang (j = 1) in den Triggervariablen Trig, TrigLe und TrigRi gespeichert, bei welcher Samplenummer i diese Ereignisse aufgetreten sind. Der Zeitversatz der beiden Signale ergibt sich aus der Differenz der beiden Trigger TrigLe und TrigRi multipliziert mit der Dauer eines Samples (1/Samplerate), so dass sich bereits aus diesen Informationen die Phase Phi bestimmen ließe:

      Phi := 360*1000/44100*(TrigLe-TrigRi);

      Form1.Label10.Caption := IntToStr(TrigLe-TrigRi);
      Form1.Label11.Caption := FloatToStrF(Phi,ffFixed,10,1);

Im Beispielprogramm wird das versuchsweise gemacht und die Ergebnisse ausgegeben. Leider ist diese Simpelmethode nicht sehr stabil, weil hier keine Mittelung vorgenommen wird: Letztlich wird nur die Zeitdifferenz zwischen zwei Schwingungszügen gemessen und mit Bezug auf die Periodendauer des Messsignals als Phasenwinkel ausgewertet. Deshalb springt die Phase zwischen positiven und negativen Werten hin und her. Außerdem ist die Auflösung sehr schlecht.

In der analogen Messtechnik werden Exklusiv-Oder-Gatter und FlipFlop-Schaltungen eingesetzt, um die Phaseninformation zu gewinnen. Eine verbreitete Methode der Phasenmessung zum Beispiel für PLL-Systeme beruht auf dem Einsatz zweier RS-FlipFlops. Diese Methode wird hier softwaremäßig nachgeahmt, sofern im Signal mindestens zwei Nulldurchgänge erkannt worden sind.

 

Die beiden RS-FlipFlops sind als boolesche Variablen RSFF1 und RSFF2 realisiert:

      if j > 2
        then begin
          RSFFLe := false; // Startbedingungen setzen
          RSFFRi := false;
          for i := Trig to BufferBlocks-1 do begin
            if (Buffer1[i,Le] > 0)
              then RSFFLe := true;
            if (Buffer1[i,Ri] > 0)
              then RSFFRi := true;
            if RSFF1 and RSFF2
              then begin
                RSFFLe := false;
                RSFFRi := false;
              end;

Jeder Nulldurchgangspeak im Buffer1 setzt das FlipFlop für seinen Kanal auf true (Set-Funktion). Sind beide FlipFlops gleichzeitig true, dann werden sie gemeinsam zurückgesetzt (Reset-Funktion). Anschließend werden die FlipFlop-Zustände in den lokalen Buffer2 übertragen. Dabei entstehen zwei Rechtecksignale, die zwischen den Werten -360 und Null und +360 und Null hin und her schalten.

            if RSFFLe
              then Buffer2[i,Le] := -360
              else Buffer2[i,Le] := 0;
            if RSFFRi
              then Buffer2[i,Ri] := +360
              else Buffer2[i,Ri] := 0;
          end; // for i

          Sum := 0;
          for i := 0 to BufferBlocks-1 do begin
            Sum := Sum + Buffer2[i,Le] + Buffer2[i,Ri];
          end; // for i
          Phi := Sum/Bufferblocks;

Der Durchschnittswert der Summe Sum aller Samples dieser beiden Rechteckschwingungen entspricht der Phasendifferenz Phi, wobei sich mit den Faktoren + und -360 das Winkelmaß in Grad ergibt. Im analogen Schaltungsvorbild entsprechen diese Bearbeitungsschritte der Summierung und anschließenden Integration der Signale durch einen Tiefpass.

          if Phi > 180
            then Phi := Phi - 360;

Phasenverschiebungen von mehr als 180 Grad kann man auch so interpretieren, als sei ein Signal gegenüber dem Bezugssignal vorgezogen worden (voreilende Phase). Deshalb rechnen wir Phasenwinkel > 180 Grad in negative Phasenwinkel um. Der Rest der Prozedur führt bei Bedarf gesteuert durch die Variable Averaging eine gleitende Mittelung der Messwerte durch. Ist keine Phase messbar (weniger als drei Nulldurchgänge), dann wird der Ergebniswert Null gesetzt. Das Ergebnis wird schließlich im WaveRecord gespeichert und an die aufrufende Routine zurückgegeben:

          if Averaging
            then Phase := 0.9*Phase + 0.1*Phi
            else Phase := Phi;

        end // if j > 2
        else begin
          Phase := 0;
        end; // else

    end; // with LocWaveRecord
  end; // procedure MeasurePhaseECFF

Ähnlich wie bei Frequenzmessungen ist auch die Präzision von Phasenmessungen in hohem Maß von der Qualität der Eingangssignale abhängig. Es ist mir erst nach einer Reihe von Experimenten gelungen, einigermaßen stabile Ergebnisse zu erzielen. Insbesondere die Einführung der Triggervariablen Trig, die dafür sorgt, dass die Auswertung zu einem definierten Zeitpunkt mit dem ersten Nulldurchgang beginnt, hat für einen Fortschritt bei der Verwertbarkeit der Messergebnisse gesorgt.

          for i := Trig to BufferBlocks-1 do begin
          ...

Trotzdem gelingt es nicht, auch bei sehr niedrigen (< 20 Hz) und sehr hohen Frequenzen (> 5000 Hz) so gute Ergebnisse zu erzielen, wie man sie mit einer Hardwarelösung bekommen kann. Der ausgewertete Signalabschnitt ist in unserem Programm lediglich 200 Millisekunden lang. Ein Problem liegt darin, dass bei einer niedrigen Frequenz von 10 Hz gerade zwei +/- Nulldurchgänge in diesen Zeitraum fallen, bei 20 Hz sind es vier – da gibt es nicht viel zu mitteln und die Fehlermarge ist groß. Andererseits wird bei hohen Frequenzen der Zeitabstand zwischen den Nulldurchgängen sehr kurz und kann durch die Samplerate von 44100 nicht mehr ausreichend genau aufgelöst werden: Bei 5000 Hz bedeutet ein Phasenversatz von 90 Grad eine Zeitdifferenz von 90°/(5000 Hz · 360°) = 0,05 Millisekunden – zum Vergleich: ein Sample umfasst 1/(44100 Hz) = 0,023 Millisekunden und beschreibt die kleinste auflösbare Zeiteinheit. Wenn man bei solch hohen Frequenzen messen wollte, müsste man mit einer deutlich höheren Abtastrate arbeiten. Eine andere Möglichkeit bestünde darin, die Phase analog zu messen und die so gefundene Messgröße über eine geeignete Schnittstelle an den Computer zu übertragen – aber das liegt außerhalb des Themenkreises dieses Tutorials. Wenn jemand eine gute Methode kennt, wie man die Phasendifferenz zweier diskretisierter periodischer Signale besser bestimmen kann, dann würde ich mich über eine Nachricht sehr freuen.

Ein weiteres Problem kann sich bei der Messung an Resonanzkreisen ergeben, wenn im Resonanzfall die Impedanz des Schwingkreises minimal wird. An dieser niedrigen Impedanz fällt nur eine sehr niedrige Messspannung ab, bei der wegen der eingeschränkten Auflösung üblicher Soundkarten von 16 Bit und auch wegen der Rauschproblematik Nulldurchgänge nicht mehr sicher erkannt werden können. In diesem Bereich kann es daher zu Phasen-Messfehlern kommen. Der nutzbare Bereich von rund 20 bis 3000 Hz reicht aber aus, um sinnvolle Messungen an dynamischen Lautsprechern durchzuführen, zum Beispiel um die Thiele-Small-Parameter zu messen.

Damit man ohne großen Aufwand eigene Experimente mit der Phasenmessung anstellen kann, habe ich einen Generator als kleines Tool programmiert, der auf dem linken und rechten Stereokanal ein Sinussignal mit einer festen Frequenz von 1000 Hz erzeugt. Die Phasendifferenz zwischen den beiden Signalen kann von 0° bis 360° eingestellt werden. Der Phasengenerator steht am Ende dieses Textes zum Download bereit.

Top

6. Offsetspannung kompensieren

Viele Soundkarten weisen teils erhebliche Offsetspannungen an ihren Eingängen auf und verfügen nicht über automatische Abgleichmethoden, um diesen Offset auszugleichen. Die Offset-Gleichspannung auf dem Eingang sorgt für eine Verschiebung der Signal-Nulllinie in positiver oder negativer Richtung, so dass bei der Messung zum Beispiel Nulldurchgänge nicht mehr symmetrisch liegen. Das kann eine Fehlerquelle sein und ist auch bei der graphischen Ausgabe eines gesampleten Signals verwirrend. Mit der Prozedur CompensateOffset kann ein Gleichspannungsoffset auf dem aufgezeichneten Signalstück erkannt und kompensiert werden.

procedure CompensateOffset (var LocWaveRecord : TWaveRecord);
  var Channel,i : word;
      Sum, Offset : extended;
  begin
    with LocWaveRecord do begin
      if (not CompOffset)
        then exit
        else begin
          for Channel := Le to Ri do begin
            Sum := 0;
            for i := 0 to BufferBlocks-1 do
              Sum := Sum + StereoBuffer[i,Channel];
            Offset := Sum/BufferBlocks;
            for i := 0 to BufferBlocks-1 do
              StereoBuffer[i,Channel] := Round(StereoBuffer
                                         [i,Channel]-Offset);
          end; // for Channel
        end; // else
    end; // with LocWaveRecord
  end; // procedure CompensateOffset

Die Offset-Kompensation wird durch die boolesche Variable CompOffset des WaveRecords gesteuert. Ist dieses Flag gesetzt, werden für beide Stereokanäle nacheinander zunächst alle Werte aufsummiert und der Durchschnitt errechnet. Bei einem symmetrischen Wechselspannungssignal wäre dieses Integral - von kleinen Randproblemen wegen der endlichen Signallaufzeit – genau Null. Ergibt sich ein Gleichspannungsversatz, so wird dieser Offsetwert von allen Samples abgezogen und so kompensiert.

Top

7. Aufruf der Auswertefunktionen

Im Teil I des Tutorials wurde bereits gezeigt, wie die BASS.DLL in regelmäßigen Zeitabständen eine Callback-Funktion aufruft, in der die in einer Buffer-Variablen übergebenen Samples gespeichert oder weiterverarbeitet werden. Die Callback-Funktion wird immer dann aufgerufen, wenn ein gefüllter Buffer zur Verfügung steht. Welche Parameter an die Funktion zu übergeben sind, ist im Teil I dargestellt. Die Bufferlänge kann bei Start des Aufzeichnungsprozesses eingestellt werden. Die Default-Länge des Recording-Buffers liegt bei 100 ms entsprechend einer Länge von 100 ms * 44100 Samples/s = 4410 Samples * 2 Kanäle * 2 Bytes = 17640 Bytes. Sie wird in der Prozedur RecordButtonClick auf 200 ms entsprechend 35280 Bytes eingestellt.

function RecordingCallback (Handle : HRecord;
                            Buffer : Pointer;
                            BufLength : DWord;
                            User   : DWord) : boolean; stdcall;
  var
    LocalBuffer: TStereoBuffer;
    i : cardinal;
  begin
    with WaveRecord do begin
      BufferBlocks := BufLength div 4;
      CopyMemory(@StereoBuffer,Buffer,BufLength);
    end;

Mit dem CopyMemory-Befehl kopieren wir den von der Soundkarte übergebenen Buffer in den StereoBuffer unseres globalen WaveRecords, der als zweidimensionales Array aufgebaut ist. So können wir die Samples des linken und rechten Kanals nach Belieben ansprechen auslesen. CopyMemory arbeitet mit Zeiger-Variablen; einen entsprechenden Pointer auf unseren StereoBuffer als Zieladresse besorgen wir uns mit Hilfe des @-Operators. Der WaveRecord nimmt alle Messergebnisse auf. Sind die Daten erst einmal in den StereoBuffer übertragen, können wir die Samples mit den vorgestellten Messroutinen aufbereitet und auswertet werden:

    CompensateOffset(WaveRecord);  // Offsetanteil kompensieren
    MeasureRMSLevels(WaveRecord);  // RMS-Level messen
    MeasurePeakLevels(WaveRecord); // Peak-Level messen
    MeasureFrequency(WaveRecord);  // Frequenz messen
    MeasurePhase(WaveRecord);      // Phasenlage von Li zu Re messen

    // Messwertanzeige aktualisieren:
    with Form1 do begin
      with WaveRecord do begin     // alle dB-Werte sind negativ
        if RadioButton1.Checked
          then ProgressBar1.Position := Round(100+dBPeakLevel.LeftChannel);
        if RadioButton2.Checked
          then ProgressBar1.Position := Round(100+dBRMSLevel.LeftChannel);
        Edit1.Text := FloatToStrF(Frequency.LeftChannel, ffFixed,10,0) + ' Hz';
        Edit2.Text := FloatToStrF(Phase,ffFixed,10,0) + ' °';
      end; // with WaveRecord
    end; // with Form1

Nach der Auswertung können im Programm einige Anzeigen auf die neuen Messergebnisse aktualisiert werden. Eine simple Fortschrittsanzeige fungiert hier als logarithmische Aussteuerungsanzeige. Mit zwei Radiobuttons kann festgelegt werden, ob RMS- oder Peak-Werte angezeigt werden sollen. Am Ende muss die Callback-Funktion als Ergebnis true zurückgeben, damit der Recording-Vorgang fortgesetzt wird.

    Result := true;

  end; // function RecordingCallback

Top

Download dieses Textes als PDF-Datei

Download des Delphi-Quelltextes für das Beispielprogramm

Download des Phasengenerator-Programms

(c) Michael Gaedtke, Im Püllenkamp 2, D-41462 Neuss, Germany – michael@gaedtke.name