Bezpośrednia synteza przebiegów w generatorach arbitralnych – problemy i przykłady
Wprowadzenie
Bezpośrednia synteza przebiegów (DDS – Direct Digital Synthesis) jest jedną z podstawowych technik cyfrowego generowania sygnałów. Szczególnie istotna staje się w kontekście tzw. generatorów arbitralnych przebiegów (AWG – Arbitrary Waveform Generator), które umożliwiają tworzenie niemal dowolnych sygnałów czasowych w sposób cyfrowy.
DDS znajduje zastosowanie m.in. w testowaniu układów elektronicznych, systemach radarowych, systemach audio oraz w edukacji. Dzięki elastyczności i precyzji DDS można generować zarówno proste przebiegi (sinusoidy, trójkąty, prostokąty), jak i złożone sekwencje o nieregularnym kształcie.
Zasada działania DDS
Podstawowe elementy DDS to:
- Rejestr fazy (phase accumulator) – licznik, który w każdej iteracji zwiększa wartość fazy o ustalony krok.
- Tablica wzorcowa (lookup table – LUT) – zawiera próbki sygnału (np. wartości sinusa).
- DAC (Digital-to-Analog Converter) – przetwornik C/A, który zamienia dane cyfrowe na sygnał analogowy (w symulacjach często pomijany lub emulowany).
Przy każdym kroku iteracji rejestr fazy wskazuje indeks w tablicy LUT, a odpowiadająca próbka jest zwracana jako aktualna wartość sygnału.
Problemy w implementacji DDS
Mimo prostoty koncepcyjnej, implementacja DDS niesie ze sobą kilka istotnych wyzwań:
1. Aliasing (aliasowanie)
Przy nieodpowiedniej częstotliwości próbkowania mogą pojawiać się artefakty wynikające z aliasowania. Jest to szczególnie ważne przy generowaniu wysokich częstotliwości lub złożonych przebiegów.
2. Ograniczona rozdzielczość fazy
W systemach o niskiej precyzji (np. JavaScript na kliencie) dokładność rejestru fazy może prowadzić do zniekształceń sygnału.
3. Złożoność przebiegów arbitralnych
Generowanie niestandardowych, arbitralnych przebiegów wymaga dynamicznej tablicy LUT lub algorytmu opisującego kształt przebiegu – co komplikuje implementację.
4. Czas przetwarzania / renderowania
W środowiskach wysokopoziomowych, jak przeglądarka internetowa, ważne jest zoptymalizowanie obliczeń, aby uniknąć lagów lub błędów synchronizacji.
Przykłady implementacji DDS w JavaScript
Poniżej przedstawiono prosty generator przebiegów sinusoidalnych z użyciem DDS, działający w przeglądarce.
Przykład 1: Sinusoida generowana cyfrowo
// Parametry DDS
const sampleRate = 44100; // Hz
const frequency = 440; // Hz
const phaseIncrement = (2 * Math.PI * frequency) / sampleRate;
let phase = 0;
// Tworzenie bufora przebiegu sinusoidalnego
function generateSinWave(samples = 1024) {
const buffer = new Float32Array(samples);
for (let i = 0; i < samples; i++) {
buffer[i] = Math.sin(phase);
phase += phaseIncrement;
if (phase >= 2 * Math.PI) phase -= 2 * Math.PI;
}
return buffer;
}
Przykład 2: Generator arbitralny (np. fala piłokształtna)
function generateSawtoothWave(samples = 1024) {
const buffer = new Float32Array(samples);
let value = -1;
const step = 2 / samples;
for (let i = 0; i < samples; i++) {
buffer[i] = value;
value += step;
}
return buffer;
}
Przykład 3: Renderowanie sygnału na kanwie HTML
<canvas id="waveform" width="800" height="200"></canvas>
<script>
const canvas = document.getElementById("waveform");
const ctx = canvas.getContext("2d");
const data = generateSinWave(800);
ctx.beginPath();
ctx.moveTo(0, 100);
for (let i = 0; i < data.length; i++) {
let y = 100 - data[i] * 90; // Skalowanie
ctx.lineTo(i, y);
}
ctx.stroke();
</script>
Fala sinusoidalna wygenerowana cyfrowo
Integracja DDS z Web Audio API w JavaScript
Web Audio API to potężne narzędzie dostępne w przeglądarkach, umożliwiające tworzenie, manipulowanie i odtwarzanie dźwięków w czasie rzeczywistym. W kontekście DDS można go użyć jako przetwornika DAC — zamiast tylko wyświetlać wykres, możemy przesłać cyfrowo wygenerowane próbki do silnika audio.
Przykład: Generator fali sinusoidalnej z użyciem Web Audio API
1. Konfiguracja i inicjalizacja
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const sampleRate = audioCtx.sampleRate; // zwykle 44100 Hz
2. Funkcja DDS dla sinusa
function generateDDSBuffer(frequency, durationSeconds = 1) {
const sampleCount = Math.floor(sampleRate * durationSeconds);
const buffer = audioCtx.createBuffer(1, sampleCount, sampleRate);
const data = buffer.getChannelData(0);
const phaseIncrement = (2 * Math.PI * frequency) / sampleRate;
let phase = 0;
for (let i = 0; i < sampleCount; i++) {
data[i] = Math.sin(phase);
phase += phaseIncrement;
if (phase >= 2 * Math.PI) phase -= 2 * Math.PI;
}
return buffer;
}
3. Odtwarzanie wygenerowanego sygnału
function playBuffer(buffer) {
const source = audioCtx.createBufferSource();
source.buffer = buffer;
source.connect(audioCtx.destination);
source.start();
}
4. Przykład użycia – generowanie tonu 440 Hz (A4)
const buffer = generateDDSBuffer(440, 2); // 2 sekundy tonu A4
playBuffer(buffer);
Generator przebiegów arbitralnych (np. piła, prostokąt, trójkąt)
Możemy zmodyfikować funkcję generateDDSBuffer
, aby akceptowała dowolną funkcję falową:
function generateArbitraryBuffer(frequency, waveformFn, durationSeconds = 1) {
const sampleCount = Math.floor(sampleRate * durationSeconds);
const buffer = audioCtx.createBuffer(1, sampleCount, sampleRate);
const data = buffer.getChannelData(0);
const phaseIncrement = frequency / sampleRate;
let phase = 0;
for (let i = 0; i < sampleCount; i++) {
data[i] = waveformFn(phase);
phase += phaseIncrement;
if (phase >= 1) phase -= 1;
}
return buffer;
}
Przykłady przebiegów:
// Sawtooth wave: od -1 do 1
const sawtooth = phase => 2 * phase - 1;
// Square wave: -1 lub 1
const square = phase => (phase < 0.5 ? 1 : -1);
// Triangle wave
const triangle = phase => 1 - 4 * Math.abs(phase - 0.5);
Odtwarzanie fali piłokształtnej 220 Hz
const buffer = generateArbitraryBuffer(220, sawtooth, 2);
playBuffer(buffer);
Możliwości rozbudowy
Z pomocą Web Audio API można także:
- Dodawać filtry (np. dolnoprzepustowe dla antyaliasingu),
- Tworzyć interaktywne GUI do wyboru przebiegu i częstotliwości,
- Łączyć DDS z oscylatorami wbudowanymi w API,
- Generować mieszanki sygnałów (np. modulację AM/FM),
- Nagrywać wygenerowane przebiegi do plików WAV.
Wnioski z integracji
Integracja DDS z Web Audio API otwiera ogromne możliwości w edukacji, testowaniu DSP oraz prototypowaniu systemów dźwiękowych. Dzięki prostocie JavaScript i wsparciu nowoczesnych przeglądarek, można stworzyć kompletny generator przebiegów arbitralnych online, który nie tylko generuje dane, ale też odtwarza je na żywo.
Minimalizacja zniekształceń harmonicznych
Zajmijmy się teraz zminimalizowaniem słyszalnych harmonicznych podczas odtwarzania fali sinusoidalnej generowanej metodą DDS. Oznacza to, że musimy zastosować interpolację lub filtrację w celu wygładzenia sygnału, ponieważ cyfrowo próbkowany sygnał zawiera zniekształcenia (aliasing, nieliniowość) szczególnie słyszalne przy prostym DDS.
Problem: Zniekształcenia i harmoniczne
Metoda DDS, jak pokazaliśmy wcześniej, generuje próbki sinusa z LUT lub Math.sin(phase)
, ale:
- Z powodu ograniczonego kroku fazowego (
phaseIncrement
) sygnał może być "schodkowany", - Wysokie częstotliwości (np. powyżej 3–4 kHz) mogą być zniekształcone, bo nie ma antyaliasingu,
- Brakuje interpolacji między próbkami, co skutkuje obecnością dodatkowych harmonicznych, słyszalnych jako "brudny" ton.
Rozwiązanie: interpolacja i filtracja
Cele:
- Wygenerować próbki z większą precyzją (interpolacja liniowa lub sin(x)/x),
- Wygładzić sygnał za pomocą filtru dolnoprzepustowego (low-pass filter).
Interpolacja liniowa między próbkami LUT
W tym przykładzie wykorzystujemy tablicę LUT z sinusem i interpolację między dwoma sąsiednimi próbkami.
1. Tworzenie LUT z próbkami sinusa
const LUT_SIZE = 2048;
const sinLUT = new Float32Array(LUT_SIZE);
for (let i = 0; i < LUT_SIZE; i++) {
sinLUT[i] = Math.sin((2 * Math.PI * i) / LUT_SIZE);
}
2. Generowanie przebiegu z interpolacją liniową
function generateInterpolatedSinBuffer(frequency, durationSeconds = 1) {
const sampleCount = Math.floor(sampleRate * durationSeconds);
const buffer = audioCtx.createBuffer(1, sampleCount, sampleRate);
const data = buffer.getChannelData(0);
let phase = 0;
const phaseIncrement = (frequency * LUT_SIZE) / sampleRate;
for (let i = 0; i < sampleCount; i++) {
const index = Math.floor(phase);
const nextIndex = (index + 1) % LUT_SIZE;
const frac = phase - index;
// interpolacja liniowa
const sample =
sinLUT[index] * (1 - frac) + sinLUT[nextIndex] * frac;
data[i] = sample;
phase += phaseIncrement;
if (phase >= LUT_SIZE) phase -= LUT_SIZE;
}
return buffer;
}
Opcjonalne wygładzenie: filtr dolnoprzepustowy (LPF)
Możesz dodać biquad filter z Web Audio API, aby odciąć wysokie harmoniczne:
function playFilteredBuffer(buffer, cutoffHz = 8000) {
const source = audioCtx.createBufferSource();
source.buffer = buffer;
const filter = audioCtx.createBiquadFilter();
filter.type = "lowpass";
filter.frequency.value = cutoffHz;
filter.Q.value = 0.707; // typowe ustawienie Butterwortha
source.connect(filter).connect(audioCtx.destination);
source.start();
}
Przykład użycia: sinus 440 Hz z interpolacją i filtrem
const buffer = generateInterpolatedSinBuffer(440, 2);
playFilteredBuffer(buffer, 5000); // filtrujemy powyżej 5 kHz
Alternatywa: interpolacja sin(x)/x (sinc) – precyzyjniejsza, ale cięższa obliczeniowo
Interpolacja sinc lepiej eliminuje aliasowanie, ale jest bardziej złożona i zwykle stosowana offline lub z FFT. W aplikacjach przeglądarkowych interpolacja liniowa + filtr LPF daje zadowalające brzmienie przy minimalnych kosztach CPU.
Wnioski z minimalizacji zniekształceń
Technika | Co daje | Koszt |
---|---|---|
LUT bez interpolacji | szybka, ale pełna zniekształceń | niski |
LUT + interpolacja liniowa | gładszy dźwięk, mniej harmonicznych | niski |
LUT + filtr LPF | jeszcze lepsza jakość | umiarkowany |
Interpolacja sinc | idealna jakość | wysoki |
Dzięki interpolacji oraz filtracji można znacząco poprawić jakość dźwięku DDS nawet w środowisku JavaScript. To podejście idealnie nadaje się do interaktywnych aplikacji edukacyjnych i testowych działających w przeglądarce.
Prosty generator DDS na Arduino (sinusoida przez PWM)
Cel:
- Wygenerować sygnał sinusoidalny za pomocą DDS.
- Użyć tablicy LUT do przechowywania wartości sinusoidalnych.
- Użyć PWM (np.
analogWrite()
na pinie) jako wyjścia DAC. - Umożliwić zmianę częstotliwości.
Wymagania sprzętowe
- Płytka Arduino Uno/Nano
- Pin PWM (np. D9)
- (Opcjonalnie) Filtr dolnoprzepustowy RC do wygładzenia sygnału z PWM
Kod źródłowy Arduino
// Parametry LUT
#define TABLE_SIZE 256
const uint8_t sineTable[TABLE_SIZE] PROGMEM = {
128,131,134,137,140,143,146,149,152,156,159,162,165,168,171,174,
177,180,183,186,189,191,194,197,200,202,205,207,210,212,215,217,
219,222,224,226,228,230,232,234,236,237,239,240,242,243,245,246,
247,248,249,250,251,252,252,253,253,254,254,254,254,254,254,254,
253,253,252,252,251,250,249,248,247,246,245,243,242,240,239,237,
236,234,232,230,228,226,224,222,219,217,215,212,210,207,205,202,
200,197,194,191,189,186,183,180,177,174,171,168,165,162,159,156,
152,149,146,143,140,137,134,131,128,125,122,119,116,113,110,107,
104,100, 97, 94, 91, 88, 85, 82, 79, 76, 73, 70, 67, 65, 62, 59,
56, 54, 51, 49, 46, 44, 41, 39, 37, 34, 32, 30, 28, 26, 24, 22,
20, 19, 17, 16, 14, 13, 11, 10, 9, 8, 7, 6, 5, 4, 4, 3,
3, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8,
9, 10, 11, 13, 14, 16, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34,
37, 39, 41, 44, 46, 49, 51, 54, 56, 59, 62, 65, 67, 70, 73, 76,
79, 82, 85, 88, 91, 94, 97,100,104,107,110,113,116,119,122,125
};
// DDS zmienne
volatile uint32_t phaseAcc = 0;
uint32_t phaseInc = 0;
// Parametry
const uint16_t freq = 440; // Hz
const uint16_t sampleRate = 31372; // Hz (Timer2 @ prescaler 1, OCR2A = 255)
// Timer2 ISR
ISR(TIMER2_COMPA_vect) {
phaseAcc += phaseInc;
uint8_t index = phaseAcc >> 24; // ucinamy do 8 bitów
uint8_t sample = pgm_read_byte_near(sineTable + index);
analogWrite(9, sample); // PWM na pinie D9
}
void setup() {
pinMode(9, OUTPUT);
// Oblicz krok fazy
phaseInc = ((uint64_t)freq << 32) / sampleRate;
// Timer2 konfiguracja
TCCR2A = _BV(WGM21); // CTC mode
TCCR2B = _BV(CS20); // prescaler = 1
OCR2A = 255; // próbkowanie przy ~31.3kHz
TIMSK2 = _BV(OCIE2A); // przerwanie
}
void loop() {
// nic nie robimy – wszystko obsługuje ISR
}
Wyjaśnienie
- DDS: wykorzystuje zmienną
phaseAcc
jako licznik fazy. Przesuwając o 24 bity w prawo uzyskujemy indeks LUT (256 próbek). - Timer2 ISR: ustawia próbki PWM na pinie D9 z częstotliwością próbkowania ~31.3 kHz.
- PWM: traktujemy jako 8-bitowy DAC. Z zewnętrznym filtrem RC daje użyteczny sygnał analogowy.
Uwaga: filtr RC
PWM wymaga prostego filtru dolnoprzepustowego (np. R=10k, C=0.1µF) do wygładzenia przebiegu i uzyskania analogowego sygnału.
Efekt
- Sinusoidalny sygnał 440 Hz na wyjściu D9
- Z możliwością zmiany częstotliwości przez
freq
- Realna jakość dźwięku z PWM + filtr RC
Arduino z drabinką R-2R
Przejdźmy do wersji DDS z drabinką rezystorową R-2R, która działa jako cyfrowy przetwornik DAC (Digital-to-Analog Converter). To znakomite rozwiązanie, jeśli chcesz uzyskać wysokiej jakości sygnał analogowy z ośmiu pinów cyfrowych Arduino, bez użycia zewnętrznego układu DAC lub PWM.
Co to jest drabinka R-2R?
Drabinka R-2R to bardzo prosty w budowie, pasywny przetwornik cyfrowo-analogowy. Wystarczy zestaw rezystorów w konfiguracji R i 2R (np. R = 10kΩ, 2R = 20kΩ), podłączonych do 8 pinów cyfrowych Arduino, reprezentujących bity od D0 (LSB) do D7 (MSB). Końcówka drabinki daje napięcie proporcjonalne do liczby binarnej ustawionej na pinach.
DDS + R-2R DAC – pełny przykład
Połączenia:
- 8 wyjść cyfrowych Arduino (np. D2–D9) → rezystory → drabinka R-2R → wyjście analogowe
- Drabinka zakończona do masy i punkt wyjściowy (analogowy)
- Rezystory 1% (np. R = 10k, 2R = 20k)
Kod źródłowy
// DDS z 8-bitową drabinką R-2R (D2–D9)
// LUT – 256 próbek sinusa (8-bit, 0–255)
#define TABLE_SIZE 256
const uint8_t sineTable[TABLE_SIZE] PROGMEM = {
// ... identyczna tablica jak wcześniej ...
128,131,134,137,140,143,146,149,152,156,159,162,165,168,171,174,
177,180,183,186,189,191,194,197,200,202,205,207,210,212,215,217,
219,222,224,226,228,230,232,234,236,237,239,240,242,243,245,246,
247,248,249,250,251,252,252,253,253,254,254,254,254,254,254,254,
253,253,252,252,251,250,249,248,247,246,245,243,242,240,239,237,
236,234,232,230,228,226,224,222,219,217,215,212,210,207,205,202,
200,197,194,191,189,186,183,180,177,174,171,168,165,162,159,156,
152,149,146,143,140,137,134,131,128,125,122,119,116,113,110,107,
104,100, 97, 94, 91, 88, 85, 82, 79, 76, 73, 70, 67, 65, 62, 59,
56, 54, 51, 49, 46, 44, 41, 39, 37, 34, 32, 30, 28, 26, 24, 22,
20, 19, 17, 16, 14, 13, 11, 10, 9, 8, 7, 6, 5, 4, 4, 3,
3, 2, 2, 2, 2, 2, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8,
9, 10, 11, 13, 14, 16, 17, 19, 20, 22, 24, 26, 28, 30, 32, 34,
37, 39, 41, 44, 46, 49, 51, 54, 56, 59, 62, 65, 67, 70, 73, 76,
79, 82, 85, 88, 91, 94, 97,100,104,107,110,113,116,119,122,125
};
// Faza DDS
volatile uint32_t phaseAcc = 0;
uint32_t phaseInc = 0;
const uint16_t freq = 440; // Hz
const uint16_t sampleRate = 31372; // Hz (Timer2)
const uint8_t outputPins[8] = {2, 3, 4, 5, 6, 7, 8, 9}; // LSB→MSB
ISR(TIMER2_COMPA_vect) {
phaseAcc += phaseInc;
uint8_t sample = pgm_read_byte_near(sineTable + (phaseAcc >> 24));
// ustawienie 8 bitów na pinach D2–D9
for (uint8_t i = 0; i < 8; i++) {
digitalWrite(outputPins[i], (sample >> i) & 0x01);
}
}
void setup() {
// Ustaw piny jako wyjścia
for (uint8_t i = 0; i < 8; i++) {
pinMode(outputPins[i], OUTPUT);
}
// Oblicz krok fazy
phaseInc = ((uint64_t)freq << 32) / sampleRate;
// Timer2 CTC @ ~31.3kHz
TCCR2A = _BV(WGM21);
TCCR2B = _BV(CS20); // prescaler 1
OCR2A = 255;
TIMSK2 = _BV(OCIE2A);
}
void loop() {
// wszystko obsługiwane w ISR
}
Co się dzieje:
- W ISR Timer2 co 32 µs (31.3 kHz) generujemy próbkę sinusa.
- Wartość 8-bitowa jest wypisywana bit po bicie na 8 wyjść cyfrowych.
- Drabinka R-2R sumuje te napięcia w sposób proporcjonalny do wartości binarnej.
- Na wyjściu R-2R uzyskujemy czysty, analogowy sygnał sinusoidalny.
Opcje rozbudowy:
- Przełączanie typów przebiegów: sinus, piła, prostokąt (zmiana LUT)
- Dynamiczna zmiana częstotliwości przez potencjometr (ADC)
- Wyjście stereo (np. 2x R-2R)
- Interpolacja między próbkami LUT dla jeszcze lepszej jakości
Podsumowanie
Bezpośrednia synteza przebiegów w generatorach arbitralnych to potężne narzędzie pozwalające na cyfrowe modelowanie różnorodnych sygnałów. Pomimo wyzwań technicznych (aliasowanie, rozdzielczość, wydajność), DDS może być skutecznie implementowany nawet w środowiskach takich jak JavaScript.
Dzięki rosnącej mocy obliczeniowej przeglądarek i dostępności Web Audio API, możliwe jest tworzenie w pełni funkcjonalnych generatorów sygnałów bezpośrednio w przeglądarce, co otwiera nowe możliwości w edukacji, prototypowaniu oraz testowaniu systemów DSP.
Zapoznaj się z moją aplikacją online do generowania przebiegów arbitralnych w paśmie akustycznym.