Przejdź do głównej treści

Generator przebiegów arbitralnych

MD

Stwórz własny dźwięk od podstaw!

Poznaj interaktywny generator przebiegów arbitralnych, który daje Ci pełną kontrolę nad kształtem fali dźwiękowej. To więcej niż zwykły generator – to narzędzie do kreatywnej eksploracji dźwięku:

Rysuj własne fale – intuicyjnie myszką, dokładnie tak, jak je sobie wyobrażasz.
Zobacz, jak brzmi – od razu sprawdź wykres LUT i usłysz efekt na żywo.
Gotowe kształty – sinus, trójkąt, prostokąt, piła – na start i dla porównania.
Złóż falę z harmonicznych - dowolny kształt fali na bazie harmonicznych
Natychmiastowe odtwarzanie – Twoje przebiegi generują realny dźwięk w czasie rzeczywistym.

Doskonałe narzędzie dla:

  • muzyków i producentów – do kreowania niestandardowych brzmień,
  • studentów – do nauki o dźwięku, przebiegach i sygnałach,
  • pasjonatów elektroniki i DSP – do eksperymentów z generacją sygnału.

Zero instalacji. Działa od razu w przeglądarce.

Wypróbuj i stwórz dźwięk, jakiego jeszcze nie było!

Rozwinięcia przebiegów w szereg Fouriera

1. Przebieg prostokątny (fala prostokątna)

Zakładamy sygnał okresowy o okresie \( T \), wartości \( +A \) w przedziale \( (0, T/2) \), wartości \( -A \) w \( (-T/2, 0) \).

Rozwinięcie w szereg Fouriera (tylko wyrazy nieparzyste):

$$ f(t) = \frac{4A}{\pi} \sum_{n=1,3,5,\dots}^{\infty} \frac{1}{n} \sin\left( \frac{2\pi n t}{T} \right) $$

2. Przebieg piłokształtny (fala piłokształtna)

Zakładamy sygnał okresowy o okresie \( T \), narastający liniowo od \( -A \) do \( A \).

Rozwinięcie:

$$ f(t) = \frac{-2A}{\pi} \sum_{n=1}^{\infty} \frac{(-1)^n}{n} \sin\left( \frac{2\pi n t}{T} \right) $$

3. Przebieg trójkątny (fala trójkątna)

Zakładamy okres \( T \), sygnał rosnący liniowo od \( 0 \) do \( A \), a potem malejący do \( 0 \), potem do \( -A \) itd.

Rozwinięcie:

$$ f(t) = \frac{8A}{\pi^2} \sum_{n=1,3,5,\dots}^{\infty} \frac{(-1)^{(n-1)/2}}{n^2} \sin\left( \frac{2\pi n t}{T} \right) $$

4. Ząbkowany (napięcie zębate, „sawtooth” – inna wersja)

Jeśli zaczyna się od zera i rośnie liniowo do \( A \), a potem nagle spada do zera:

$$ f(t) = \frac{A}{2} - \frac{A}{\pi} \sum_{n=1}^{\infty} \frac{1}{n} \sin\left( \frac{2\pi n t}{T} \right) $$

5. Sygnał prostokątny niesymetryczny (współczynnik wypełnienia D)

Jeśli sygnał ma wartość \( A \) przez czas \( DT \), a \( 0 \) przez \( (1-D)T \):

$$ f(t) = A D + \sum_{n=1}^{\infty} \frac{2A}{n\pi} \sin(n\pi D) \cos\left( \frac{2\pi n t}{T} \right) $$

6. Sygnał impulsowy (Dirac comb – ciąg impulsów)

Sygnał złożony z delta Diraców co \( T \):

$$ \sum_{n=-\infty}^{\infty} \delta(t - nT) \quad \Rightarrow \quad \text{Fourier: } \frac{1}{T} \sum_{n=-\infty}^{\infty} e^{j 2\pi n f_0 t}, \quad f_0 = \frac{1}{T} $$

Modulacja przebiegu

Modulacja amplitudy (AM)

Formuła AM:

$$ s_{AM}(t) = [1 + m \cdot \sin(2\pi f_m t)] \cdot \sin(2\pi f_c t) $$

Gdzie:

  • \( s_{AM}(t) \) – sygnał zmodulowany amplitudowo,
  • \( m \) – głębokość modulacji (modulation index, \( 0 \leq m \leq 1 \)),
  • \( f_m \) – częstotliwość sygnału modulującego,
  • \( f_c \) – częstotliwość nośna.

Modulacja częstotliwości (FM)

Formuła FM:

$$ s_{FM}(t) = A \cdot \sin\left(2\pi f_c t + \beta \cdot \sin(2\pi f_m t)\right) $$

