Messen mit der Soundkarte III: Funktionsgenerator in DDS-Technik mit BASS.DLL

Soundkarten-Messung III:
Sinus- und Funktions-Generator

Druckfassung

von Michael Gaedtke

Letzte Änderungen vom 14. Dezember 2006

1. Direkte Digitale Frequenz Synthese – der DDS-Generator

2. Signalgeneratoren mit definierter Phasendifferenz

3. Stream-Ausgabe mit der BASS.DLL – Vorbereitungen

4. Initialisierung der Soundkarte - Create und Destroy

5. Soundkarten-Ausgabe

6. Die Callback-Routine

7. Downloads

 

1. Direkte Digitale Synthese – der DDS-Generator

Zum Messen braucht man Testsignale. Auf der Werkbank von Elektronikern und in vielen physikalischen und akustischen Labors findet man deshalb als Minimalausstattung einen Sinusgenerator. Mit einem Funktionsgenerator lassen sich verschiedene Signalformen (neben dem Sinussignal meist Rechteck-, Dreieck- und Sägezahn-Signale, manchmal auch Burst-Signale) erzeugen. Soweit es sich um Testsignale für die Niederfrequenztechnik im Audiobereich bis 20.000 Hz handelt, kann die Soundkarte für die Erzeugung der Testsignale eingesetzt werden. Für die Ausgabe von Tonsignalen ist sie geradezu gemacht und die Wiedergabe gespeicherter Töne oder Tonfolgen zum Beispiel aus Wave-Dateien stellt auch in Delphi fast kein Problem dar – das geht sogar mit der mitgelieferten TMediaPlayer-Komponente (im Gegensatz zur Aufnahme von Signalen).

Allerdings müsste dazu ein Testsignal als Wave-Datei vorhanden sein. Eine entsprechende Datei hat immer eine endliche Länge. Wenn man ein länger andauerndes Signal benötigt, dann müsste das gespeicherte Signal in einer Schleife gegebenenfalls "geloopt" werden, wobei Probleme an der Schnittstelle auftreten können. In jedem Fall ist es schwierig, bei gespeicherten Signalen online Einstellungen von Frequenz und Phase vorzunehmen. Besser wäre es daher, wenn man verschiedene Testsignale zur Programmlaufzeit erzeugen und direkt über die Soundkarte ausgeben könnte. Darum soll es in diesem Tutorial gehen. Das Bild links zeigt den dabei entstandenen kleinen Sinusgenerator mit einstellbarer Frequenz und Amplitude.

Bevor wir uns mit der Frage beschäftigen, wie die Speicherstrukturen für die Kommunikation mit der Soundkarte beschaffen sein müssen und wie man die Soundkarte mit Hilfe der BASS.DLL von Ian Luck dazu bekommt, die Samples aus dieser Speicherstruktur auszugeben, müssen wir zunächst klären, wie wir das gewünschte Signal überhaupt "generieren"? Soweit es uns um eine Sinusschwingung geht, ist das mathematisch kein großes Problem, denn die Sinus-Funktion ist in Pascal bereits vordefiniert. Um bei einer Samplerate von 44.100 Hz ein Sinussignal mit einer Sekunde Dauer zu erzeugen, müssen 44.100 einzelne Samples errechnet werden, die für diese 44.100 Zeitpunkte im Abstand 1/44.100 den jeweiligen momentanen Amplitudenwert haben:

Der Nullphasenwinkel j0 hat an dieser Stelle keine Bedeutung; wir können ihn Null setzen. Der griechische Buchstabe omega bezeichnet die Kreisfrequenz (w = 2πf). Xpeak ist die gewünschte Spitzenamplitude, mit der der Sinuswert multipliziert werden muss, denn die Sinusfunktion bewegt sich nur im Bereich -1 bis +1. Im Quelltext könnte das dann so aussehen:

Xpeak := 100.0;
Frequency := 1000.0; // Hz

for t := 0 to 44100-1 do begin
  Sine[t] := Trunc(Xpeak * Sin(2*pi*Frequency * t/44100));
end;

In dem der BASS.DLL beigefügten Programmbeispiel StreamTest (STREAMTEST.DPR und STMAIN.PAS), das einen User-definierten Audiostream demonstriert, wird nach diesem Muster vorgegangen. Eine solche direkte Berechnung der Sinusfunktion "online" hat allerdings den Nachteil, dass das Programm immer wieder die recht zeitaufwändige Sinusfunktion aufrufen muss. Diese Zeit geht für andere Berechnung bei der Auswertung der aufgenommenen Samples verloren – bei langen FFT-Berechnungen kann dann die Zeit schnell knapp werden. Nachteilig kann die Methode auch sein, wenn man die Frequenz des Signals bei laufendem Generator ändern will: Sprünge im Signal können zu unschönen Nebengeräuschen führen. Insbesondere ist beim Frequenzwechsel zu beachten, dass die berechneten Blöcke definiert mit Nulldurchgängen enden und beginnen müssen. Das erfordert zusätzlichen Rechenaufwand.

Eine deutlich schnellere Methode zur Erzeugung eines Sinussignals hat Martin Ohsmann in seiner DSP-Einführung für Elektor beschrieben, die im Elektor-Verlag auch als kleine Broschüre erschienen ist [Espresso - Der schnelle Einstieg in die digitale Signalverarbeitung DSP, Aachen (Elektor Verlag) 1999]. Das Verfahren beruht auf einem sinnreichen Einsatz der Additionstheoreme der Sinus- und Cosinusfunktion:

Für die Programmierung wird zunächst die Schreibweise vereinfacht:

Damit sind ck und sk die diskreten Stützwerte einer Cosinus- bzw. Sinusschwingungen der Frequenz f bei der Samplefrequenz fs. Diese Abkürzungen setzt man mit der Laufvariablen k in die Additionstheoreme ein:

Die Parameter P und Q müssen nur am Anfang einmalig errechnet werden. Alle weiteren neuen Abtastwerte Next_ck und Next_sk für den Zeitpunkt k+1 lassen sich aus den Werten ck und sk, den Werten zum Abtastzeitpunkt k, durch vier Multiplikationen, eine Addition und eine Subtraktion errechnen. Das ist ein sehr schnelles Verfahren. Übersetzt in Quelltext könnte das zum Beispiel so aussehen:

