Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Bezpośrednia synteza przebiegów w generatorach arbitralnych – problemy i przykłady

MD

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:

  1. Rejestr fazy (phase accumulator) – licznik, który w każdej iteracji zwiększa wartość fazy o ustalony krok.
  2. Tablica wzorcowa (lookup table – LUT) – zawiera próbki sygnału (np. wartości sinusa).
  3. 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.

27 maja 2025 26

Kategorie

programowanie

Dziękujemy!
()

Powiązane wpisy


Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.