Messen mit der Soundkarte III:
Funktionsgenerator in DDS-Technik mit BASS.DLL
Soundkarten-Messung III:
Sinus- und Funktions-Generator
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
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 (2p*0/8) = 0
S(1) = sin (2p*1/8) = 0.707
S(2) = sin (2p*2/8) = 1.0
S(3) = sin (2p*3/8) = 0.707
S(4) = sin (2p*4/8) = 0
S(5) = sin (2p*5/8) = -0.707
S(6) = sin (2p*6/8) = -1
S(7) = sin (2p*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.
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.
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.
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
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.
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.
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