var

  ...
  P,Q,ck,sk : extended;
  Next_ck,Next_sk : extended;
  NewStart: boolean;

  P := Cos(2*pi*Frequency/cSampleRate);
  Q := Sin(2*pi*Frequency/cSampleRate);
  if NewStart
    then begin
      ck := 1;
      sk := 0;
      NewStart := false;
    end; // if NewStart
  for i := 0 to Length-1 do begin
    LocalBuffer^[Le] := Trunc(Amplitude*
cMaxAudio *ck);
    LocalBuffer^[Ri] := Trunc(Amplitude*
cMaxAudio *ck);
    Next_ck := P*ck - Q*sk;
    Next_sk := Q*ck + P*sk;
    ck := Next_ck;
    sk := Next_sk;
    Inc(LocalBuffer);
  end; // for i

Damit der Generator startet, müssen die Anfangswerte von ck und sk richtig gesetzt sein. Das kann beim Neustart des Generators durch die boolesche Variable NewStart gesteuert werden. Von Nachteil ist, dass sich mit dieser Methode nur Sinussignale herstellen lassen; die Möglichkeit, auf einfache Art und Weise auch andere Signalformen zu erzeugen, entfällt. Allerdings ist die Methode sehr schnell, weil die Sinus- und Cosinus-Funktionen nur je einmal beim Wechsel der Frequenz aufgerufen zu werden brauchen. Bei der Implementierung sollte man darauf achten, dass mindestens die beiden Variablen ck und sk global definiert sind, da es sonst zu Brüchen im Signal mit unschönen Nebengeräuschen kommt.

Ein Aufsatz von Gregor Kleine aus einer alten Elektor-Ausgabe [Gregor Kleine: DDS – Direkte Digitale Synthese, in: Elektor, Nr. 5, 1992, S. 52ff] hat mich auf die Idee gebracht, die DDS oder DDFS-Technik für die softwaremäßige Signalerzeugung einzusetzen. Das Verfahren arbeitet mit einer Sinus-Tabelle, die nur einmal gefüllt zu werden braucht, und ist wegen seiner Schnelligkeit besonders gut für die online Erzeugung von Signalen geeignet.

Die Direkte Digitale Synthese (DDS, manchmal auch DDFS für Direkte Digitale Frequenz Synthese) ist ein besonders elegantes Verfahren, um digitale Generatoren aufzubauen, das seit Mitte der 80er Jahre entwickelt worden ist, um den Aufbau von Empfängern zu vereinfachen. [vgl. Eva Murphy, Colm Slattery: All About Direct Digital Synthesis, in: Analog Dialogue 38-08, August 2004] Häufig wird in Funk- und Hörfunkempfängern heute der Lokale Oszillator als DDS-Frequenzsyntheziser ausgeführt. Das ermöglicht die numerische Eingabe der Abstimmfrequenz, meist über einen Mikrokontroller, der über eine Datenschnittstelle mit dem DDS-Baustein verbunden ist. DDS-Generatoren sind heute als integrierte Schaltungen erhältlich. Wer mehr darüber wissen will, sollte auch auf der Internetseite von Elexs vorbeischauen. Burkhard Kainka setzt integrierte DDS-Generatoren zum Selbstbau von DRM-Empfängern für 500 kHz bis 22 MHz ein. Eine sehr gute Erklärung der DDS findet sich auch auf der Homepage von Martin Pechanec aus Prag. Viele weiterführende Links sind im Netz zu finden.

Der Aufbau eines DDS-Generators ist relativ simpel und oben im Bild skizziert. Man erkennt vier Elemente: Ein Phasenregister (Akkumulator), eine Sinustabelle, einen Digital/Analog-Wandler und das zugehörige antialiasing Tiefpass-Filter. Bei einer Hardware-Lösung wird außerdem ein Impulsgeber benötigt, dessen Clock-Impulse die einzelnen Baugruppen synchronisieren. D/A-Wandler und Tiefpass-Filter sind Bestandteile der Soundkarte, um die wir uns nicht weiter zu kümmern brauchen.

Das Phasenregister, das häufig auch als Akkumulator bezeichnet wird, hat einen Clock-Eingang. Bei jedem Clock-Impuls wird das Register um einen vorgewählten Inkrement-Wert (Jump Size) weitergezählt – das Register "sammelt", "akkumuliert" diese Inkremente. Dieser Akkumulator ist als Ring aufgebaut, d.h. er zählt von Null bis zu einem Höchstwert, der der Registerlänge entspricht, und springt dann wieder auf Null um - der Zählvorgang beginnt von vorn.

Als Pascal-Quelltext könnte das wie folgt aussehen:

const
  MaxPhase : cardinal = 7;
  PhaseIncrement : cardinal = 1;

type
  TPhase = word;

var
  ...
  Phase : TPhase;

function PhaseAccumulator : word;
  begin
    Phase := (Phase + PhaseIncrement) mod MaxPhase;
    Result := Phase;
  end; // function PhaseAccumulator

Die Funktion PhaseAccumulator zählt die Variable Phase um den festgelegten Inkrement-Wert hoch. Wird dabei die in der Konstanten MaxPhase festgelegte Länge des Rings überschritten, so springt der Akkumulator auf Null zurück. Die Funktion mod liefert den Restbetrag einer ganzzahligen Teilung zurück und erzeugt hier das gleiche Ergebnis wie

if Phase > MaxPhase
  then Phase := 0;

Hat zum Beispiel der Phasen-Akkumulator eine Länge von 8 und ein Inkrement von 1, dann erscheinen am Ausgang nacheinander die Zustände 0, 1, 2, ..., 6, 7, 0, 1, 2 … , wenn der Akkumulator zyklisch durch ein Clock-Signal aufgerufen wird.

Man kann sich den Akkumulator auch als ein Phasenrad vorstellen, das in N gleiche Teile geteilt ist, die jeweils einen Winkel von 2p/N umfassen. Das Phasenrad hat – ähnlich wie ein Glücksrad - einen Zeiger, der bei jedem Clock-Impuls – oder im Programmbeispiel bei jedem Aufruf der Funktion – um eine bestimmte Anzahl von Winkeleinheiten weiter gedreht bzw. inkrementiert wird.

Nehmen wir nun an, jede Position auf dem Phasen-Rad sei mit einer Speicherzelle einer Tabelle verbunden, in der der zum jeweiligen Winkel gehörende Sinuswert sin (2pi*n/N) gespeichert ist. Für unser Beispiel mit N = 8 ergibt das die Speicherinhalte für n = 0 bis 7:

  S(n) = sin (2p*n/N)

  S(0) = sin (2
p*0/8) = 0
  S(1) = sin (2
p*1/8) = 0.707
  S(2) = sin (2
p*2/8) = 1.0
  S(3) = sin (2
p*3/8) = 0.707
  S(4) = sin (2
p*4/8) = 0
  S(5) = sin (2
p*5/8) = -0.707
  S(6) = sin (2
p*6/8) = -1
  S(7) = sin (2
p*7/8) = -0.707

  S(8) = S(0) = 0 usw.

