Tutorial: DTMF mit der Soundkarte | Touch Tones with Soundcards
Dual Tone Multi Frequency Steuerung mit der Soundkarte
Menü Messen mit der Soundkarte
von Michael Gaedtke
Letzte Änderungen vom 17. November 2006
1. Soundkarte als Steuerungs-Schnittstelle
2. DTMF-Empfänger mit MT8870 IC
3. Eine fortschrittliche Lösung: Auswertung mit dem Microcontroller
4. DTMF-Signalerzeugung mit der Soundkarte
5. Codeeingabe mit dem Ziffernblock der Tastatur
Beim Messen mit der Soundkarte kommt manchmal der Wunsch auf, auch die eine oder andere Steuerungsfunktion per Software vom Computer aus schalten zu können – zum Beispiel, um die Eingangsempfindlichkeit der Messanordnung anzupassen oder um beim Messen von Lautsprechern zwischen Akustik- und Impedanz-Modi umschalten zu können. Es ist unkomfortabel – und manchmal auch fehlerträchtig – wenn man dazu immer erst Schalter an einer externen Messbox betätigen muss, für deren Position es auf der Programmoberfläche auch keine Rückmeldung gibt.
Für solche Schaltaufgaben würde man natürlich normalerweise eine der parallelen, seriellen oder USB-Schnittstellen des Computers einsetzen. Aber das bringt mindestens dann, wenn an den entsprechenden Buchsen bereits Drucker und Modems angeschlossen sind weitere Stöpseleien mit sich und die Beherrschung der USB-Schnittstelle ist für eigene Entwicklungen auch nicht ganz ohne. Der Gedanke liegt daher nahe, die Soundkarte auch für diese Anwendung zweckzuentfremden, weil man sie ja ohnehin programmieren muss. Schalten über die Soundkarte – wie soll das gehen?
1. Soundkarte als Steuerungs-Schnittstelle
Nachdem ich mich bereits einige Zeit mit dem Gedanken getragen hatte, eine eigene Lösung mit mehreren steilflankigen Bandpassfiltern zusammen zu basteln, fiel mir ein interessanter Aufsatz von Klaus-Dieter Grüninger in die Hände, der auf dem Landesbildungsserver Baden-Württemberg den Fachbereich Physik betreut. Dort wird beschrieben, wie die internationalen Telefongesellschaften genau dieses Problem (natürlich schon längst) gelöst haben. Und weil Telefongesellschaften dazu neigen, eine technische Lösung nach Möglichkeit ein paar Millionen Mal zu verkaufen, gibt es natürlich auch ein spezialisiertes IC, dass alle Baugruppen vereint, die man braucht, um Steuerimpulse aus Tonfrequenzsignalen zu machen.
Früher, als die Telefonvermittlung noch weitgehend mechanisch funktioniert hat, war das keine Frage. Telefonieren funktionierte mit dem Impulswahlverfahren und die dafür notwendigen elektrischen Impulse konnten mit dem Wählscheibenmechanismus mechanisch erzeugt werden. Dabei wird die elektrische Telefonverbindung in rascher Folge kurz unterbrochen. Wer einmal ein älteres Telefon auseinander genommen hat, hat eine anschauliche Vorstellung von diesem Vorgang. Später wurden die Impulse von einfachen elektronischen Schaltungen erzeugt, mit denen auch eine Tastenwahl möglich wurde. Leider ist die Reichweite bei diesem Verfahren eingeschränkt und bei Fernverbindungen (und in den USA ist fern eben auch ferner als im kleinen Europa) konnten die Verbindungen nicht automatisch hergestellt werden. Dazu brauchte man die Fräuleins vom Amt (man kann natürlich auch Operator sagen). Außerdem lässt das Impulswahlverfahren keine drahtlosen Verbindungen über Mikrowellen oder Satelliten zu.
In den Bell Laboratories (denen wir auch die Erfindung des Transistors verdanken und die u.a. dem amerikanischen Telefonriesen AT&T gehören) wurde deshalb Ende der 1950er Jahre das sogenannte Mehrfrequenzwahlverfahren (MFV) oder Tonwahlverfahren entwickelt, das zur gebräuchlichen Wähltechnik in der analogen Telefontechnik geworden ist. Die amerikanische Bezeichnungen für dieses Verfahren sind Touch Tone und Dual Tone Multiple Frequency DTMF. Touch-Tone-Telefone wurden auf der New Yorker Weltausstellung von 1964 zuerst der Öffentlichkeit vorgestellt. Wer sich detailliert über die Hintergründe der DTMF-Technik informieren will, sollte sich den entsprechenden Grundlagenartikel in der Wikipedia ansehen. Dort finden sich auch Informationen über die Verwendung des Nummern-Zeichens # und des Stern-Symbols * sowie der (in Europa heute kaum noch verwendeten) Buchstaben A, B, C und D, mit denen die 16 möglichen Codezeichen komplettiert werden, z.B. für US-militärische und Sicherheits-Zwecke.
Die grundlegende Idee war, den Kanal der Sprachsignalübertragung auch für die Übertragung des Wahlcodes zu nutzen. Man spricht deshalb von In-Band-Signalisierung. Dazu werden Encoder und Decoder benutzt, die hörbare Signaltöne erzeugen und erkennen. Wie der Name Dual Tone bereits nahe legt, wird jede der insgesamt 16 übermittelbaren Zahlen als Kombination zweier verschiedener Tonfrequenzen übertragen. Die Frequenzpaare werden nach den Spalten und Zeilen einer 4 mal 4 Matrix kodiert, die sich an dem heute gebräuchlichen Tastenlayout der Telefone orientiert (das ja anders als auf der Computertastatur ist). Die Frequenzen liegen mitten im übertragenen Sprachband und können deshalb von jedem für die Telefonie geeigneten Medium transportiert werden. Das Bell-Labs-Verfahren ist von der International Telecommunication Union ITU standardisiert und wird auf der ganzen Welt eingesetzt. Als Bausteine werden acht Sinussignale verschiedener Frequenz eingesetzt, von denen jeweils zwei zu einem Signalton überlagert werden.
|
DTMF Tastenbelegung |
||||
|
|
1209 Hz |
1336 Hz |
1477 Hz |
1633 Hz |
|
697 Hz |
1 |
2 |
3 |
A |
|
770 Hz |
4 |
5 |
6 |
B |
|
852 Hz |
7 |
8 |
9 |
C |
|
941 Hz |
* |
0 |
# |
D |
Die Tabelle zeigt die Tastenbelegung und Frequenzzuordnung in der 4 mal 4 Matrix. Jede Reihe wird durch eine von vier niedrigen Frequenzen repräsentiert, jede Spalte durch eine von vier hohen Frequenzen. Drückt man auf einer Touch-Tone-Tastatur die Taste '9', so werden gleichzeitig die beiden Frequenzen 852 Hz und 1477 Hz ausgegeben. Die acht "krummen" Frequenzwerte wurden so gewählt, dass es durch Verzerrungen während der Übertragung nicht zu Störungen kommen kann. Deshalb wurden bei der Frequenzwahl sowohl harmonische Vielfache einer gemeinsamen Grundfrequenz wie auch mögliche Intermodulationsprodukte vermieden. Keine Frequenz entspricht deshalb einem ganzzahligen Vielfachen einer anderen Frequenz und die Summe zweier Frequenzen entspricht keiner der verwendeten Signalfrequenzen. Der Abstand der Frequenzen entspricht ungefähr dem Verhältnis 21/19 – das ist etwas weniger als ein musikalischer Ganzton. Vom Decoder müssen diese beiden Frequenzen eindeutig erkannt und ausgewertet werden. Eine Vorgabe der Telefongesellschaften ist, dass die Frequenzen um nicht mehr als 1,5 Prozent vom Sollwert abweichen und mindestens 40 Millisekunden dauern. Die Amplituden der jeweiligen zwei Frequenzen sollten gleich sein.
Die Erzeugung und Ausgabe von Sinustönen über die Soundkarte ist kein Problem, wenn man dazu zum Beispiel Routinen wie die BASS.DLL einsetzt. Für die mathematische Generierung der Töne gibt es mehrere Möglichkeiten; ich werde auch hier die komfortable Direkte Digitale Synthese einsetzen. Die dazu erforderlichen Tools sind im Teil III meines Soundkarten- Messtutorials beschrieben. Wie man dabei vorgehen kann, wird im Softwareteil am Beispiel eines kleinen Testprogramms beschrieben. Die notwendige Hardware ist mit der Soundkarte bereits vorhanden. Bleibt die Aufgabe, die Signale auf der Empfängerseite auszuwerten. Wie macht man das?
2. DTMF-Empfänger mit MT8870 IC
Es ist nicht überraschend, dass die Industrie für diese komplexe Aufgabe der Signalaufbereitung, Filterung und Auswertung ein Spezial-IC anbietet, denn andernfalls wäre dazu eine ziemlich umfängliche Schaltung (teuer) mit möglicherweise mehreren Abgleichpunkten (noch teurer) erforderlich. Der MT8870D wird von MITEL produziert und umfasst einen kompletten Integrierten DRMF Receiver. Das CMOS-IC enthält einen Eingangsverstärker, Bandfilter in Switched-Capacitor-Technik und einen digitalen Decoder für die Signaltöne. Der Decoder nutzt ein digitales Zählverfahren zur Unterscheidung der Signale und gibt die empfangene Codezahl als 4-Bit-Zeichen aus. Auch eine Clockoszillator (mit Ausnahme des Quarzes) und ein Businterface am Ausgang sind integriert. Der MT8870 ist zum Beispiel bei Conrad und bei Segor zum Preis von rund vier Euro erhältlich. Bei Segor gibt es auch eine SMD-Version des ICs. Neben diesem Chip sind nur wenige externe Bauteile erforderlich.
|
|
Die Schaltung im nebenstehenden Bild ist dem Applikationsvorschlag aus dem Datenblatt des MT8870 entnommen. Als Quarz ist ein gut erhältlicher 3,579545 MHZ Typ erforderlich. Der Eingang könnte auch als Differenzverstärker beschaltet werden; für unsere Zwecke ist das nicht erforderlich. Klaus-Dieter Grüninger weist in seinem bereits erwähnten Aufsatz darauf hin, dass diese Quarzfrequenz in amerikanischen Fernsehempfängern auch als Zwischenfrequenz verwendet wird. Für die Eingangsbeschaltung des integrierten Operationsverstärkers sind zwei Widerstände und ein kleiner Kondensator erforderlich. Die Zeitkonstante des RC-Glieds an den IC-Anschlüssen 16 und 17 bestimmt die Dauer, bis ein Signalton als gültig erkannt wird. Auch hier braucht von den Standardwerten der Applikation nicht abgewichen zu werden. |
Die Schaltung wird mit +5V versorgt. Dafür habe ich einen integrierten LM2940 CT5 Low Drop Regler eingesetzt. Wegen des niedrigen Stromverbrauchs braucht der Regler nicht gekühlt zu werden. Die beiden Steuereingänge INH und PWDN können in unserer Anwendung unbeschaltet bleiben; sie sind intern nach Ma.sse gezogen. High-Pegel an INH sperrt bei Bedarf die Auswertung der Töne für die Zeichen A, B, C und D. High-Pegel an PWDN schaltet den Dekoder ab und sperrt den Oszillator.
|
|
An den Anschlüssen 11, 12, 13 und 14 des MT8870 steht nach der Entschlüsselung das empfangene Codezeichen als 4-Bit-Datenwort an. Wenn die geplante Anwendung nur die Betätigung von vier Schaltern durch Relais erfordert, dann kann man diese Leitungen unmittelbar nutzen, um die Relais über geeignete Treiberschaltungen anzusteuern. Sollen mehr Relais geschaltet werden, dann muss das "Nibble" weiter dekodiert werden. Dazu habe ich einen Line Decoder oder Demultiplexer 74HC154 eingesetzt, von dessen 16 Ausgängen jeweils einer in Abhängig vom binären Code am Eingang aktiviert wird. Aktiviert heißt in diesem Fall, dass der Ausgang Low geschaltet wird. Die nicht angesprochenen Ausgänge liegen an High-Potential. Die Bauteile sind auf einer 80 mal 100 Millimeter großen Platine untergebracht, die zum Download zur Verfügung steht. |
Mit diesen Ausgängen können zwar insgesamt 16 verschiedene Schaltvorgänge gesteuert werden – aber immer nur jeweils einer. Meist wird es aber erforderlich sein, zwei, drei oder noch mehr Relais gleichzeitig und unabhängig voneinander zu schalten. Dazu braucht jedes Relais eine Speicherzelle, die sich merkt, welcher Schaltzustand gültig ist. Für die Speicherung werden acht Latches oder RS-FlipFlops eingesetzt. Von den 16 ansprechbaren Leitungen werden acht genutzt, um Relais über die Set-Eingänge ihres FlipFlops einzuschalten, die verbleibenden acht, um sie über die Reset-Eingänge auszuschalten.
Diese Latch-Funktion kann auf unterschiedliche Weise realisiert werden: Man kann die RS-FlipFlops aus zwei über Kreuz verschalteten NAND-Gattern (74LS00 oder MOS4000) selbst aufbauen oder integrierte Latchbausteine wie den CD4044 (Quad 3-State NAND RS Latches) oder den 74LS279 (Quad RS Lowpower Schottky Latch) einsetzen. Wichtig ist, dass RS-FlipFlops in NAND-Logik verwendet werden – NOR-Logik funktioniert an dieser Stelle nicht. Das hängt damit zusammen, dass der "normale" Zustand der bei S- und R-Eingänge High ist. Bei NAND-Logik bedeutet dies, dass sich der Schaltzustand nicht ändert. Beim NOR-FlipFlop würde High an beiden Eingängen zu einem nicht definierten Ausgangszustand führen. Das muss unbedingt vermieden werden.
|
|
Für meine Versuche habe ich den CD4044 verwendet und auf einer separaten kleinen Platine untergebracht, die zwei Relais ansteuern kann und mit vier Leitungen von den Ausgängen des 74HC154 angesteuert wird. Die entsprechenden Flachbandkabel habe ich der Einfachheit halber direkt auf der Platine verlötet. Die Schaltzustände werden wieder mit LEDs angezeigt. Um Relais ansteuern zu können, sind in der Praxis noch Relais-Treiber nötig; dafür kommen Darlington-Transistoren mit Basis-Vorwiderständen oder Kleinleistungs-MOSFETs wie zum Beispiel der BS170 in Frage, die unmittelbar „vor Ort“, am Relais montiert werden können. Dioden zur Unterdrückung der Schaltimpulse durch die Relaisspule nicht vergessen! |
3. Eine fortschrittliche Lösung:
Auswertung mit dem Microcontroller
Gemessen an der kompakten Erkennung und Umsetzung der DTMF-Signale in nur einem IC mutet die Auswertung der Digitalsignale mit Digitalschaltkreisen doch recht aufwendig an. Als ich meinem Freund Burkhard Kainka das Projekt vorgestellt habe, war für ihn die Sache sofort klar: Das ist eine klassische Aufgabe für einen Microcontroller. Dieser "intelligente" (das heißt hier: programmgesteuerte) Schaltkreis übernimmt die gesamte Auswertung des 4-Bit-Datennibbles, wie es vom MT8870 geliefert wird und stellt 8 Schaltleitungen für die Relais bereit, die die Schalttransistoren treiben. Teuer ist das nicht, denn der Microcontroller kostet weniger als 1,50 Euro und schneidet damit gegen die Einzel-IC-Lösung bestens ab, vor allem, wenn man mit gesockelten ICs arbeitet. Vor allem wird die Entwicklung einer Platine deutlich einfacher, wenn man sich im Hobbybereit auf einseitige Platinen beschränkt.
Allerdings erkauft man sich diese Vorteile mit einem zusätzlichen Arbeitsschritt, denn der Microcontroller kann ohne ein Programm zunächst einmal gar nichts. Das stellt eine gewisse Hürde dar, denn für die Programmierung werden ein paar hard- und softwaremäßige Hilfsmittel benötigt und wie jede Programmieraufgabe erfordert auch die Programmierung eines Microcontrollers eine Reihe von Kenntnissen, wie man das macht. Wenn man nur einen einzigen Controller braucht, ist der Aufwand bestimmt nicht gerechtfertigt. Wenn man ohnehin daran denkt, in die faszinierende Welt dieser kleinen Tausendsassas einzusteigen, kann dies ein geeignetes Beispielprojekt sein. Hilfreich ist dabei, wenn man sich mit einer Programmiersprache wie Basic, Pascal oder Assembler bereits auskennt. Der finanzielle Aufwand ist vergleichsweise gering, denn gute Entwicklungs-Software wie zum Beispiel die Basic-Entwicklungsumgebung BASCOM AVR oder das Pascal-Entwicklungssystem AVRco sind in abgespeckten Versionen als Freeware im Internet verfügbar und auch die Anschlussadapter für das "Brennen" des Chips sind so preisgünstig erhältlich, dass man überlegen muss, ob man teure Selbstbauzeit nicht besser in andere Platinen investiert. Eine Beschreibung des Programmiervorgangs geht über das Thema dieses Tutorials allerdings hinaus.
|
|
Burkhard hat mir den Microcontroller mit Hilfe des BASCOM Entwicklungssystems programmiert. Die Wahl fiel auf den Typ ATtiny2313 von ATMEL, der gerade die notwendige Anzahl von Ports hat und sehr preisgünstig ist. Wie man der nebenstehenden Abbildung entnehmen kann, ist das Programm sehr überschaubar: Nach ein paar Zeilen Präliminarien, die definieren, wie die Ports behandelt werden sollen, wird eine Variable I definiert und vom Port D eingelesen. An die vier Leitungen dieses Ports ist der Datenausgang des MT8870 angeschlossen. Die Teilung durch 4 und Maskierung mit 15 ist notwendig, weil Port D hier nicht mit Bit 0 beginnt und insgesamt nur 4 Bit ausgewertet werden. Die Variable Alt dient zur Unterdrückung von Schaltprell-Artefakten; es wird geprüft, ob das Eingangssignal bei zwei aufeinander folgenden Abfragen gleich ist. Die gesamt Auswertung und Speicherung der Schaltzustände, für die wir bei der Hardware-Lösung diverse ICs verbraten haben, wird mit 16 if-then-Abfragen erledigt: Liegt ein bestimmtes Signal an, wird der zugehörige Port-Ausgang zur Steuerung der Relais high oder low geschaltet. Das alles findet in einer do-loop-Programmschleife statt und wird nach einer kurzen Pause (Waitms 50) wiederholt, so lange Spannung am Controller anliegt. |
Damit die Schnittstellenplatine Relais unmittelbar treiben kann, gibt es an den Ausgängen noch Relaistreiber, die für den notwendigen Strom sorgen. Dazu wird ein achtfaches Darlington-Array ULN2803A eingesetzt, dass den nötigen Vorwiderstand für die Ansteuerung von den 5-Volt-Ausgängen der Ports mitbringt und auch die Dioden zur Unterdrückung der Schaltimpulse der Relaisspule integriert. Die Schaltzustände der Relais werden mit acht Low Current LEDs angezeigt, was sich vor allem während der Experimentierphase der Programmentwicklung als nützlich erweist. Die Relais werden über Platinen-Steckbuchsen verdrahtet und mit 12 Volt versorgt.
|
|
Der Bestückungsplan zeigt die Lage der Bauteile und der Anschlüsse auf der einseitigen Platine. Beim Bestücken muss man besonders auf die Polarität des Elkos und Dioden und auf die Ausrichtung der ICs achten, die ich gesockelt eingebaut habe. Die Vorwiderstände für die acht LEDs sind aus Platzersparnisgründen SMD-Typen, die ich auf der Platinenunterseite verlötet habe. Alle gezeigten Platinen-Layouts sind im Sprint-Layout-Format und stehen zum Download bereit. Sie können mit dem kostenfreien Viewer von Abacom ausgedruckt werden. |
4. Signalerzeugung mit der Soundkarte
Die Grundlagen der Signalausgabe über die Soundkarte mit der BASS.DLL brauchen hier nicht noch einmal wiederholt zu werden. Sie finden sich zusammengefasst im Tutorial Soundkarten-Messung III: Sinus- und Funktions-Generator. In diesem Tutorial ist auch die Erzeugung von Sinustönen mit der Direkten Digitalen Synthese DDS beschrieben, die sich auch für die Generierung der DTMF-Signaltöne anbietet. Dazu muss die Unit mgDDFS in das Programm eingebunden werden, die die entsprechenden virtuellen Generatoren zur Verfügung stellt:
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls, ComCtrls,
mgDDFS; // DDS-Routinen zur Signalerzeugung
Im Beispielprogramm sind die Signaltöne in zwei Konstanten-Arrays vom Typ TTones gespeichert, die bei der Definierung mit den genormten Frequenzen vorbelegt werden. Die Arrays werden mit dem Dezimalwert des zu übertragenden Codezeichens indiziert.
type
TTones = Array [0..15] of word;
const
// DTMF-Dialtones:
Tone1 : TTones = ( 941, 697, 697, 697, 770, 770, 770, 852,
852, 852, 941, 941, 941, 697, 770, 852);
Tone2 : TTones = (1633, 1209, 1336, 1477, 1209, 1336, 1477, 1209,
1336, 1477, 1336, 1209, 1477, 1633, 1633, 1633);
Für die Signalerzeugung wird eine Callback-Routine benötigt, die von der Soundkarte immer dann angefordert werden kann, wenn der Datenbuffer für die Ausgabe geleert ist. Diese Callback-Funktion muss nach einem festgelegten Schema aufgebaut sein, damit die Daten korrekt an den Soundkartentreiber übergeben werden können.
function
GeneratorCallback (Handle: HStream;
Buffer: Pointer;
BufLength: DWord;
User: DWord): DWord; stdcall;
Über den Parameter Handle wird der Stream angesprochen, den die BASS.DLL für die Ausgabe eingerichtet hat. Die Pointer-Variable Buffer zeigt auch einen Pufferspeicher, in dem die neu generierten Ausgabedaten abgelegt werden. BufLength übergibt die Länge des reservierten Speicherbereichs in Bytes. Die Verwendung der User-Variablen ist optional; wir verwenden sie hier nicht. Die Funktion muss ihre Parameter nach der Standard-Call-Aufrufkonvention ans Betriebssystem übergeben und wird deshalb als stdcall definiert.
var
LocalBuffer : ^TStereoSample;
i, Length : cardinal;
SignalForm : TSignalForm;
Innerhalb der Callback-Funktion wollen wir mit einem lokalen Speicherbereich arbeiten, dessen Struktur festgelegt ist. Im erwähnten Tutorial ist beschrieben, die die Daten im Puffer für die Samples organisiert sein müssen. Im Typ TStereoSample ist eine solche Struktur aus zwei kanalweise hintereinander gespeicherten vorzeichenbehafteten 16-Bit-Wörtern definiert. Der LocalBuffer ist ein Zeiger auf diese Datenstruktur.
begin
LocalBuffer := Buffer;
Length := BufLength div cBlockLength;
CodeNr := CodeArr[CodePos];
Im nächsten Schritt wird der von der BASS.DLL übergebene Buffer-Parameter unserem eigenen lokalen Puffer zugewiesen. Außerdem wird errechnet, wie viele Stereosamples in der erwarteten BufLength gespeichert werden können. Beim 16-Bit-Stereobetrieb beträgt die Blocklänge vier Bytes. Die Erzeugung der Sinussignale für die DTMF-Signaltöne wird wieder mit dem DLL-Generator erledigt:
for i :=
0 to Length-1 do begin
if CodeLength > 0 then begin
LocalBuffer^[Ri] := Trunc
(0.3*PhaseToSine(PhaseAccumulator(9,Tone1[CodeNr]))+
(0.3*PhaseToSine(PhaseAccumulator(10,Tone2[CodeNr]))));
Dec(CodeLength);
end // then
Die Signaltöne werden hier über den rechten Stereokanal der Soundkarte ausgegeben. Damit die Hardware nicht übersteuert wird, werden die beiden Signaltöne auf eine Amplitude von 0,3 festgelegt; die addierten Amplituden dürfen den Wert 1,0 in keinem Fall übersteigen. Wie man sieht werden die beiden virtuellen DDS-Generatoren Nummer 9 und 10 für die Erzeugung der zwei Signaltöne benutzt. Welcher Code übertragen werden soll, ist in der globalen Variablen CodeNr gespeichert, die Werte von 0 bis 15 annehmen kann. Dieser Wert wird hier als Indes für die beiden Frequenzarrays Tone1 und Tone2 benutzt. Die Laufvariable CodeLength zählt mit, wie viele Samples der beiden Frequenzen erzeugt werden sollen und entscheidet damit über die zeitliche Länge des generierten Signaltons. Die Länge des Signaltons wird über die Konstante cCodeLength festgelegt. Ich habe für meine Versuche 100 und 200 Millisekungen (4410 und 8820 Samples) benutzt; beides funktioniert, ist aber vermutlich üppig lang. Mit der Länge kann man daher experimentieren. Eine aktuelle CodeLength kleiner 0 zeigt an, dass das Zeichen abgearbeitet ist. In diesem Fall wird über die globale Variable CodePos und den im Feld Null (ähnlich wie bei Pascal-Strings) gespeicherten Füllstand des CodeArr geprüft, ob ein weiteres Codezeichen ausgegeben werden muss.
else begin
// neues Codezeichen holen
LocalBuffer^[Ri] := 0;
if CodePos < CodeArr[0]
then begin
CodePos := CodePos+1;
CodeNr := CodeArr[CodePos];
CodeLength := cCodeLength;
end; // if
end;
In diesem Fall wird die CodePos um eins erhöht und das nächstanstehende Zeichen aus dem CodeArr ausgelesen. Die CodeLength wird wieder auf den in der Konstanten cCodeLength (SampleRate mal gewünschte Signaldauer in Sekunden) Maximalwert gesetzt.
Normalerweise würde man den zweiten Stereokanal für andere Aufgaben einsetzen. Ich habe die DTMF-Steuerung zum Beispiel entwickelt, um damit ein kleines System für elektroakustische Messungen steuern zu können, bei dem verschiedene Messmodi über Relais umgeschaltet werden. Gleichzeitig wird die Soundkarte als Generator für die Stimulussignale verwendet. Beim Experimentieren ist es aber zunächst ganz hilfreich, wenn man auch hören kann, was vom Programm ausgegeben wird. Deshalb wird hier der linke Kanal mit dem gleichen Signal wie der rechte gespeist. Wenn man einen Aktivlautsprecher an diesen Kanal hängt, kann man bei der Steuerung mithören. Im Anschluss daran wird der Zeiger auf den Signalpuffer inkrementiert, damit das nächste Sample erzeugt werden kann.
LocalBuffer^[Le] := LocalBuffer^[Ri];
Inc(LocalBuffer);
end; // for i
Damit die Signalausgabe im Loopbetrieb fortgesetzt wird, muss die Callback-Funktion als Rückgabewert die Zahl der erzeugten Bits liefern:
Result := BufLength;
end; // function GeneratorCallback
Für die Ausgabe über die Soundkarte sind außerdem noch einige Schritte erforderlich, die ausführlich im Tutorial Soundkarten-Messung III: Sinus- und Funktions-Generator angesprochen sind und die deshalb hier nur kurz erwähnt werden. In der FormCreate-Prozedur wird geprüft, ob die korrekte Version der BASS.DLL geladen werden kann. Dazu muss die DLL-Datei im gleichen Verzeichnis wie die EXE-Programmdatei stehen. Sebstverständlich werden in der FormDestroy-Prozedur die verwendeten Ressourcen wieder freigegeben. Die Ausgabe wird mit einem Klick auf den Start-Button eingeschaltet. Dabei wird zunächst geprüft, ob sich BASS für die Soundkarte initialisieren lässt:
procedure
TForm1.BtnStartClick(Sender: TObject);
begin
if not BASS_Init (cDefaultDevice,
cSampleRate,
c16BitAudio,
Handle,
cDirectXPointer)
then begin
Error('BASS kann nicht initialisiert werden!');
Exit;
end; // if not BASS_Init
Andernfalls wird eine Fehlermeldung ausgegeben und die Routine verlassen. Im Erfolgsfall versucht BASS, einen SignalStream mit den festgelegten Parametern zu initialisieren …
SignalStream := BASS_StreamCreate (cSampleRate,
cNumChannels,
c16BitAudio,
@GeneratorCallback,
cDefaultUser);
if (SignalStream = 0)
then begin
Error('User-Stream kann nicht angelegt werden!');
Exit;
end;
… und dann zu starten:
if not BASS_ChannelPlay (SignalStream,
cNoRestart)
then begin
Error('Wiedergabe kann nicht gestartet werden.');
Exit;
end;
Die eigentliche Signalerzeugung erledigt die Callback-Routine. Um die Kommunikation mit der Soundkarte kümmert sich die BASS.DLL im Hintergrund.
// Buttons freigeben:
BtnStart.Enabled := false;
BtnStop.Enabled := true;
BtnClose.Enabled := false;
end; // procedure BtnStartClick
Damit die Wiedergabe über die BASS.DLL nach einem Stop korrekt neu gestartet werden kann, muss beim Anhalten die Routine BASS_Free aufgerufen werden, die intern verwendete Ressourcen freigibt. Andernfalls bekommt man beim Neustart eine Fehlermeldung:
procedure TForm1.BtnStopClick(Sender: TObject);
begin
BASS_Stop;
BASS_Free; // notwendig, damit BASS neu gestartet werden kann
BtnStart.Enabled := true;
BtnStop.Enabled := false;
BtnClose.Enabled := true;
end; // procedure BtnStopClick
5. Codeeingabe mit dem Ziffernblock der Tastatur
Um für Experimentalzwecke verschiedene Codefolgen bequem wie bei einem Telefon eingeben zu können, werden im Beispielprogramm die Codeziffern über den Ziffernblock der Tastatur eingegeben. Dazu muss die Num-Lock Taste gedrückt sein. Der Num-Modus wird üblicherweise durch eine LED angezeigt. Das könnte man natürlich von Hand machen; weniger fehleranfällig und bequemer ist es allerdings, wenn das Programm beim Start von selbst den entsprechenden Modus setzt. Dazu dient die folgende Prozedur, die eine angepasste Standardlösung aus dem Netz darstellt:
procedure
SetKeyLockMode(Key: Byte; SetOn: Boolean);
var
KS : TKeyboardState;
OnOrOff: Boolean;
begin
GetKeyboardState(KS);
OnOrOff := KS[Key] <> 0;
// Wenn Status vom gewünschten abweicht
if (OnOrOff xor SetOn)
then begin
// Je nach Plattform / Key unterschiedliche Strategien
if (Win32Platform = VER_PLATFORM_WIN32_NT)
or (Key <> vk_NumLock)
then begin
// Tastendruck simulieren
keybd_event(Key, $45, KEYEVENTF_EXTENDEDKEY, 0);
keybd_event(Key, $45, KEYEVENTF_EXTENDEDKEY or
KEYEVENTF_KEYUP, 0);
end // then
else begin
// Gewünschten Status per Setkeyboardstate setzen
KS[Key]:= Ord(SetOn);
SetKeyboardState(KS);
end; // else
end; // then
end; // procedure SetKeyLockMode
In der FormCreate-Prozedur wird die Routine aufgerufen und der NumLock-Modus eingeschaltet:
procedure
TForm1.FormCreate(Sender: TObject);
begin
...
CodeStr := '';
SetKeyLockMode(vk_NumLock,true);
...
end; // procedure FormCreate
In der FormDestroy-Prozedur wird wieder in den Normal-Modus der Tastatur zurückgeschaltet:
procedure
TForm1.FormDestroy(Sender: TObject);
begin
...
SetKeyLockMode(vk_NumLock,false);
end; // procedure FormDestroy
Die Ausgabe der Signaltöne für die 16 verschiedenen Codezeichen wird über eine Reihe globaler Variablen gesteuert, auf die von allen Programmteilen zugegriffen werden kann:
var
Form1: TForm1;
CodeNr : integer;
CodeLength : cardinal;
CodeArr : Array[0..255] of byte;
CodePos : byte;
|
|
Das für die Codefolge vorgesehene Array CodeArr kann bis zu 255 Zeichen aufnehmen. Im Feld Null wird (ähnlich wie bei den alten Pascal-Strings) gespeichert, wie viele Zeichen im Array sind. Die aktuelle Schreib- oder Leseposition wird in der Variablen CodePos festgehalten. Um die Eingabe der Codezeichen kümmert sich die Prozedur FormKeyDown, die als Reaktion auf das Ereignis Tastendruck den Keycode der gedrückten Taste (nicht das ASCII-Zeichen) in der Variablen Key liefert. Die Tasten des Ziffernblocks liefern für die Zifferntasten 0 bis 9 die Keycodes 96 (entspricht der '0') bis 105 (entspricht der '9'). Die Entertaste liefert den Keycode 13. Der Quelltext des Programms steht zum Download bereit. |
Die Reaktion auf das Ereignis KeyDown ermöglicht es uns, das angeschlagene Zeichen zu "sehen", bevor es an eine Eingaberoutine eines anderen Elements auf unserem Formular weitergeleitet wird. Das setzt allerdings voraus, dass die Eigenschaft KeyPreview unseres Formulars Form1 auf true gesetzt wird (was sie standardmäßig nicht ist). Andernfalls bekommt die Prozedur das Tastaturereignis nicht vor dem jeweils aktiven Dialogelement zu sehen.
procedure
TForm1.FormKeyDown(Sender: TObject; var Key: Word;
Shift: TShiftState);
begin
if Key in [13,96,97,98,99,100,101,102,103,104,105]
// #96 = '0' auf dem Zifferblock, wenn NumLock gesetzt ist
// #97 = '1'
// #98 = '2'
// ...
// #105 = '9'
// #13 = Enter
then begin
if Key = 13
then BtnSendClick(Self)
else CodeStr := CodeStr + Char(48+Key-96);
end;
Mit der Enter-Taste kann die BtnSendClick-Prozedur ausgelöst werden, als ob man auf den entsprechenden Button geklickt hätte. Wir können beim DTMF-Verfahren mehr als 10 Codezeichen ansprechen, deshalb müssen die Codes zweistellig eingegeben werden. Für Codes unter 10 werden führende Nullen (also 00 für 0, 01 für 1 usw.) eingetippt. Die Zeichen werden durch Kommata getrennt in einem Edit-Field ausgegeben.
if
Length(CodeStr) = 2 then begin
if Length(Edit2.Text) > 0 then
Edit2.Text := Edit2.Text + ',';
Edit2.Text := Edit2.Text + CodeStr;
CodePos := Succ(CodePos);
CodeArr[CodePos] := StrToInt(CodeStr);
CodeArr[0] := CodePos;
CodeStr := '';
end; // if Length
end; // procedure FormKeyDown
Wenn eine neue Befehlsfolge übertragen werden soll, muss die aktuelle Folge im Array zunächst gelöscht werden:
procedure
TForm1.BtnNewClick(Sender: TObject);
begin
CodePos := 0;
FillChar(CodeArr,SizeOf(CodeArr),0);
Edit2.Text := '';
end; // procedure BtnNewClick
Damit nach der Eingabe die Callback-Routine selbständig mit der Übertragung der eingegebenen Zeichenfolge beginnt, muss lediglich der Schreib-/Lese-Zeiger CodePos auf den Anfang des Arrays gesetzt werden und die Ausgabe gestartet sein (andernfalls wird die Callback-Routine ja nicht angesprochen). Das wird durch einen Klick auf den Send-Button oder wahlweise durch die Enter-Taste ausgelöst:
procedure
TForm1.BtnSendClick(Sender: TObject);
begin
CodePos := 0;
end; // procedure BtnSendClick
Wenn man festgelegte Steuerzeichenfolgen ansprechen will, dann ist es besser, die entsprechende Befehlsfolge dafür festzulegen und durch ein eigenes Ereignis abrufbereit zu halten. Über die DTMF-Schnittstellenplatine können bis zu acht Relais ein- und ausgeschaltet werden. Das ist im Beispielprogramm mit acht Checkbox-Komponenten gelöst. Wird das Häkchen in der Box gesetzt, dann wird das zugehörige Relais eingeschaltet; wird das Häkchen gelöscht, wird das Relais abgeschaltet. Das OnClick-Ereignis löst die entsprechende Aktion aus:
procedure TForm1.Chk1Click(Sender: TObject);
var C : byte;
begin
if Chk1.Checked
then C := 0 // Set
else C := 1; // Reset
BtnNewClick(Self);
CodePos := Succ(CodePos);
CodeArr[CodePos] := C;
CodeArr[0] := CodePos;
BtnSendClick(Self);
end; // procedure Chk1Click
Das ist hier beispielhaft für den Relaiskanal 1 gezeigt. Die anderen Routinen für die weiteren Checkbox-Komponenten wären entsprechend aufgebaut; lediglich die zu übertragenden Codes in der Variablen C müssen angepasst werden. Aber acht OnClick-Ereignishandler mit fast identischem Inhalt sind ziemlich unelegant und blähen den Programmcode unnötig auf. Es geht nämlich auch anders:
procedure
TForm1.Chk1Click(Sender: TObject);
var C : byte;
begin
if (Sender as TCheckBox).Checked
then C := 2*(Sender as TCheckBox).Tag - 2 // Set
else C := 2*(Sender as TCheckBox).Tag - 1; // Reset
BtnNewClick(Self);
CodePos := Succ(CodePos);
CodeArr[CodePos] := C;
CodeArr[0] := CodePos;
BtnSendClick(Self);
end; // procedure Chk1Click
Auch hier wird für das OnClick-Ereignis der CheckBox Chk1 eine Ereignishandling-Prozedur geschrieben. Der as-Operator dient zur Durchführung überprüfter Type-Casts. Wir können damit den Sender-Parameter auswerten, der zu jedem Event-Handler gehört und der die wichtige Information transportiert, welche Komponente das Ereignis ausgelöst hat. Die Variable Sender ist vom Typ TObject und entspricht damit dem unspezifischsten Objekt-Typ in Delphi. Mit dem is-Operator kann man bei Bedarf feststellen, ob das Ereignis von einem bestimmten Komponententyp oder von einem ganz bestimmten Objekt ausgelöst worden ist.
if Sender is
TButton
then begin
…
Da der Sender-Typ unspezifisch ist, muss man die Variable vor der weiteren Verwendung im Programm mit Hilfe des Type-Castings in eine interpretierbare Form bringen. Das leistet der as-Operator, mit dem wir dem Compiler sagen, dass wir Sender als Typ TIrgendwas interpretieren wollen. So können die Eigenschaften des Objekts abgefragt werden:
i := (Sender as TCheckBox).Tag;
Für unseren Zweck können wir die Tag-Eigenschaft von TCheckbox einsetzen. Die Tag-Eigenschaft ist im Basistyp TComponent implementiert, so dass auf Grund der Vererbung jede Delphi-Komponente diese Eigenschaft besitzt. Im Tag können beliebige Integer-Werte gespeichert werden – wir nutzen das, um die Nummer des Relais zu übergeben, dass wir ansprechen wollen. Es eröffnet aber auch komplexere Möglichkeiten: Auch Speicheradressen sind Integer-Werte, so dass man zum Beispiel die Speicheradressen von Strings, Arrays oder Records im Tag ablegen kann – bis hin zu kompletten Objekten. Bei der Anlage der Programmoberfläche legen wir also nicht nur Name und Caption für jede der acht Checkboxes fest, sondern tragen im Objekt-Inspektor auch die Nummern 1 bis 8 in die jeweiligen Tag-Eigenschaften ein. So kann der Ereignis-Handler aus dem Sender-Parameter erkennen, welche Checkbox geklickt wurde und aus der Tag-Eigenschaft errechnen, welches Codezeichen an die DTMF-Schnittstelle gesendet werden muss.
Damit das Programm auf alle acht Checkboxes reagiert, muss die Prozedur Chk1Click nicht nur von der ersten, sondern von allen acht Checkboxes ausgelöst werden. Dazu öffnet man im Objekt-Inspektor jeder Checkbox die Ereignisse-Seite. Beim OnClick-Eintrag öffnet sich ein Auswahl-Fenster, in dem alle bereits im Quelltext formulierten Ereignis-Prozeduren aufgelistet werden, darunter auch unter Chk1Click-Prozedur. Wenn wir diesen Eintrag anklicken, wird er als OnClick-Ereignis für die aktuelle Checkbox eingetragen und zur Lautzeit ausgeführt, sobald die Komponente geklickt wird. Voila!
Download des Programm-Quelltexts
Download dieses Textes als PDF-Datei
(c) Michael Gaedtke, Im Püllenkamp 2, D-41462 Neuss, Germany – michael@gaedtke.name