Gdzie:

  • \( s_{FM}(t) \) sygnał zmodulowany częstotliwościowo,
  • \( A \) – amplituda sygnału (stała),
  • \( f_c \) – częstotliwość nośna (carrier),
  • \( f_m \) – częstotliwość modulująca (modulator),
  • \( \beta \) – indeks modulacji:

    $$ \beta = \frac{\Delta f}{f_m} $$

gdzie \( \Delta f \) to maksymalne odchylenie częstotliwości nośnej.

Modulacja dwuwstęgowa (DSB)

DSB (Double Sideband) to forma modulacji amplitudy, w której:

  • nośna jest mnożona przez sygnał modulujący,
  • brak dodatkowej nośnej (w przeciwieństwie do klasycznej AM).

Formuła DSB:

$$ s(t) = m(t) \cdot \cos(2\pi f_c t) $$

Gdzie:

  • \( m(t) \) – sygnał modulujący (np. sinus, piła itp.)
  • \( f_c \) – częstotliwość nośna (carrier)

Zniekształcenia harmoniczne

Co to jest THD?

THD [%] to stosunek całkowitej mocy harmonicznych (bez podstawowej) do mocy harmonicznej podstawowej, zwykle wyrażony w procentach:

$$ \text{THD} = \frac{\sqrt{V_2^2 + V_3^2 + \dots + V_n^2}}{V_1} \cdot 100\% $$

Typowe zniekształcenia sygnałów audio – tabela porównawcza

Rodzaj zniekształcenia Opis efektu Charakterystyka zniekształcenia Szacunkowy THD Uwagi
Clipping symetryczny Obcięcie sygnału po obu stronach (np. wzmacniacz tranzystorowy bez zapasu) Twarda nieliniowość 30–60% Bogate w nieparzyste harmoniczne
Clipping asymetryczny Obcięcie tylko jednej strony (np. taśma bez prądu podkładu) Nieliniowość niesymetryczna 40–80% Silna dominacja parzystych harmonicznych
Tape saturation (taśma + AC bias) Zaokrąglona nieliniowość przy większych amplitudach Miękka saturacja (symetryczna) 5–15% Typowa dla analogowych rejestratorów
Tape without bias (DC) Obcięcie jednej strony sinusa – silnie asymetryczne Nasycenie jednostronne 40–70% Sygnał brzmi „zatkany”, słabo słyszalny
Magnetic core saturation (trafo) Miękkie ograniczenie amplitudy przy wysokich poziomach Zniekształcenia symetryczne lub asymetryczne 10–30% Zależne od typu rdzenia
Overdrive (lampa) Zaokrąglona saturacja z dominacją niższych harmonicznych Miękka nieliniowość, lekko asymetryczna 5–20% Dźwięk „ciepły”, bogaty
Soft limiting (kompresja) Redukcja dynamiki przy wysokim poziomie Łagodna nieliniowość 5–15% Efekt „loudness”, bez dużej utraty barwy
Diode clipping (np. fuzz) Twarde obcięcie przez diody (np. 0.7V) Symetryczne lub asymetryczne 40–80% Bardzo agresywny, używany w efektach gitarowych
Nonlinear transfer (np. tanh) Łagodne zaokrąglenie amplitudy Nieliniowość hiperb. 10–30% Używane w cyfrowych symulacjach „taśmy”
Zapis z przesuniętym biasem DC Stałe przesunięcie punktu pracy na taśmie → asymetria Zniekształcenie jednostronne 30–60% Daje sygnał „przesunięty”

Jak obsługiwać aplikację?

  • ustaw ilość próbek, częstotliwość i amplitudę
  • wybierz sposób generowania przebiegu klikając na odpowiednią zakładkę
  • uaktualnij LUT klikając przycisk Generuj LUT
  • odtwórz dźwięk klikając przycisk
  • przy każdej zmianie przebiegu uaktualnij LUT przyciskiem Generuj LUT
  • zakończenie rysowania przebiegu uaktualni LUT automatycznie

Jak obserwować przebiegi?

Podłącz dowolny oscyloskop do wyjścia słuchawkowego swojego smartfona i kliknij przycisk Odtwórz po wygenerowaniu LUT. Na swoim oskopie zobaczysz przebieg odpowiadający wygenerowanemu tutaj przebiegowi. Obraz poniżej przedstawia kilka wygenerowanych przebiegów: sinusoidalny, piłokształtny, modulacja AM, modulacja FM, łagodne asymetryczne obcinanie, przebieg składany z harmonicznych.

Obraz Oscyloskop
Przebiegi z generatora arbitralnego

Kliknij Generuj LUT po każdej zmianie w polach powyżej.

Nr harmonicznej Amplituda Akcje