Diese Werte braucht man nur noch so zu normieren, dass sie mit dem Digital/Analog-Wandler der Soundkarte zusammenpassen, dann hat man alle Bestandteile für einen Sinus-Generator. Da der D/A-Wandler 16 Bit Zahlen verarbeitet, müssen die Werte der Sinustabelle mit ½ * 216 = 32767 multipliziert und in 2 Byte Integerwerte gewandelt werden. Wie man auch erkennt, brauchen wegen der Symmetrieeigenschaften der Sinusfunktion nur die Werte für das erste Viertel der Funktion errechnet zu werden: Danach spiegeln sich die Werte. Außerdem muss die Tabelle nur ein einziges Mal zum Beginn der Programmausführung initialisiert werden. Dann ist der Generator nicht mehr auf langwierige Berechnungen angewiesen, sondern greift die Werte aus der Tabelle ab.

Wenn nun ein Clock-Signal mit einer Frequenz von 8 Hz den Phasen-Akkumulator hoch zählt, dann werden innerhalb einer Sekunde alle Werte der Sinustabelle nacheinander aufgerufen und am Ausgang des D/A-Wandlers wird ein kompletter Sinuszyklus pro Sekunde erscheinen – also eine Frequenz von einem Hertz. Die Verhältnisse bleiben gleich, wenn man statt der noch von Hand zu berechnenden acht Werte des Beispiels 44.100 Werte in die Sinustabelle schreibt, den Phasen-Akkumulator bis 44.099 laufen lässt und die Clock-Frequenz ebenfalls auf 44.100 Hz erhöht. Das Ergebnis ist wieder 1 Hz, allerdings natürlich mit deutlich höherer Auflösung. Jeder Schwingungszyklus wird aus 44.100 Werten aufgebaut.

Bisher haben wir mit dem Phasen-Inkrement 1 gearbeitet; jeder mögliche Zustand des Phasen-Akkumulators wird damit nacheinander erreicht. Wenn wir das Inkrement zum Beispiel auf zwei erhöhen, dann wird am Ausgang nur jeder zweite Wert der Sinus-Tabelle erscheinen, wegen der festen Clock-Frequenz wird der Akkumulator jedoch zwei mal pro Sekunde umlaufen – die Ausgangsfrequenz steigt auf zwei Hertz. Wie man sieht stimmen bei den gewählten Ausgangswerten (Akkumulatorlänge = Abtastrate = 44.100) Phasen-Inkrement und Ausgangsfrequenz des Generators überein, sodass eine weitere Umrechnung entfällt. Es ergibt sich über das gesamte darstellbare Spektrum eine Auflösung von einem Hertz. Für unsere Zwecke reicht das aus. Die obere Grenzfrequenz des Generators liegt wegen der Nyquist –Frequenz bei der halben Akkumulatorlänge, also bei 22.050 Hz. Die Grundfunktionen der DDF-Synthese sind in der Unit mgDDFS zusammengefasst und können so leicht in eigene Quelltexte importiert werden. Wer will, könnte aus der Unit bei Bedarf auch eine Komponente machen. Die Unit steht am Ende dieses Tutorial zum Download bereit. Sie stellt insgesamt 16 einzelne „Generatoren“ zur Verfügung, so dass mehrere Frequenzen unabhängig voneinander und mit beliebiger Phasenlage zueinander erzeugt werden können. Der Aufbau des Programms folgt dem Blockschaltbild der DDS. Deshalb stellt die Unit im Interface einen Phasen-Akkumulator nebst einer Funktion, um ihn zurück zu stellen, und eine Sinus-Tabelle zur Verfügung:

unit mgDDFS;

interface

function PhaseAccumulator (N : byte; Frequenz : extended) : cardinal;
function ResetPhaseAccumulator (N : byte) : cardinal;
function PhaseToSine      (P : cardinal) : smallint;

Dem Phasen-Akkumulator wird beim Aufruf in der Variablen N die Nummer des angesprochenen Generators übergeben, in der Variablen Frequenz die gewünschte Frequenz. Die Funktion liefert als Ergebniswert eine Cardinal-Zahl, die wiederum als Adresse für die Sinus-Tabelle dient. Die Tabelle wird durch die Funktion PhaseToSine abgefragt:

implementation

type
  TFeld  = array [0..44099] of smallint;
  TPhase = array [0..16] of cardinal;

var
  SinusFeld     : TFeld;
  ...

const
  MaxPhase : cardinal = 44099;
  MaxFrequenz : extended = 22000.0;

procedure InitPhaseFeld;
  var i : byte;
  begin
    for i := 1 to 16 do
      Phase[i] := 0;
  end; // procedure InitPhaseFeld;

procedure InitSinusFeld;
  var t : cardinal;
  begin
    for t := 0 to 11024 do begin
      SinusFeld[t] := Round(32767*sin(2*pi*t/44099));
      SinusFeld[22049-t] := SinusFeld[t];
      SinusFeld[t+22050] := -1*SinusFeld[t];
      SinusFeld[44099-t] := -1*SinusFeld[t];
    end; // for t
  end; // procedure InitSinusFeld

function ResetPhaseAccumulator (N : byte) : cardinal;
  begin
    if N in [1..16]
      then Phase[N] := 0;
    Result := Phase[N];
  end; // procedure ResetDDFS

function PhaseAccumulator (N : byte; Frequenz : extended) : cardinal;
  begin
    if N in [1..16]
      then begin
        Result := Phase[N];
        if (Frequenz > 1.0) and (Frequenz <= MaxFrequenz)
          then Phase[N] := (Phase[N]+Round(Frequenz)) mod MaxPhase
          else Result := 0;
      end; // if N in [1..16]
  end; // function PhaseAccumulator

function PhaseToSine (P : cardinal) : smallint;
  begin
    Result := SinusFeld[P];
  end; // function PhaseToSine

