Generator przebiegów arbitralnych
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.

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.
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.