Kliknij Generuj LUT po każdej zmianie w polach powyżej.

Narysuj jeden okres przebiegu (kliknij i przeciągnij):
Lewy brzeg = początek okresu, prawy = koniec. Góra = 1, dół = -1.

LUT zostanie uaktualniony po zakończeniu rysowania.

Kod po stronie przeglądarki

<style>
    textarea {
      font-family: monospace;
    }
    canvas#drawCanvas {
      border: 1px solid #ccc;
      background: #f9f9f9;
      cursor: crosshair;
    }
</style>
<div id="app" class="container">
    <div class="row my-3">
        <div class="col-md-4 mb-3">
            <label for="sampleCount" class="form-label">Próbki</label>
            <input type="number" id="sampleCount" class="form-control" value="64" min="8" max="512">
        </div>
        <div class="col-md-4 mb-3">
            <label for="frequency" class="form-label">Częstotliwość (Hz)</label>
            <input type="number" id="frequency" class="form-control" value="440" min="20" max="2000">
        </div>
        <div class="col-md-4 mb-3">
            <label for="amplitude" class="form-label">Amplituda</label>
            <input type="number" id="amplitude" class="form-control" value="1" step="0.1" min="0" max="2">
        </div>
    </div>

    <ul class="nav nav-tabs" id="modeTabs" role="tablist">
        <li class="nav-item">
            <button id="btnPresetTab" class="nav-link active" data-bs-toggle="tab" data-bs-target="#preset" type="button">Gotowe przebiegi</button>
        </li>
        <li class="nav-item">
            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#harmonics" type="button">Harmoniczne</button>            
        </li>
        <li class="nav-item">
            <button class="nav-link" data-bs-toggle="tab" data-bs-target="#custom" type="button">Rysuj własny</button>
        </li>
    </ul>
    <div class="tab-content mt-3">
        <div class="tab-pane fade show active" id="preset">
            <div class="row mb-3">
                <div class="col-lg-3 mb-3">
                    <label for="waveformType" class="form-label">Typ przebiegu</label>
                    <select class="form-select" id="waveformType">
                        <option value="sine">sinusoidalny</option>
                        <option value="triangle">trójkątny</option>
                        <option value="square">prostokątny</option>
                        <option value="sawtooth">piłokształtny</option>
                        <option value="clipped_sine">przycięty sinus</option>
                        <option value="noise">szum</option>
                        <option value="impulse_train">ciąg impulsów</option>
                        <option value="burst">impuls periodyczny</option>
                        <option value="am">AM (modulacja amplitudy)</option>
                        <option value="fm">FM (modulacja częstot.)</option>
                        <option value="dsb">DSB (modulacja dwuwstęg.)</option>
                        <option value="pwm">PWM (wypełnienie)</option>
                    </select>
                </div>
                <div class="col-lg-3 mb-3">
                    <label for="modType" class="form-label">Sygnał modulujący</label>
                    <select id="modType" class="form-select" disabled>
                        <option value="sin">sinus</option>
                        <option value="triangle">trójkąt</option>
                        <option value="square">prostokąt</option>
                        <option value="sawtooth">piła</option>
                    </select>
                </div>
                <div class="col-lg-3 mb-3">
                    <label for="modIndex" class="form-label">Indeks modulacji / Wsp. wyp.</label>
                    <input type="number" id="modIndex" class="form-control" value="0.5" step="0.1" min="0" max="1" disabled>
                </div>
                <div class="col-lg-3 mb-3">
                    <label for="distortion" class="form-label">Zniekształcenie</label>
                    <select id="distortion" class="form-select">
                        <option value="none">Brak</option>
                        <option value="clip">Obcinanie</option>
                        <option value="halfwave">Półfalowe</option>
                        <option value="fullwave">Pełnofalowe</option>
                        <option value="no_bias_tape">Taśma bez podkładu</option>
                        <option value="dc_bias_tape">Taśma prąd podkładu DC</option>
                        <option value="ac_bias_tape">Taśma prąd podkładu AC</option>
                        <option value="trafo_saturation">Nasycenie transformatora</option>
                    </select>
                </div>
                <div class="col-md-3 mb-3">
                    <button class="btn btn-primary" onclick="generateLUT()"><i class="bi bi-cpu"></i> Generuj LUT</button>
                </div>
                <div class="col-md-12 mb-3">
                    <div class="alert alert-warning">
                        <p class="mb-0">Kliknij Generuj LUT po każdej zmianie w polach powyżej.</p>
                    </div>
                </div>
            </div>
        </div>
        <div class="tab-pane fade" id="harmonics" role="tabpanel">
            <div class="mt-3">
                <table class="table table-sm table-bordered align-middle" id="harmonicsTable">
                    <thead>
                        <tr>
                            <th>Nr harmonicznej</th>
                            <th>Amplituda</th>
                            <th>Akcje</th>
                        </tr>
                    </thead>
                    <tbody>
                        <!-- Wiersze będą dodawane dynamicznie -->
                    </tbody>
                </table>
                <button class="btn btn-success btn-sm mb-3" onclick="addHarmonicRow()"><i class="bi bi-plus-circle"></i> Dodaj harmoniczną</button>
                <button class="btn btn-primary btn-sm mb-3" onclick="generateFromHarmonics()"><i class="bi bi-cpu"></i> Generuj LUT</button>
                <div class="alert alert-warning">
                    <p class="mb-0">Kliknij Generuj LUT po każdej zmianie w polach powyżej.</p>
                </div>
            </div>
        </div>
        <div class="tab-pane fade" id="custom">
            <div class="mb-3">
                <div class="mb-2">Narysuj jeden okres przebiegu (kliknij i przeciągnij):</div>
                <canvas id="drawCanvas" width="310" height="150"></canvas>
                <div class="form-text">Lewy brzeg = początek okresu, prawy = koniec. Góra = 1, dół = -1.</div>
            </div>
            <div class="mb-3">
                <div class="alert alert-info">
                    <p class="mb-0">LUT zostanie uaktualniony po zakończeniu rysowania.</p>
                </div>
            </div>
        </div>
    </div>

    <label for="lutOutput" class="form-label">LUT (Look-Up Table)</label>
    <textarea id="lutOutput" class="form-control mb-3" rows="3" readonly></textarea>
    <canvas id="lutChart" height="100"></canvas>
    <canvas id="fftChart" height="100"></canvas>

    <div class="mb-3" id="dcInfo">
    </div>

    <div class="mb-3" id="thdInfo">
    </div>

    <div class="mb-3">
        <button class="btn btn-success me-2 mb-2" onclick="playLUT()"><i class="bi bi-play-fill"></i> Odtwórz</button>
        <button class="btn btn-danger me-2 mb-2" onclick="stopSound()"><i class="bi bi-stop-fill"></i> Zatrzymaj</button>
        <button class="btn btn-warning me-2 mb-2" onclick="exportWAV()"><i class="bi bi-file-earmark-music"></i> Zapisz do WAV</button>
        <button class="btn btn-info me-2 mb-2" onclick="exportLUT()"><i class="bi bi-box-arrow-up"></i> Eksportuj LUT</button>
        <input type="file" id="importLUTInput" accept=".json" hidden onchange="importLUT(event)" aria-label="Wybierz plik JSON">
        <button class="btn btn-info me-2 mb-2" onclick="document.getElementById('importLUTInput').click()"><i class="bi bi-box-arrow-in-down"></i> Importuj LUT</button>
    </div>