Der Aufruf der Sinus-Tabelle mit der Funktion PhaseToSine liefert das erforderliche Datenformat – 16 Bit Integer mit Vorzeichen – für die Soundkarte. Die Frequenz-Variable muss auf einen ganzzahligen Wert gerundet werden, weil wir lediglich ganzzahlige Frequenzwerte auflösen können. Integrierte DDS-Bausteinen arbeiten meist mit einem Phasenregister hoher Bitzahl (32 oder 48), bei dem für die Auswertung die untersten Bits abgeschnitten werden, so dass sich ein Rundungseffekt ergibt. Die Auslesung der aktuellen Position des Phasen-Akkumulators erfolgt übriges vor der Inkrementierung der Position, damit beim Start des Generators das Signal mit dem Wert Null begonnen wird. Die Verwendung einer Auflösung von 16 Bit bei einer Sample-Rate von 44.100 Hz führt zu THD-Verzerrungswerten, die je nach Qualität der Soundkarte und ihrer D/A-Wandler in der Größenordnung von CD-Qualität liegen. Das ist in Analogtechnik bei durchstimmbaren Generatoren nicht ganz einfach zu erreichen.

Im Initialisierungsteil der Unit werden die Tabellen mit Werten vorbelegt. Diese Funktionen brauchen im Regelfall während der Programmlaufzeit nicht erneut aufgerufen zu werden.

initialization

  InitPhaseFeld;
  InitSinusFeld;
  ...

end.

Es ist sehr einfach, fast beliebige andere periodische Signale zur erzeugen. Dazu muss lediglich eine veränderte Tabelle an die Stelle des Sinus-Registers treten. Mit einem (hier nicht implementierten) Cosinus-Register wäre es zum Beispiel möglich, zwei Signale mit starrer Phasendifferenz von 90 Grad zu erzeugen., Durch Hinzufügung von Rechteck-, Dreieck- und Sägezahn-Tabellen erweitern wir den Sinus-Generator zum Funktionsgenerator. Die Initialisierung dieser Signaltypen wird durch drei Prozeduren erledigt, die ebenfalls im Initialisierungsteil aufgerufen werden:

procedure InitSquareFeld;
  var t : cardinal;
  begin
    for t := 0 to 22049 do
      SquareFeld[t] := 26000;
    for t := 22050 to 44099 do
      SquareFeld[t] := -26000;
  end; // procedure InitSquareFeld

procedure InitTriangleFeld;
  var t : cardinal;
  begin
    for t := 0 to 11024 do begin
      TriangleFeld[t] := Round(26000*t/11024);
      TriangleFeld[22049-t] := TriangleFeld[t];
      TriangleFeld[t+22050] := -1*TriangleFeld[t];
      TriangleFeld[44099-t] := -1*TriangleFeld[t];
    end; // for t
  end; // procedure InitTriangleFeld

procedure InitRampFeld;
  var t : cardinal;
  begin
    for t := 0 to 44099 do
      RampFeld[t] := Round(-1*((52000*t/44099)-26000));
  end; // procedure InitRampFeld

initialization

  ...
 
InitSquareFeld;
  InitTriangleFeld;
  InitRampFeld;

end.

Die Umwandlung des Phasensignals vom Akkumulator ist wie beim Sinussignal mit drei Funktionen gelöst:

function PhaseToSquare (P : cardinal) : smallint;
  begin
    Result := SquareFeld[P];
  end; // function PhaseToSquare

function PhaseToTriangle (P : cardinal) : smallint;
  begin
    Result := TriangleFeld[P];
  end; // function PhaseToTriangle

function PhaseToRamp (P : cardinal) : smallint;
  begin
    Result := RampFeld[P];
  end; // function PhaseToRamp

Bei Signalen, die sehr steile Flanken aufweisen, ist allerdings zu berücksichtigen, dass sie wegen des steilflankigen Tiefpass-Filters am Ausgang der Soundkarte möglicherweise nicht unverzerrt wieder gegeben werden können. Solche Signale enthalten Frequenzanteile weit oberhalb der Nyquist -Frequenz, für die die Soundkarte nicht vorbereitet ist. Wer solche Signale für Messzwecke benötigt, muss möglicherweise auf externe Generatoren ausweichen, die zum Beispiel über eine Serielle oder UMS-Schnittstelle gesteuert werden. Am besten schaut man sich die Generator-Ausgangsspannung mit einem Oszilloskop an, um die Signalform zu begutachten. Bei Sinus-Signalen treten Probleme mit Aliasing-Filtern im Regelfall nicht auf.

Wer einen Generator mit höherer Auflösung bauen möchte – was für Messungen im Bereich sehr tiefer Frequenzen erforderlich sein kann – der braucht dazu lediglich die Länge des Akkumulators zum Beispiel auf das Zehnfache zu verlängern und ein Sinusregister entsprechender Auflösung vorzusehen. Bei konstanter Clock-Frequenz muss man dann die gewünschte Ausgangsfrequenz mit 10 multiplizieren, um das erforderliche Phasen-Inkrement zu erhalten. Ein Generator mit diesen Werten hätte eine Frequenzauflösung von 0,1 Hz. Der Preis dafür ist ein höherer Speicherbedarf für die Sinustabelle, die nun 441.000 Werte umfasst (und wahrscheinlich schon merkbare Zeit für ihre – nur einmal erforderliche – Berechnung in Anspruch nimmt).

Ein wichtiger Vorteil der DDF-Synthese besteht darin, dass beim Wechsel der Frequenzen keine Phasensprünge auftreten, auch ohne dass man besondere Vorsichtsmassnahmen treffen müsste. Wir können das Inkrement – und damit die Frequenz – gewissermaßen in vollem Lauf ändern, was eine unmittelbare Änderung der Tonhöhe zur Folge hat. Das ist besonders praktisch für die Programmierung von durchstimmbaren Generatoren, für Sweep-Generatoren, terz- oder oktav-gewobbelte Messsignale und für frequenzmodulierte Signale allgemein. Auch eine Kombination von Sweep- und FM-Steuerung ist möglich. Wir nutzen als akustisches Messsignal für Lautsprecher zum Beispiel einen Sweep mit einstellbaren Frequenzgrenzen, der ein terzgewobbeltes Signal durchstimmt. Der Generator erzeugt die entsprechenden Werte für die Soundkarte in Echtzeit. Aber das geht über die Grenzen dieses Tutorials hinaus.

Zum Abschluss ein Anwendungsbeispiel der DDS-Funktionen, mit dem ein Sinuston von 440 Hz und 10 Sekunden Länge mit der maximalen Amplitude erzeugt werden soll. Die zeitdiskreten Werte werden in eine vorbereitete Wave-Datei geschrieben:

uses
  ...
  mgDDFS;

procedure WriteAudioData (DateiName : string);
  const
    Rate = 44100;      // Samplerate
    Amplitude = 1.0;   // maximale Amplitude für 16 Bit Audio
    Freq = 440.0;      // Frequenz in Hz / der Kammerton A
    Dauer = 10;        // Dauer des Signals in Sekunden
  var
    WaveFile : file of smallint; // smallint : 16 Bits
    AudioData : SmallInt;
    t : cardinal;
  begin
    // Der Header der WAV-Datei muss vorbereitet sein.
    AssignFile (WaveFile,DateiName);
    Reset (WaveFile);  //Reset als file of smallint
    // Daten am Ende des Header anfügen:
    Seek (WaveFile,FileSize(WaveFile));

    // hier folgt die eigentliche Tonerzeugung mit den DDFS-Funktionen:
    for t := 1 to (Rate*Dauer) do begin
     
AudioData := Round(Amplitude*PhaseToSine (PhaseAccumulator(1,Freq)));

      // Audiodaten in Datei schreiben
      Write (WaveFile,AudioData);
    end; // for t
    CloseFile(WaveFile);
  end; // procedure WriteAudioData

Dieses Beispiel berücksichtigt nicht, wie der Header der Wave-File aufgebaut wird und wie man zum Abschluss der Signalerzeugung die Wave-Datei komplettiert. Hier soll lediglich gezeigt werden, wie der Aufruf der DDS-Funktionen erfolgt. Dazu ist innerhalb einer for-to-Schleife eine einzige Quelltextzeile erforderlich, in der der Phasen-Akkumulator mit der Nummer 1 und der gewünschte Frequenz aufgerufen wird. Mit dem Ergebnis der Akkumulatorfunktion wird das Sinus-Register ausgelesen und mit der gewünschten normierten Amplitude multipliziert.

Top

2. Signalgeneratoren mit definierter Phasendifferenz

In manchen Fällen ist es praktisch, Signale mit gleicher Frequenz und definierter Phasendifferenz erzeugen zu können. Solche Signale werden zum Beispiel für bestimmte Modulationsverfahren gebraucht. Ich habe solche Signalgeneratoren eingesetzt, um für Testzwecke den im Mess-Tutorial II dargestellten Generator mit einstellbarer Phasenlage zu programmieren. 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 DDS-Technik ermöglicht die Einstellung der Phasenlage auf besonders einfache Weise: Dazu braucht für einen zweiten Generator lediglich die "Phasenuhr" einmalig um einen bestimmten Winkel vor- oder nachgestellt zu werden. Bei der gewählten Aufteilung in 44100 Schritte entsprechen 22050 Schritte einer Phasendifferenz von 180°, 11025 Schritte entsprechen 90° und so weiter. Mit der Formel Phase * 44100/360 kann man die Akkumulatordifferenz der beiden Phasenakkumulatoren für jeden beliebigen Winkel bestimmen. In der Funktion InitPhase ist das verwirklicht:

function InitPhase (N1,N2 : byte; Ph : extended) : cardinal;
  var
    PC : cardinal;
  begin
    PC := Round(cMaxPhase*Ph/360);
    if (N1 in [1..8]) and (N2 in [9..16])
      then Phase[N2] := (Phase[N1] + PC) mod cMaxPhase;
    Result := PC;
  end; // function InitPhase

Die Bezugssignale werden von den acht Generatoren #1 bis #8 erzeugt. Jedem dieser Signale können Signale der Generatoren #9 bis #16 mit einer definierten Phasendifferenz zugeordnet werden. Die Nummer des Bezugsgenerators wird in N1 übergeben, die Nummer des zu setzenden Generators in N2. Die gewünschte Phasendifferenz wird im Parameter Ph übergeben. Sie kann von -180° bis +180° betragen. Mit dem Aufruf

InitPhase(1,9,180);

würde zum Beispiel festgelegt, dass der Generator #9 mit einer Phasendifferenz von +180° gegenüber dem Bezugsgenerator #1 "schwingt". Auch in diesem Fall zeigt sich, dass die DDF-Synthese besonders elegante Lösungsmöglichkeiten bietet.

Top

3. Stream-Ausgabe mit der BASS.DLL - Vorbereitungen

Voraussetzung für die Nutzung der BASS-Routinen ist, dass die DLL in die eigene Anwendung eingebunden wird. Für ein neues Projekt legt man dazu ein eigenes Verzeichnis an, in das alle zum Projekt gehörenden Dateien kommen. In dieses Verzeichnis kopiert man auch die BASS.dll und die zugehörige Unit BASS.pas, die die DLL für Delphi portiert. Später muss die DLL-Datei auch mit der fertigen EXE in einem Verzeichnis stehen.

In den Interface-Teil des neuen Programms nimmt man in die uses-Klausel die BASS-Unit auf:

interface

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

  mgDDFS,
  BASS;

Außerdem werden eine Reihe von Konstanten und Typen definiert. Zu den Konstanten gehören auch Flags, die für die Steuerung einiger BASS-Routinen benötigt werden. Der SET-Typ TSignalForm definiert die vier Signalformen, die unser Funktionsgenerator erzeugen soll:

const
  cDefaultDevice = -1;    // Default Device Identifier
  cSampleRate = 44100;    // PCM-Audio
  cNumChannels = 2;       // Stereo
  cBlockLength = 4;       // 2 x 2 Bytes für Stereo in 16 Bit
  cRecordingTime = 200;   // ms (10 - 500 ms / Default 100 ms)
  cBufferBlocks = 44100;  // Länge des Puffers für Audiosamples
  c16BitAudio = 0;        // Flag für 16 Bit Audio
  cMaxAudio = 32768;      // maximaler Pegel bei 16 Bit
  cNoRestart = false      // Flag für BASS_ChannelPlay
  cDefaultUser = 0;       // UserIdentifier (not used)
  cDirectXPointer = nil;  // Pointer für DirectX Class Identifier
  Le = 0;                 // Left Channel
  Ri = 1;                 // Right Channel  cSampleRate = 44100;

type
  TAudioSample = SmallInt;
  TStereoSample = array[Le..Ri] of TAudioSample;
  TSignalForm = (sfSine, sfSquare, sfTriangle, sfRamp);

In der Unit BASS.pas sind die Funktionsaufrufe der externen DLL zusammengefasst. Näheres dazu ist im Teil I des Tutorials erläutert.

Top

4. Initialisierung der Soundkarte
- Create und Destroy

Damit ist der Interface-Teil der Applikation bereits abgeschlossen und wir kommen zur Implementation: In der FormCreate-Prozedur wird beim Programmstart zunächst geprüft, ob die korrekte Version von BASS geladen werden kann, andernfalls wird eine Fehlermeldung ausgegeben und die Ausführung abgebrochen:

procedure TForm1.FormCreate(Sender: TObject);
  var
    i : byte;
    Par : Integer;
    ChannelName : PChar;
  begin
    if (HiWord(BASS_GetVersion) <> BassVersion)
      then begin
        MessageBox(0,'Falsche Version der BASS.dll geladen!',
                   nil ,MB_IconError);
        Halt;
      end; // then

Die Versionskennung der DLL wird im HiWord-Teil der Funktionsantwort von BASS_GetVersion zurück geliefert. Beim Schließen des Programms müssen die von der DLL belegten Speicherbereiche wieder frei gegeben werden. Dazu dient die Funktion BASS_Free die alle Ressouren der Eingangs-Einheit einschließlich der Samples und Streams freigibt. Die Funktion gibt im Fehlerfall den boolschen Wert false zurück; der Rückgabewert braucht aber nicht zwingend ausgewertet zu werden. Mit BASS_Stop werden sicherheitshalber alle Ausgaben gestoppt.

procedure TForm1.FormDestroy(Sender: TObject);
  begin
    BASS_RecordFree;
    BASS_Free;
    BASS_Stop;
  end; // procedure FormClose

Top

5. Soundkarten-Ausgabe

Um die Wiedergabe des errechneten Signals zu starten, muss das BASS-System initialisiert werden. Das übernimmt in der Ereignis-Prozedur StartButtonClick die Funktion BASS_Init:

procedure TForm1.StartButtonClick(Sender: TObject);
  begin

    // BASS für die Dafault-Soundkarte initialisieren:

    if not (BASS_Init (cDefaultDevice,
                       cSampleRate, ,    // Samplerate
                       c16BitAudio, ,    // Flags; 0 = 16 Bit Audio
                       Handle, ,         // Applikation-Handle
                       cDirectXPointer)) // Pointer für DirectX
      then begin
        BASS_Free;
        MessageBox(0,'Default-Soundkarte kann
                   nicht gestartet werden!',
                   nil, MB_IconError);
        Halt;
      end; // then

An BASS_Init werden fünf Parameter übergeben, die

*             das Ausgabegerät (Device) festlegen; wir verwenden das Default-Device, dazu muss der als Konstante cDefaultDevice festgelegte Wert -1 übergeben werden

*             die Abtastrate (Samplerate) für die Ausgabe festlegen; wir verwenden 44.100 Hz, dieser Wert ist in der Konstanten cSampleRate festgelegt

*             über Flags eine Reihe von Ausgabemöglichkeiten wie Auflösung (8 oder 16 Bit) und die Zahl der Kanäle (mono oder stereo) u.a. steuern; wird statt eines oder mehrerer gesetzter Flags 0 übergeben, so wird die Soundkarte mit 16 Bit Auflösung in Stereo betrieben

*             das Main-Window des Programms; mit 0 wird das Fenster verwendet, dass sich aktuell im Vordergrund befindet; die Variable Handle übergibt das Windows-Handle von Form1 unseres Programms

*             und einen Identifier, um ein DirectSound-Objekt zu initialisieren; um diesen Vorgang kümmert sich die BASS.DLL im Hintergrund, es reicht, wenn wir nil übergeben, dann wird das Default-Objekt verwendet.

Wenn die Soundkarte erfolgreich initialisiert werden konnte, dann gibt die Funktion den booleschen Wert true zurück, andernfalls false. In diesem Fall kann mit der Funktion BASS_ErrorGetCode der Fehlercode ermittelt werden. Näheres dazu findet sich in der mitgelieferten Dokumentation bass.chm. BASS_Init muss erfolgreich aufgerufen werden, bevor Samples oder Streams ausgegeben werden können.

Im nächsten Schritt muss ein Stream-Objekt geschaffen werden. Das übernimmt die Funktion BASS_StreamCreate:

    SignalStream := BASS_StreamCreate (cSampleRate,
                                       cNumChannels,
                                       c16BitAudio,
                                       @GeneratorCallback,
                                       cDefaultUser);
    if (SignalStream = 0)
      then begin
        Error('User-Stream kann nicht angelegt werden!');
        Exit;
      end; // if SignalStream

In unserem Fall brauchen wir einen Ausgabe-Stream mit einer SampleRate von 44100 Hz und mit zwei Ausgabekanälen. Die Ausgabefrequenz kann maximal die halbe SampleRate betragen. BASS_StreamCreate erwartet fünf Parameter, von denen die meisten als Konstanten (erkennbar an dem kleinen c) definiert sind:

*             Die Abtastrate (cSampleRate); wir verwenden 44.100 Hz, was CD-Qualität entspricht; die verwendete Samplerate kann über die Funktion BASS_ChannelSetAttributes geändert werden.

*             Die Anzahl der Kanäle (cNumChannels), also 1 für mono, 2 für stereo, 4 für Quadrophonie; 6 steht für Wiedergabe im 5.1-Format, 8 für das 7.1-Format; alle Formate mit mehr als zwei Stereokanälen erfordern WDM-Treiber; für unsere Messzwecke ist das aber uninteressant.

*             Über eine Reihe von Flags (c16BitAudio) können auch hier die Ausgabemöglichkeiten gesteuert werden. Für Messzwecke ist in erster Linie die Auflösung des ausgegebenen Signals wichtig. Mit einer Null erreichen wir, dass 16 Bit Auflösung aktiviert werden.

*             Die Callback-Funktion, mit der der erzeugte Stream mit Daten gefüllt werden soll, wird als Zeiger übergeben. Mit dem @-Operator übergeben wir einen Pointer auf unsere Funktion GeneratorCallback.

*             Als letzter Parameter wird eine User-Variable (cDefaultUser) im DWORD-Format übergeben, mit der wir bei Bedarf einen beliebigen Wert an unsere Callback-Funktion übergeben könnten. Diese Möglichkeit wird hier nicht genutzt, deshalb wird Null übergeben.

BASS_StreamCreate gibt im Erfolgsfall einen Handle für den neu angelegten Stream zurück, der in der HStream-Variablen SignalStream gespeichert wird. Im Fehlerfall wird der Wert Null zurückgegeben und der aufgetretene Fehler kann mit der Funktion BASS_ErrorGetCode ermittelt werden. Einzelheiten dazu erläutert die Dokumentation bass.chm. Wir errechnen hier unsere Daten selbst bzw. erzeugen sie mit Hilfe eines Software-DDS-Generators. Die "Alltagsaufgabe" von BASS ist allerdings die Wiedergabe von Streams im MP3-, MP2-, MP1-, OGG-, WAV- und AIFF-Format. Dazu wird vorzugsweise die Funktion BASS_StreamCreateFile verwendet, die die Daten unmittelbar aus Dateien lesen kann.