</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
    //variables
    let audioCtx;
    let sourceNode;
    let chart;
    let currentSamples = [];

    const canvas = document.getElementById('drawCanvas');
    const ctx = canvas.getContext('2d');
    let drawing = false;
    let drawnPoints = [];
    let lastX = null,
        lastY = null;

    var chartFFT;

    function dft(signal) {
        const X = [];
        const N = signal.length;
        for (let k = 0; k < N; k++) {
            let re = 0,
                im = 0;
            for (let n = 0; n < N; n++) {
                const angle = -2 * Math.PI * k * n / N;
                re += signal[n] * Math.cos(angle);
                im += signal[n] * Math.sin(angle);
            }
            X.push({
                re,
                im
            });
        }
        return X;
    }

    function calculateTHD(magnitudes) {
        const fundamental = magnitudes[1]; // Pomijamy DC (magnitudes[0])
        const harmonics = magnitudes.slice(2); // od 2. harmonicznej wzwyż

        const harmonicPower = harmonics.reduce((sum, val) => sum + val ** 2, 0);
        const thd = Math.sqrt(harmonicPower) / fundamental;

        return thd * 100;
    }

    function generateLUT() {
        const type = document.getElementById('waveformType').value;
        const count = parseInt(document.getElementById('sampleCount').value);
        const modIndex = parseFloat(document.getElementById('modIndex').value);
        const distortion = document.getElementById('distortion')?.value || 'none';
        const samples = [];
        const fc = 5; // nośna
        const fm = 1; // modulująca

        for (let i = 0; i < count; i++) {
            const t = i / count;
            let val = 0;

            // wybór typu sygnału modulującego
            const mType = document.getElementById('modType').value;
            switch (mType) {
                case 'sin':
                    modulator = Math.sin(2 * Math.PI * fm * t);
                    break;
                case 'triangle':
                    modulator = 1 - 4 * Math.abs(Math.round(t - 0.25) - (t - 0.25));
                    break;
                case 'square':
                    modulator = (t % 1) < 0.5 ? 1 : -1;
                    break;
                case 'sawtooth':
                    modulator = 1 - 2 * (t % 1);
                    break;
            }

            switch (type) {
                case 'sine':
                    val = Math.sin(2 * Math.PI * t);
                    // Zastosuj zniekształcenie jeśli wybrane
                    switch (distortion) {
                        case 'clip':
                            val = Math.max(-0.5, Math.min(0.5, val)); // twarde obcinanie
                            break;
                        case 'halfwave':
                            val = Math.max(0, val); // tylko dodatnia część
                            break;
                        case 'fullwave':
                            val = Math.abs(val); // całość dodatnia
                            break;
                        case 'no_bias_tape':
                            // Efekt jak przy braku prądu podkładu: asymetryczne ograniczenie
                            val = val >= 0 ? 0.9 * val : 0.3 * Math.tanh(3 * val);
                            break;
                        case 'dc_bias_tape':
                            const bias = 0.5; // wartość stała przesuwająca sygnał
                            val = Math.tanh(val + bias) - Math.tanh(bias);
                            break;
                        case 'ac_bias_tape':
                            val = Math.tanh(1.0 * val); // łagodna symetryczna saturacja
                            break;
                        case 'trafo_saturation':
                            val = Math.tanh(1.5 * val); // nieliniowość rdzenia
                            break;
                            // brak domyślnej – pozostaw val bez zmian
                    }
                    break;
                case 'triangle':
                    val = 1 - 4 * Math.abs(Math.round(t - 0.25) - (t - 0.25));
                    break;
                case 'square':
                    val = t < 0.5 ? 1 : -1;
                    break;
                case 'sawtooth':
                    val = 2 * t - 1;
                    break;
                case 'clipped_sine':
                    val = Math.sin(2 * Math.PI * t);
                    val = Math.max(-0.5, Math.min(0.5, val)); // Przycięcie
                    break;
                case 'noise':
                    val = 2 * Math.random() - 1;
                    break;
                case 'impulse_train':
                    // Co N/8 próbek impuls (zwraca 1), reszta 0
                    val = (i % Math.floor(count / 8)) === 0 ? 1 : 0;
                    break;
                case 'burst':
                    // Sinus aktywny tylko przez pierwsze 1/8 okresu
                    val = t < 0.125 ? Math.sin(2 * Math.PI * 8 * t) : 0;
                    break;
                case 'am':
                    const A = 0.7;
                    const m = modIndex; // głębokość AM: 0.0–1.0
                    val = A * (1 + m * modulator) * Math.sin(2 * Math.PI * fc * t);
                    break;
                case 'fm':
                    val = Math.sin(2 * Math.PI * fc * t + modIndex * modulator * 2 * Math.PI);
                    break;
                case 'dsb':
                    val = modIndex * modulator * Math.cos(2 * Math.PI * fc * t);
                    break;
                case 'pwm':
                    const duty = modIndex; // duty cycle ∈ [0,1]
                    val = (t % 1) < duty ? 1 : -1;
                    break;

                default:
                    val = 0;
            }

            samples.push(parseFloat(val.toFixed(4)));
        }

        currentSamples = samples;
        updateDisplay(samples);
        updateFFT(samples);
    }

    function playLUT() {
        stopSound();
        const frequency = parseFloat(document.getElementById('frequency').value);
        const amplitude = parseFloat(document.getElementById('amplitude').value);
        const sampleRate = 44100;
        const periodLength = Math.floor(sampleRate / frequency);
        const lut = currentSamples;

        audioCtx = new(window.AudioContext || window.webkitAudioContext)();
        const buffer = audioCtx.createBuffer(1, periodLength, sampleRate);
        const data = buffer.getChannelData(0);

        for (let i = 0; i < periodLength; i++) {
            const phase = (i * lut.length / periodLength) % lut.length;
            const index = Math.floor(phase);
            const frac = phase - index;
            const nextIndex = (index + 1) % lut.length;
            data[i] = amplitude * (lut[index] * (1 - frac) + lut[nextIndex] * frac);
        }

        sourceNode = audioCtx.createBufferSource();
        sourceNode.buffer = buffer;
        sourceNode.loop = true;
        sourceNode.connect(audioCtx.destination);
        sourceNode.start();
    }

    function stopSound() {
        if (sourceNode) {
            sourceNode.stop();
            sourceNode.disconnect();
            sourceNode = null;
        }
        if (audioCtx) {
            audioCtx.close();
            audioCtx = null;
        }
    }

    function updateFFT(samples) {
        const spectrum = dft(samples);
        const N = samples.length;
        const Fs = N; // Zakładamy 1 cykl na N próbek ⇒ Fs = N (1 okres = 1 sekunda)

        // Częstotliwości (do N/2 - jednostronne widmo)
        const freqs = Array.from({
            length: N / 2
        }, (_, i) => i * Fs / N);

        // Amplitudy – skalowanie dla jednostronnego widma
        const magnitudes = spectrum.slice(0, N / 2).map((c, i) => {
            let mag = Math.sqrt(c.re ** 2 + c.im ** 2) / N;
            if (i !== 0) mag *= 2; // tylko dla składowych > 0 Hz
            return mag;
        });

        const thd = calculateTHD(magnitudes);

        drawFFT(freqs, magnitudes);
        doAlerts(magnitudes, thd);
    }

    function drawFFT(freqs, magnitudes) {
        const ctxFFT = document.getElementById('fftChart').getContext('2d');

        if (chartFFT) {
            chartFFT.data.labels = freqs;
            chartFFT.data.datasets[0].data = magnitudes;
            chartFFT.update();
            return;
        }

        chartFFT = new Chart(ctxFFT, {
            type: 'bar',
            data: {
                labels: freqs,
                datasets: [{
                    label: 'FFT (Amplituda)',
                    data: magnitudes,
                    backgroundColor: 'rgba(255, 99, 132, 0.5)',
                    borderColor: 'rgb(255, 99, 132)',
                    borderWidth: 1
                }]
            },
            options: {
                responsive: true,
                scales: {
                    x: {
                        title: {
                            display: true,
                            text: 'Harmoniczna'
                        }
                    },
                    y: {
                        title: {
                            display: true,
                            text: 'Amplituda'
                        }
                    }
                }
            }
        });
    }

    function showAlertTHD(message, type = "info") {
        const alert = `
        <div class="alert alert-${type}" role="alert">
            <p class="mb-0">${message}</p>
        </div>`;
        document.getElementById("thdInfo").innerHTML = alert;
    }
    
    function doAlerts(magnitudes, thd) {
        document.getElementById('dcInfo').innerHTML = '';
        if (magnitudes[0] > 0.01) {
            document.getElementById('dcInfo').innerHTML =
                '<div class="alert alert-info"><p class="mb-0"><i class="bi bi-info-square"></i> <strong>Informacja:</strong> Wykryto składową stałą DC w widmie – odpowiada zerowej harmonicznej (0 Hz). Jest to normalne przy przebiegach niesymetrycznych (np. wyprostowanych).</p></div>';
        }
                
        const thdInfo = document.getElementById('thdInfo');
        const presetTabActive = document.getElementById('btnPresetTab').classList.contains('active');
        const wave = document.getElementById('waveformType').value;
        if (presetTabActive && ((wave === 'am') || (wave === 'fm') || (wave === 'dsb')))
        {
            thdInfo.innerHTML = '';
            return;        
        }
        
        let level, message, alertClass;

        if (thd < 1) {
            level = "success";
            message = `THD: ${thd.toFixed(2)}% – Sygnał o bardzo niskim poziomie zniekształceń.`;
        } else if (thd < 5) {
            level = "info";
            message = `THD: ${thd.toFixed(2)}% – Niski poziom zniekształceń, typowy dla systemów audio.`;
        } else if (thd < 15) {
            level = "warning";
            message = `THD: ${thd.toFixed(2)}% – Umiarkowane zniekształcenia. Możliwe efekty nieliniowości.`;
        } else if (thd < 30) {
            level = "danger";
            message = `THD: ${thd.toFixed(2)}% – Wysoki poziom zniekształceń!`;
        } else if (thd < 100) {
            level = "danger";
            message = `<i class="bi bi-exclamation-triangle"></i> THD: ${thd.toFixed(2)}% – Bardzo silne zniekształcenia! Sygnał może być nasycony lub obcięty.`;
        } else {
            level = "danger";
            message = `<i class="bi bi-exclamation-circle"></i> THD przekracza 100%! Oznacza to, że sygnał zawiera więcej energii w harmonicznych niż w tonie podstawowym. Sygnał jest silnie zniekształcony lub niemal całkowicie pozbawiony podstawowej częstotliwości.`;
        }       
        
        showAlertTHD(message, level);
    }

    function updateDisplay(samples) {
        document.getElementById('lutOutput').value = samples.join(', ');
        drawChart(samples);
    }

    function drawChart(data) {
        const labels = [...Array(data.length).keys()];
        if (chart) chart.destroy();
        chart = new Chart(document.getElementById('lutChart'), {
            type: 'line',
            data: {
                labels: labels,
                datasets: [{
                    label: 'LUT',
                    data: data,
                    borderColor: 'teal',
                    tension: 0.3,
                    pointRadius: 2
                }]
            },
            options: {
                responsive: true,
                animation: false,
                scales: {
                    y: {
                        min: -1.5,
                        max: 1.5
                    },
                    x: {
                        display: false
                    }
                },
                plugins: {
                    legend: {
                        display: false
                    }
                }
            }
        });
    }

    function drawFromLUT(lut) {
        ctx.clearRect(0, 0, canvas.width, canvas.height);
        ctx.beginPath();
        ctx.lineWidth = 2;
        ctx.strokeStyle = '#000';
        for (let i = 0; i < lut.length; i++) {
            const x = i / lut.length * canvas.width;
            const y = (1 - (lut[i] + 1) / 2) * canvas.height;
            if (i === 0) {
                ctx.moveTo(x, y);
            } else {
                ctx.lineTo(x, y);
            }
        }
        ctx.stroke();
    }

    function drawPoint(x, y) {
        drawnPoints[x] = y;
        ctx.fillStyle = 'black';
        ctx.fillRect(x, y, 1, 1);
    }

    function generateLUTFromDrawing() {
        const lutSize = parseInt(document.getElementById('sampleCount').value);
        const samples = [];

        for (let i = 0; i < lutSize; i++) {
            const x = Math.floor(i * canvas.width / lutSize);
            let y = drawnPoints[x];

            if (y === undefined) {
                // Brak punktu: domyślnie 0
                y = canvas.height / 2;
            }

            const val = 1 - (y / canvas.height) * 2; // Zamiana Y na [-1, 1]
            samples.push(parseFloat(val.toFixed(4)));
        }

        currentSamples = samples;
        updateDisplay(samples);
        updateFFT(samples);
    }

    function addHarmonicRow(n = '', a = '') {
        const row = document.createElement('tr');
        row.innerHTML = `
            <td><input type="number" class="form-control form-control-sm" value="${n}" min="1" aria-label="Harmoniczna nr. ${n}"></td>
            <td><input type="number" class="form-control form-control-sm" value="${a}" step="0.01" aria-label="Amplituda wartość ${a}"></td>
            <td><button class="btn btn-danger btn-sm" onclick="this.closest('tr').remove()" aria-label="Usuń harmoniczną"><span class="bi bi-x"></span></button></td>
        `;
        document.querySelector('#harmonicsTable tbody').appendChild(row);
    }

    function generateFromHarmonics() {
        const rows = document.querySelectorAll('#harmonicsTable tbody tr');
        const sampleCount = parseInt(document.getElementById('sampleCount').value);
        const lut = new Array(sampleCount).fill(0);

        for (let row of rows) {
            const n = parseInt(row.cells[0].querySelector('input').value);
            const a = parseFloat(row.cells[1].querySelector('input').value);
            if (isNaN(n) || isNaN(a)) continue;

            for (let i = 0; i < sampleCount; i++) {
                const t = i / sampleCount;
                lut[i] += a * Math.sin(2 * Math.PI * n * t);
            }
        }

        // Normalizacja (opcjonalnie)
        const max = Math.max(...lut.map(Math.abs));
        if (max > 0) {
            for (let i = 0; i < lut.length; i++) {
                lut[i] /= max;
            }
        }

        currentSamples = lut;
        updateDisplay(lut);
        drawFromLUT(lut);
        updateFFT(lut);
    }


    function exportWAV() {
        const frequency = parseFloat(document.getElementById('frequency').value);
        const amplitude = parseFloat(document.getElementById('amplitude').value);
        const duration = 2; // sekundy
        const sampleRate = 44100;
        const totalSamples = duration * sampleRate;
        const lut = currentSamples;
        const buffer = new Float32Array(totalSamples);

        for (let i = 0; i < totalSamples; i++) {
            const phase = (i * lut.length * frequency / sampleRate) % lut.length;
            const index = Math.floor(phase);
            const frac = phase - index;
            const nextIndex = (index + 1) % lut.length;
            buffer[i] = amplitude * (lut[index] * (1 - frac) + lut[nextIndex] * frac);
        }

        // Konwersja do WAV
        const wavData = encodeWAV(buffer, sampleRate);
        const blob = new Blob([wavData], {
            type: 'audio/wav'
        });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = 'waveform.wav';
        a.click();
        URL.revokeObjectURL(url);
    }

    // Pomocnicza funkcja WAV (mono 16-bit PCM)
    function encodeWAV(samples, sampleRate) {
        const buffer = new ArrayBuffer(44 + samples.length * 2);
        const view = new DataView(buffer);

        function writeStr(offset, str) {
            for (let i = 0; i < str.length; i++) {
                view.setUint8(offset + i, str.charCodeAt(i));
            }
        }

        writeStr(0, 'RIFF');
        view.setUint32(4, 36 + samples.length * 2, true);
        writeStr(8, 'WAVE');
        writeStr(12, 'fmt ');
        view.setUint32(16, 16, true);
        view.setUint16(20, 1, true);
        view.setUint16(22, 1, true);
        view.setUint32(24, sampleRate, true);
        view.setUint32(28, sampleRate * 2, true);
        view.setUint16(32, 2, true);
        view.setUint16(34, 16, true);
        writeStr(36, 'data');
        view.setUint32(40, samples.length * 2, true);

        for (let i = 0; i < samples.length; i++) {
            const s = Math.max(-1, Math.min(1, samples[i]));
            view.setInt16(44 + i * 2, s * 0x7FFF, true);
        }

        return view;
    }

    function exportLUT() {
        const lut = currentSamples;
        const blob = new Blob([JSON.stringify(lut, null, 2)], {
            type: 'application/json'
        });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = 'lut.json';
        a.click();
        URL.revokeObjectURL(url);
    }

    function importLUT(event) {
        const file = event.target.files[0];
        if (!file) return;

        const reader = new FileReader();
        reader.onload = function(e) {
            try {
                const json = JSON.parse(e.target.result);
                if (!Array.isArray(json)) throw new Error("Niepoprawny format LUT");

                currentSamples = json;
                document.getElementById('sampleCount').value = currentSamples.length;

                updateDisplay(currentSamples);
                drawFromLUT(currentSamples);
            } catch (err) {
                alert("Błąd podczas importu LUT: " + err.message);
            }
        };
        reader.readAsText(file);;
    }

    // Obsługa rysowania
    canvas.addEventListener('mousedown', e => {
        drawing = true;
        ctx.clearRect(0, 0, canvas.width, canvas.height); // czyszczenie
        drawnPoints = [];
        lastX = e.offsetX;
        lastY = e.offsetY;
    });

    canvas.addEventListener('mousemove', e => {
        if (!drawing) return;

        const x = e.offsetX;
        const y = e.offsetY;

        ctx.strokeStyle = '#000';
        ctx.lineWidth = 2; // grubsza linia
        ctx.beginPath();
        ctx.moveTo(lastX, lastY);
        ctx.lineTo(x, y);
        ctx.stroke();

        for (let i = lastX; i <= x; i++) {
            drawnPoints[i] = y; // zapamiętaj punkt
        }

        lastX = x;
        lastY = y;
    });

    canvas.addEventListener('mouseup', () => {
        drawing = false;
        generateLUTFromDrawing();
    });

    canvas.addEventListener('touchstart', (e) => {
        e.preventDefault();
        const touch = e.touches[0];
        const mouseEvent = new MouseEvent('mousedown', {
            clientX: touch.clientX,
            clientY: touch.clientY
        });
        canvas.dispatchEvent(mouseEvent);
    }, {
        passive: false
    });

    canvas.addEventListener('touchmove', (e) => {
        e.preventDefault();
        const touch = e.touches[0];
        const mouseEvent = new MouseEvent('mousemove', {
            clientX: touch.clientX,
            clientY: touch.clientY
        });
        canvas.dispatchEvent(mouseEvent);
    }, {
        passive: false
    });

    canvas.addEventListener('touchend', (e) => {
        e.preventDefault();
        const mouseEvent = new MouseEvent('mouseup', {});
        canvas.dispatchEvent(mouseEvent);
    }, {
        passive: false
    });

    // Inicjalizacja domyślnego przebiegu
    generateLUT();

    const waveformType = document.getElementById('waveformType');
    waveformType.addEventListener('change', function(e) {
        e.preventDefault();
        const modType = document.getElementById('modType');
        const modIndex = document.getElementById('modIndex');
        const distortion = document.getElementById('distortion');
        let value = waveformType.value;

        switch (value) {
            case 'sine':
                modType.disabled = true;
                modIndex.disabled = true;
                distortion.disabled = false;
                break;
            case 'am':
                modType.disabled = false;
                modIndex.disabled = false;
                distortion.disabled = true;
                break;
            case 'fm':
                modType.disabled = false;
                modIndex.disabled = false;
                distortion.disabled = true;
                break;
            case 'dsb':
                modType.disabled = false;
                modIndex.disabled = false;
                distortion.disabled = true;
                break;
            case 'pwm':
                modType.disabled = true;
                modIndex.disabled = false;
                distortion.disabled = true;
                break;
            default:
                modType.disabled = true;
                modIndex.disabled = true;
                distortion.disabled = true;
                break;
        }
    });

</script>

Kod po stronie serwera

Informacja

Aplikacja nie korzysta z kodu po stronie serwera.

Tagi

JavaScript

Dziękujemy!
()

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.