Jetzt kann die Wiedergabe des neuen Streams gestartet werden:

    if not BASS_ChannelPlay (SignalStream,
                             cNoRestart)
      then begin
        Error('Wiedergabe kann nicht gestartet werden.');
        Exit;
      end;

Diese Aufgabe übernimmt die Funktion BASS_ChannelPlay, mit der sich eine gestoppter Wiedergabe- oder Aufnahmeprozess auch wieder aufnehmen (resume) lässt. Die Funktion erwartet zwei Parameter, nämlich

*             den Handle des wiederzugebenden Streams vom Typ HChannel, HMusic, HStream oder auch HRecord und

*             einen Restart-Schalter, mit dem veranlasst wird, dass die Wiedergabe jeweils am Anfang der Aufzeichnung beginnt. Wenn der Handle zu einem User-Stream gehört, dann würde der Inhalt des aktuellen Puffers gelöscht. Der Schalter wird hier deshalb mit der Konstanten cNoRestart auf false gesetzt. Bei der Aufnahme von Signalen hat der Restart-Schalter keinen Einfluss.

Im Erfolgsfall gibt die Funktion true zurück, andernfalls false. Der aufgetretene Fehler kann ggfs. mit der Funktion BASS_ErrorGetCode ermittelt werden. Weitere Einzelheiten finden sich in der mitgelieferten Dokumentation bass.chm.

Bevor wir zum Kernstück des Funktionsgenerators, der Callback-Routine, kommen, schauen wir uns noch kurz an, wie der Wiedergabeprozess wieder gestoppt wird.

procedure TForm1.StopButtonClick(Sender: TObject);
  begin
    BASS_Stop;
    BASS_Free;
    StartButton.Enabled := true;
    StopButton.Enabled := false;
    CloseButton.Enabled := true;
  end; // procedure StopButtonClick

Die BASS.DLL sieht dafür die Funktion BASS_Stop vor. Die Funktion braucht keine Parameter und gibt im Erfolgsfall true zurück. Die Funktion gibt false zurück, wenn zuvor kein ordnungsgemäßer Aufruf von BASS_Init erfolgt ist.

Mit der Funktion BASS_Free werden alle belegten Ressourcen einschließlich aller Samples und Streams freigegeben. Auch diese Funktion erwartet keine Parameter und gibt im Erfolgsfall true zurück. BASS_Free sollte für alle initialisierten Geräte (Devices) aufgerufen werden, bevor das laufende Programm geschlossen wird. Da BASS_Free die Freigabe auch der jeweils benutzten Samples und Streams übernimmt, ist es nicht erforderlich, diese einzeln freizugeben. Der Aufruf an dieser Stelle ist notwendig, damit der Wiedergabeprozess mit einem weiteren Aufruf von BASS_Init neu gestartet werden kann.

Top

6. Die Callback-Routine

Der Puffer des angelegten Streams muss für die Wiedergabe in regelmäßigen Abständen mit Daten gefüllt werden. Das wird – analog zum Prozess der Aufzeichnung, der in Teil I dieses BASS-Tutorials erläutert ist – mit einer Callback-Routine erledigt. Wie wir bereits gesehen haben, wird die Speicheradresse der Callback-Routine als vierter Parameter bei der Erzeugung des Streams an die Function BASS_StreamCreate übergeben. Zur Ermittlung der Adresse dient der @-Operator:

    SignalStream := BASS_StreamCreate (cSampleRate,
                                       cNumChannels,
                                       c16BitAudio,
                                       @GeneratorCallback,
                                       cDefaultUser);

Der Stream "weiß" deshalb, welche Funktion er aufrufen muss, um an neue Daten zu kommen und wo er diese Funktion im Speicher findet. Der Aufruf erfolgt ereignisgesteuert immer dann, wenn die Soundkarte die Samples in einem Puffer abgearbeitet hat. Das vollzieht sich im Hintergrund und wir brauchen uns um die Ablaufsteuerung nicht zu kümmern. Die Callback-Funktion muss ihre Parameter nach einem festgelegten Muster aufrufen, damit die Kommunikation mit dem Stream funktioniert. Auch das entspricht der Vorgehensweise, die wir bereits beim Recording in Teil I gesehen haben.

function GeneratorCallback (Handle: HStream;
                            Buffer: Pointer;
                            BufLength: DWord;
                            User: DWord): DWord; stdcall;
   ...

Damit die Parameterübergabe an das Betriebssystem funktioniert, muss die Aufrufkonvention stdcall gesetzt sein. Die Callback-Routine muss in Pascal/Delphi als Funktion aufgebaut sein, da ein Rückgabewert erwartet wird.

*             Als erster Parameter wird der Handle des Streams erwartet, der gefüllt werden soll.

*             Danach folgt ein Pointer auf einen Puffer, in den die Daten geschrieben werden. Dabei werden folgende Datenformate vorausgesetzt: 8-Bit-Samples sind vorzeichenlos (unsigned), 16-Bit-Samples haben ein Vorzeichen (wir verwenden den 2-Bytes-Delphi-Typ SmallInt); die ebenfalls verwendbaren 32-Bit-Floating-Point-Samples reichen von -1 bis +1.

*             Im Parameter BufLength wird die Länge des zu füllenden Puffers in Bytes übergeben als DWord übergeben.

*             Als vierter und letzter Parameter kann bei Bedarf eine User-Variable übergeben werden, die bei der Erzeugung des Stream mit BASS_CreateStream gesetzt wird. Auch diese Variable ist vom DWord-Typ; sie wird hier nicht genutzt.

Der Rückgabewert der Funktion ist die Zahl der in den Puffer geschriebenen Bytes (wohlgemerkt, nicht der geschriebenen Samples!) Das wird im Quelltext am Ende der Funktion erledigt.

    ...
    Result := BufLength;
  end; // function GeneratorCallback

Es ist evident, das die Funktion zur Füllung des Streams so schnell wie möglich arbeiten sollte, da andere Prozesse nicht fortgesetzt werden können, so lange die Routine läuft. Mit modernen, einigermaßen schnellen Rechnern ist das heute kein sehr großes Problem mehr. Trotzdem sollten im Zweifel eher häufiger kleine Datenpakete geschnürt werden. Dabei kann die Callback-Routine weniger Samples liefern, als BASS anfordert. Wenn der Puffer-Level allerdings zu niedrig wird, verzögert BASS automatisch die Wiedergabe des Streams, bis der Puffer wieder gefüllt ist. Mit der Funktion BASS_ChannelGetData(BASS_Data_Available) kann der Füllstand des Puffers erforderlichenfalls ermittelt werden. In jedem Fall sollte eine komplette Zahl von Samples geliefert werden. Im Regelfall funktioniert aber alles ohne diese Vorkehrungen und eine schnelle Ermittlung der Samples haben wir bei der Programmierung der DDF-Synthese bereits berücksichtigt.

Einige BASS-Routinen können Probleme verursachen, wenn sie innerhalb einer Stream-Funktion aufgerufen werden. Dazu gehören – nachvollziehbar ! - zum Beispiel BASS_Stop, BASS_Free, BASS_StreamCreate oder BASS_ChannelStop. Erläuterungen dazu liefert der Abschnitt STREAMPROC callback in der die Dokumentation bass.chm.

Für die mehrkanalige Wiedergabe müssen die Daten im SmallInt-Format (16 Bits mit Vorzeichen) in einer bestimmten Reihenfolge in den Puffer geschrieben werden – immer zuerst die beiden Bytes für den linken Kanal, dann die für den rechten Kanal. Bei der Definition des Typs TStereoSample ist das berücksichtigt:

type
  TAudioSample = SmallInt;
  TStereoSample = array[Le..Ri] of TAudioSample;

Ähnliche Festlegungen gelten auch für drei-, vier- und mehrkanalige Widergabe, die uns hier aber nicht zu interessieren brauchen. In der Callback-Routine verwenden wir einen lokalen Puffer, der als Zeiger auf den Datentyp TStereoSample definiert ist:

  var
    LocalBuffer : ^TStereoSample;
    i, Length : cardinal;
    SignalForm : TSignalForm;

Damit machen wir den lokalen Puffer kompatibel mit der von BASS als Parameter übergebenen Buffer-Variablen. Bei der Erzeugung der Samples kann die Schreibposition im Puffer mit dem Inc-Befehl  erhöht werden. Dabei wird die Schreibposition jeweils um die kompletten vier Bytes des Typs TSignalForm weitergeschoben.

  begin
    LocalBuffer := Buffer;
    Length := BufLength div cBlockLength;
    ...

Im ersten Arbeitsschritt wird der LocalBuffer-Pointer auf den übergebenen Buffer-Pointer gesetzt. Die interne Length-Variable (in 4-Bytes-Stereo-Samples) wird aus der von BASS übergebenen BufLength (in Bytes) errechnet. Anschließend kann mit der Errechnung der einzelnen Samples begonnen werden.

Der Amplituden-Faktor wird mit einem Schieberegler eingestellt. Er nimmt in diesem Fall – anders als bei unserem Recording-Beispiel in Teil I - keinen Einfluss auf die Einstellungen des Windows-Mixers, sondern beeinflusst unmittelbar das Ergebnis der Amplitudenberechnung. Die Positions-Eigenschaft des TrackBars für die Amplitudeneinstellung kann zwischen Min = 0 und Max = 32767 eingestellt werden. Da die mgDDFS-Funktionen eine Variable zwischen 0 (aus) und 1 (volle Aussteuerung) erwarten, muss der übergebene Wert in der entsprechenden Ereignisprozedur AmplitudeTrackBarChange mit 32767 normiert werden. Hier ist eine lineare Einstellung der Amplitude verwirklicht worden. Für eine Reihe von Messungen kann bei Bedarf eine logarithmische Einstellung vorgesehen werden.

Da der verwendete Algorithmus von der gewünschten Signalform abhängig ist, werden die entsprechenden Funktionen der DDS-Unit in einer case-Abfrage aufgerufen:

    SignalForm := Form1.CheckSignalForm;

    case SignalForm of

      sfSine : for i := 0 to Length-1 do begin
                 LocalBuffer^[Le] :=
                      Trunc(Amplitude*PhaseToSine
                      (PhaseAccumulator(1,Frequency)));
                 LocalBuffer^[Ri] :=
                      Trunc(Amplitude*PhaseToSine
                      (PhaseAccumulator(9,Frequency)));
                 Inc(LocalBuffer);
               end; // for i

    sfSquare : for i := 0 to Length-1 do begin
                 LocalBuffer^[Le] :=
                      Trunc(Amplitude*PhaseToSquare
                      (PhaseAccumulator(1,Frequency)));
                 LocalBuffer^[Ri] :=
                      Trunc(Amplitude*PhaseToSquare
                      (PhaseAccumulator(9,Frequency)));
                 Inc(LocalBuffer);
               end; // for i

  sfTriangle : for i := 0 to Length-1 do begin
                 LocalBuffer^[Le] :=
                      Trunc(Amplitude*PhaseToTriangle
                      (PhaseAccumulator(1,Frequency)));
                 LocalBuffer^[Ri] :=
                      Trunc(Amplitude*PhaseToTriangle
                      (PhaseAccumulator(9,Frequency)));
                 Inc(LocalBuffer);
               end; // for i

      sfRamp : for i := 0 to Length-1 do begin
                 LocalBuffer^[Le] :=
                      Trunc(Amplitude*PhaseToRamp
                      (PhaseAccumulator(1,Frequency)));
                 LocalBuffer^[Ri] :=
                      Trunc(Amplitude*PhaseToRamp
                      (PhaseAccumulator(9,Frequency)));
                 Inc(LocalBuffer);
               end; // for i

    end; // case SignalForm

Die Trunc-Funktion ist erforderlich, weil die DDF-Synthese Float-Werte liefert, die Datenübergabe an die Soundkarte aber Integer-Typen benötigt. Von den insgesamt 16 Generatoren der mgDDFS-Unit werden die Nummer (1) und (9) eingesetzt. Generator (9) ist defaultmäßig in Gegenphase (+180°) zu Generator (1). Wir erhalten an den beiden Stereo-Ausgängen der Soundkarte daher gegenphasige Signale. Die Phasenlage der Generatoren könnte bei Bedarf mit der InitPhase-Funktion der mgDDFS-Unit eingestellt werden.

Alles Weitere im Quelltext des Funktionsgenerator-Programms dient der Bedienung und braucht hier nicht weiter erläutert zu warden. Tricks warden dabei nicht angewendet; alle Komponenten des Formulars sich Standardware aus der VCL-Palette von Delphi. Programmiert wurde mit Delphi 6 – Probleme sollte es aber auch mit anderen Delphi-Versionen nicht geben.

Top

 

Download dieses Textes als PDF-Datei

Download des Delphi-Quelltextes für das Beispielprogramm

 

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