seqeyes-python 0.3.10__tar.gz → 0.3.12__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/PKG-INFO +1 -1
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/pyproject.toml +1 -1
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/src/seqeyes/resources/pulseq-bundle.js +287 -10
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/src/seqeyes/resources/webview-bundle.js +329 -39
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/.gitignore +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/README.md +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/examples/demo.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/src/seqeyes/resources/seqeyes-labels.js +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/conftest.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/debug_full.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/integration_test.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/test_plot.py +0 -0
- {seqeyes_python-0.3.10 → seqeyes_python-0.3.12}/tests/test_renderer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: seqeyes-python
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.12
|
|
4
4
|
Summary: Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bughht/seqeyes_plugin
|
|
6
6
|
Project-URL: Repository, https://github.com/bughht/seqeyes_plugin
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seqeyes-python"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.12"
|
|
8
8
|
description = "Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -89,7 +89,7 @@ var Pulseq = (() => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
// package.json
|
|
92
|
-
var version = "0.3.
|
|
92
|
+
var version = "0.3.12";
|
|
93
93
|
|
|
94
94
|
// src/pulseq/decompressor.ts
|
|
95
95
|
function decompressShape(compressed, numSamples) {
|
|
@@ -2399,7 +2399,7 @@ var Pulseq = (() => {
|
|
|
2399
2399
|
decimatedSamples,
|
|
2400
2400
|
columns,
|
|
2401
2401
|
fftPoints,
|
|
2402
|
-
totalCells: columns * bins * 4,
|
|
2402
|
+
totalCells: columns * bins * (input.includeRf ? 7 : 4),
|
|
2403
2403
|
decimationFactor
|
|
2404
2404
|
};
|
|
2405
2405
|
}
|
|
@@ -4657,6 +4657,125 @@ var Pulseq = (() => {
|
|
|
4657
4657
|
return Math.floor((coreSamples - 1) / Math.max(1, factor)) + 1;
|
|
4658
4658
|
}
|
|
4659
4659
|
|
|
4660
|
+
// src/pulseq/rfAcoustic.ts
|
|
4661
|
+
function normalizePeak(values) {
|
|
4662
|
+
let peak = 0;
|
|
4663
|
+
for (let i = 0; i < values.length; i++) {
|
|
4664
|
+
const v = values[i] < 0 ? -values[i] : values[i];
|
|
4665
|
+
if (v > peak) peak = v;
|
|
4666
|
+
}
|
|
4667
|
+
if (peak > 0) for (let i = 0; i < values.length; i++) values[i] /= peak;
|
|
4668
|
+
}
|
|
4669
|
+
function rfPowerAt(rf, t) {
|
|
4670
|
+
const tp = rf.timePoints, mag = rf.magnitude;
|
|
4671
|
+
const last = tp.length - 1;
|
|
4672
|
+
if (last < 0) return 0;
|
|
4673
|
+
if (last === 0) return mag[0] * mag[0];
|
|
4674
|
+
if (t < tp[0] - GRADIENT_ENDPOINT_TOLERANCE_SEC || t > tp[last] + GRADIENT_ENDPOINT_TOLERANCE_SEC) return 0;
|
|
4675
|
+
if (t <= tp[0]) return mag[0] * mag[0];
|
|
4676
|
+
if (t >= tp[last]) return mag[last] * mag[last];
|
|
4677
|
+
let lo = 0, hi = last;
|
|
4678
|
+
while (hi - lo > 1) {
|
|
4679
|
+
const m = lo + hi >> 1;
|
|
4680
|
+
if (tp[m] <= t) lo = m;
|
|
4681
|
+
else hi = m;
|
|
4682
|
+
}
|
|
4683
|
+
const span = tp[hi] - tp[lo];
|
|
4684
|
+
const pLo = mag[lo] * mag[lo];
|
|
4685
|
+
if (span <= 0) return pLo;
|
|
4686
|
+
const pHi = mag[hi] * mag[hi];
|
|
4687
|
+
return pLo + (pHi - pLo) * (t - tp[lo]) / span;
|
|
4688
|
+
}
|
|
4689
|
+
function rfPowerArea(rf) {
|
|
4690
|
+
const tp = rf.timePoints, mag = rf.magnitude;
|
|
4691
|
+
let area = 0;
|
|
4692
|
+
for (let i = 1; i < tp.length; i++) {
|
|
4693
|
+
const pPrev = mag[i - 1] * mag[i - 1];
|
|
4694
|
+
const pCurr = mag[i] * mag[i];
|
|
4695
|
+
area += 0.5 * (pPrev + pCurr) * (tp[i] - tp[i - 1]);
|
|
4696
|
+
}
|
|
4697
|
+
return area;
|
|
4698
|
+
}
|
|
4699
|
+
function resampleRfAcousticSources(blocks, options) {
|
|
4700
|
+
const dt = options.dt;
|
|
4701
|
+
if (!(dt > 0)) throw new Error("resampleRfAcousticSources requires a positive dt.");
|
|
4702
|
+
const t0 = Number.isFinite(options.startSec) ? options.startSec : 0;
|
|
4703
|
+
const n = Math.max(0, Math.floor(options.sampleCount));
|
|
4704
|
+
const rfScale = Math.max(0, Number.isFinite(options.rfScale) ? options.rfScale : 1);
|
|
4705
|
+
const edgeMode = options.edgeMode === "absolute" ? "absolute" : "signed";
|
|
4706
|
+
const power = new Float32Array(n);
|
|
4707
|
+
const gate = new Float32Array(n);
|
|
4708
|
+
const thermo = new Float32Array(n);
|
|
4709
|
+
const control = new Float32Array(n);
|
|
4710
|
+
const empty = {
|
|
4711
|
+
t0,
|
|
4712
|
+
dt,
|
|
4713
|
+
n,
|
|
4714
|
+
power,
|
|
4715
|
+
gate,
|
|
4716
|
+
thermo,
|
|
4717
|
+
control,
|
|
4718
|
+
silent: true,
|
|
4719
|
+
eventCount: 0,
|
|
4720
|
+
subSampleEventCount: 0
|
|
4721
|
+
};
|
|
4722
|
+
if (!n) return empty;
|
|
4723
|
+
const tEnd = t0 + (n - 1) * dt;
|
|
4724
|
+
let eventCount = 0;
|
|
4725
|
+
let subSampleEventCount = 0;
|
|
4726
|
+
for (const block of blocks) {
|
|
4727
|
+
const rf = block.rf;
|
|
4728
|
+
if (!rf || !rf.timePoints || !rf.timePoints.length || !rf.magnitude.length) continue;
|
|
4729
|
+
const last = rf.timePoints.length - 1;
|
|
4730
|
+
const rfStart = rf.timePoints[0];
|
|
4731
|
+
const rfEnd = rf.timePoints[last];
|
|
4732
|
+
if (rfEnd < t0 - GRADIENT_ENDPOINT_TOLERANCE_SEC || rfStart > tEnd + GRADIENT_ENDPOINT_TOLERANCE_SEC) continue;
|
|
4733
|
+
eventCount++;
|
|
4734
|
+
const gateLo = clampIndex(Math.floor((rfStart - t0) / dt), n);
|
|
4735
|
+
const gateHi = clampIndex(Math.ceil((rfEnd - t0) / dt), n);
|
|
4736
|
+
for (let i = gateLo; i <= gateHi; i++) gate[i] = 1;
|
|
4737
|
+
const coverLo = clampIndex(Math.ceil((rfStart - t0) / dt), n);
|
|
4738
|
+
const coverHi = clampIndex(Math.floor((rfEnd - t0) / dt), n);
|
|
4739
|
+
const spansASample = rfEnd - rfStart >= dt && coverHi >= coverLo;
|
|
4740
|
+
if (spansASample) {
|
|
4741
|
+
for (let i = coverLo; i <= coverHi; i++) power[i] += rfPowerAt(rf, t0 + i * dt);
|
|
4742
|
+
} else {
|
|
4743
|
+
subSampleEventCount++;
|
|
4744
|
+
const area = last === 0 ? rf.magnitude[0] * rf.magnitude[0] * dt : rfPowerArea(rf);
|
|
4745
|
+
const mid = clampIndex(Math.round((0.5 * (rfStart + rfEnd) - t0) / dt), n);
|
|
4746
|
+
power[mid] += area / dt;
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
const silent = eventCount === 0;
|
|
4750
|
+
if (silent) return { ...empty, silent: true };
|
|
4751
|
+
normalizePeak(power);
|
|
4752
|
+
const invDt = 1 / dt;
|
|
4753
|
+
for (let i = 1; i < n; i++) thermo[i] = (power[i] - power[i - 1]) * invDt;
|
|
4754
|
+
normalizePeak(thermo);
|
|
4755
|
+
const amplitudeGain = rfScale * rfScale;
|
|
4756
|
+
if (amplitudeGain !== 1) for (let i = 0; i < n; i++) thermo[i] *= amplitudeGain;
|
|
4757
|
+
for (let i = 1; i < n; i++) {
|
|
4758
|
+
const d = gate[i] - gate[i - 1];
|
|
4759
|
+
control[i] = edgeMode === "absolute" ? Math.abs(d) : d;
|
|
4760
|
+
}
|
|
4761
|
+
return { t0, dt, n, power, gate, thermo, control, silent: false, eventCount, subSampleEventCount };
|
|
4762
|
+
}
|
|
4763
|
+
function clampIndex(index, n) {
|
|
4764
|
+
if (!Number.isFinite(index)) return 0;
|
|
4765
|
+
if (index < 0) return 0;
|
|
4766
|
+
if (index > n - 1) return n - 1;
|
|
4767
|
+
return index;
|
|
4768
|
+
}
|
|
4769
|
+
function combineRfSources(sources, thermoWeight, controlWeight) {
|
|
4770
|
+
const wThermo = Number.isFinite(thermoWeight) ? thermoWeight : 1;
|
|
4771
|
+
const wControl = Number.isFinite(controlWeight) ? controlWeight : 1;
|
|
4772
|
+
const out = new Float32Array(sources.n);
|
|
4773
|
+
for (let i = 0; i < sources.n; i++) {
|
|
4774
|
+
out[i] = wThermo * sources.thermo[i] + wControl * sources.control[i];
|
|
4775
|
+
}
|
|
4776
|
+
return out;
|
|
4777
|
+
}
|
|
4778
|
+
|
|
4660
4779
|
// src/pulseq/gradSpectrum.ts
|
|
4661
4780
|
var GAMMA_HZ_PER_M_PER_MT_PER_M = 42576;
|
|
4662
4781
|
var GAMMA_HZ_PER_M_PER_T_PER_M = 42576e3;
|
|
@@ -4669,7 +4788,15 @@ var Pulseq = (() => {
|
|
|
4669
4788
|
overlap: 0.75,
|
|
4670
4789
|
oversample: 3,
|
|
4671
4790
|
targetColumns: 256,
|
|
4672
|
-
normalize: true
|
|
4791
|
+
normalize: true,
|
|
4792
|
+
// Off by default: the RF terms are research proxies with no calibrated
|
|
4793
|
+
// level, so they are opt-in rather than something a user meets by accident
|
|
4794
|
+
// in a panel whose other channels are in mT/m.
|
|
4795
|
+
includeRf: false,
|
|
4796
|
+
rfScale: 1,
|
|
4797
|
+
rfThermoWeight: 1,
|
|
4798
|
+
rfControlWeight: 1,
|
|
4799
|
+
rfEdgeMode: "signed"
|
|
4673
4800
|
});
|
|
4674
4801
|
function resolveSpectrogramParams(params) {
|
|
4675
4802
|
const merged = { ...DEFAULT_SPECTROGRAM_PARAMS, ...params ?? {} };
|
|
@@ -4682,7 +4809,12 @@ var Pulseq = (() => {
|
|
|
4682
4809
|
overlap: clamp2(finite(merged.overlap, 0.75), 0, 0.9375),
|
|
4683
4810
|
oversample: clamp2(Math.round(finite(merged.oversample, 3)), 1, 4),
|
|
4684
4811
|
targetColumns: clamp2(Math.round(finite(merged.targetColumns, 256)), 64, 512),
|
|
4685
|
-
normalize: merged.normalize !== false
|
|
4812
|
+
normalize: merged.normalize !== false,
|
|
4813
|
+
includeRf: merged.includeRf === true,
|
|
4814
|
+
rfScale: clamp2(finite(merged.rfScale, 1), 0, 1e3),
|
|
4815
|
+
rfThermoWeight: clamp2(finite(merged.rfThermoWeight, 1), 0, 1e3),
|
|
4816
|
+
rfControlWeight: clamp2(finite(merged.rfControlWeight, 1), 0, 1e3),
|
|
4817
|
+
rfEdgeMode: merged.rfEdgeMode === "absolute" ? "absolute" : "signed"
|
|
4686
4818
|
};
|
|
4687
4819
|
}
|
|
4688
4820
|
function chooseWindowSamples(params, decimatedDt, viewDurationSec, warnings) {
|
|
@@ -4786,6 +4918,29 @@ var Pulseq = (() => {
|
|
|
4786
4918
|
const dx = decimatePadded(sx, plan, padSamples, nDecimated);
|
|
4787
4919
|
const dy = decimatePadded(sy, plan, padSamples, nDecimated);
|
|
4788
4920
|
const dz = decimatePadded(sz, plan, padSamples, nDecimated);
|
|
4921
|
+
let dRfThermo = null;
|
|
4922
|
+
let dRfControl = null;
|
|
4923
|
+
let dRfCombined = null;
|
|
4924
|
+
if (params.includeRf) {
|
|
4925
|
+
const rfSources = resampleRfAcousticSources(blocks, {
|
|
4926
|
+
startSec: startSec - padSamples * raster,
|
|
4927
|
+
dt: raster,
|
|
4928
|
+
sampleCount: totalSamples,
|
|
4929
|
+
rfScale: params.rfScale,
|
|
4930
|
+
edgeMode: params.rfEdgeMode
|
|
4931
|
+
});
|
|
4932
|
+
if (rfSources.silent) {
|
|
4933
|
+
warnings.push("No RF events in the visible window; the RF proxy channels are empty.");
|
|
4934
|
+
} else if (rfSources.subSampleEventCount > 0) {
|
|
4935
|
+
warnings.push(
|
|
4936
|
+
`${rfSources.subSampleEventCount} RF event(s) are shorter than the ${formatSeconds(raster)} gradient raster and were deposited as area-preserving impulses; their edge timing is raster-quantised.`
|
|
4937
|
+
);
|
|
4938
|
+
}
|
|
4939
|
+
const combined = combineRfSources(rfSources, params.rfThermoWeight, params.rfControlWeight);
|
|
4940
|
+
dRfThermo = decimatePadded(rfSources.thermo, plan, padSamples, nDecimated);
|
|
4941
|
+
dRfControl = decimatePadded(rfSources.control, plan, padSamples, nDecimated);
|
|
4942
|
+
dRfCombined = decimatePadded(combined, plan, padSamples, nDecimated);
|
|
4943
|
+
}
|
|
4789
4944
|
const binLow = Math.max(0, Math.floor(params.fMinHz / dfBin));
|
|
4790
4945
|
const binHigh = Math.min(fftPoints / 2, Math.ceil(params.fMaxHz / dfBin));
|
|
4791
4946
|
const nFreq = Math.max(1, binHigh - binLow + 1);
|
|
@@ -4794,6 +4949,9 @@ var Pulseq = (() => {
|
|
|
4794
4949
|
const gyOut = new Float32Array(cells);
|
|
4795
4950
|
const gzOut = new Float32Array(cells);
|
|
4796
4951
|
const rssOut = new Float32Array(cells);
|
|
4952
|
+
const rfThermoOut = dRfThermo ? new Float32Array(cells) : null;
|
|
4953
|
+
const rfControlOut = dRfControl ? new Float32Array(cells) : null;
|
|
4954
|
+
const rfOut = dRfCombined ? new Float32Array(cells) : null;
|
|
4797
4955
|
const w = hannWindow(windowSamples);
|
|
4798
4956
|
const gain = params.normalize ? windowCoherentGain(w) : 1;
|
|
4799
4957
|
const invGain = gain > 0 ? 1 / gain : 1;
|
|
@@ -4806,8 +4964,18 @@ var Pulseq = (() => {
|
|
|
4806
4964
|
const magA = new Float64Array(fftPoints / 2 + 1);
|
|
4807
4965
|
const magB = new Float64Array(fftPoints / 2 + 1);
|
|
4808
4966
|
const magC = new Float64Array(fftPoints / 2 + 1);
|
|
4967
|
+
const rfActive = !!(dRfThermo && dRfControl && dRfCombined && rfThermoOut && rfControlOut && rfOut);
|
|
4968
|
+
const frameD = rfActive ? new Float64Array(fftPoints) : null;
|
|
4969
|
+
const frameE = rfActive ? new Float64Array(fftPoints) : null;
|
|
4970
|
+
const frameF = rfActive ? new Float64Array(fftPoints) : null;
|
|
4971
|
+
const magD = rfActive ? new Float64Array(fftPoints / 2 + 1) : null;
|
|
4972
|
+
const magE = rfActive ? new Float64Array(fftPoints / 2 + 1) : null;
|
|
4973
|
+
const magF = rfActive ? new Float64Array(fftPoints / 2 + 1) : null;
|
|
4974
|
+
const thermoSilent = rfActive && isAllZero(dRfThermo);
|
|
4975
|
+
const controlSilent = rfActive && isAllZero(dRfControl);
|
|
4809
4976
|
let minValue = Number.POSITIVE_INFINITY;
|
|
4810
4977
|
let maxValue = 0;
|
|
4978
|
+
let rfMaxValue = 0;
|
|
4811
4979
|
for (let col = 0; col < columns; col++) {
|
|
4812
4980
|
const offset = col * hop;
|
|
4813
4981
|
prepareFrame(frameA, dx, offset, windowSamples, w, fftPoints);
|
|
@@ -4815,6 +4983,23 @@ var Pulseq = (() => {
|
|
|
4815
4983
|
prepareFrame(frameC, dz, offset, windowSamples, w, fftPoints);
|
|
4816
4984
|
realFFTPairMagnitude(frameA, frameB, fftPoints, scratchRe, scratchIm, magA, magB);
|
|
4817
4985
|
realFFTMagnitude(frameC, fftPoints, scratchRe, scratchIm, magC);
|
|
4986
|
+
if (rfActive) {
|
|
4987
|
+
prepareFrame(frameF, dRfCombined, offset, windowSamples, w, fftPoints);
|
|
4988
|
+
realFFTMagnitude(frameF, fftPoints, scratchRe, scratchIm, magF);
|
|
4989
|
+
if (thermoSilent) magD.fill(0);
|
|
4990
|
+
if (controlSilent) magE.fill(0);
|
|
4991
|
+
if (!thermoSilent && !controlSilent) {
|
|
4992
|
+
prepareFrame(frameD, dRfThermo, offset, windowSamples, w, fftPoints);
|
|
4993
|
+
prepareFrame(frameE, dRfControl, offset, windowSamples, w, fftPoints);
|
|
4994
|
+
realFFTPairMagnitude(frameD, frameE, fftPoints, scratchRe, scratchIm, magD, magE);
|
|
4995
|
+
} else if (!thermoSilent) {
|
|
4996
|
+
prepareFrame(frameD, dRfThermo, offset, windowSamples, w, fftPoints);
|
|
4997
|
+
realFFTMagnitude(frameD, fftPoints, scratchRe, scratchIm, magD);
|
|
4998
|
+
} else if (!controlSilent) {
|
|
4999
|
+
prepareFrame(frameE, dRfControl, offset, windowSamples, w, fftPoints);
|
|
5000
|
+
realFFTMagnitude(frameE, fftPoints, scratchRe, scratchIm, magE);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
4818
5003
|
for (let bin = binLow; bin <= binHigh; bin++) {
|
|
4819
5004
|
const row = bin - binLow;
|
|
4820
5005
|
const index = row * columns + col;
|
|
@@ -4828,6 +5013,13 @@ var Pulseq = (() => {
|
|
|
4828
5013
|
rssOut[index] = vr;
|
|
4829
5014
|
if (vr > maxValue) maxValue = vr;
|
|
4830
5015
|
if (vr < minValue) minValue = vr;
|
|
5016
|
+
if (rfActive) {
|
|
5017
|
+
const vRf = magF[bin] * invGain;
|
|
5018
|
+
rfThermoOut[index] = magD[bin] * invGain;
|
|
5019
|
+
rfControlOut[index] = magE[bin] * invGain;
|
|
5020
|
+
rfOut[index] = vRf;
|
|
5021
|
+
if (vRf > rfMaxValue) rfMaxValue = vRf;
|
|
5022
|
+
}
|
|
4831
5023
|
}
|
|
4832
5024
|
}
|
|
4833
5025
|
if (!Number.isFinite(minValue)) minValue = 0;
|
|
@@ -4845,9 +5037,17 @@ var Pulseq = (() => {
|
|
|
4845
5037
|
dfResolutionHz: decimatedRate / windowSamples,
|
|
4846
5038
|
unit: params.source === "dGdt" ? "T/m/s" : "mT/m",
|
|
4847
5039
|
source: params.source,
|
|
4848
|
-
data: {
|
|
5040
|
+
data: {
|
|
5041
|
+
gx: gxOut,
|
|
5042
|
+
gy: gyOut,
|
|
5043
|
+
gz: gzOut,
|
|
5044
|
+
rss: rssOut,
|
|
5045
|
+
...rfActive ? { rfThermo: rfThermoOut, rfControl: rfControlOut, rf: rfOut } : {}
|
|
5046
|
+
},
|
|
4849
5047
|
minValue,
|
|
4850
5048
|
maxValue,
|
|
5049
|
+
rfIncluded: rfActive,
|
|
5050
|
+
rfMaxValue,
|
|
4851
5051
|
decimationFactor: plan.factor,
|
|
4852
5052
|
decimatedRateHz: decimatedRate,
|
|
4853
5053
|
windowSamples,
|
|
@@ -4904,10 +5104,17 @@ var Pulseq = (() => {
|
|
|
4904
5104
|
gx: new Float32Array(0),
|
|
4905
5105
|
gy: new Float32Array(0),
|
|
4906
5106
|
gz: new Float32Array(0),
|
|
4907
|
-
rss: new Float32Array(0)
|
|
5107
|
+
rss: new Float32Array(0),
|
|
5108
|
+
...params.includeRf ? {
|
|
5109
|
+
rfThermo: new Float32Array(0),
|
|
5110
|
+
rfControl: new Float32Array(0),
|
|
5111
|
+
rf: new Float32Array(0)
|
|
5112
|
+
} : {}
|
|
4908
5113
|
},
|
|
4909
5114
|
minValue: 0,
|
|
4910
5115
|
maxValue: 0,
|
|
5116
|
+
rfIncluded: params.includeRf,
|
|
5117
|
+
rfMaxValue: 0,
|
|
4911
5118
|
decimationFactor,
|
|
4912
5119
|
decimatedRateHz: decimatedRate,
|
|
4913
5120
|
windowSamples,
|
|
@@ -5047,6 +5254,15 @@ var Pulseq = (() => {
|
|
|
5047
5254
|
if (value >= 10) return `${value.toFixed(0)} Hz`;
|
|
5048
5255
|
return `${value.toFixed(1)} Hz`;
|
|
5049
5256
|
}
|
|
5257
|
+
function isAllZero(values) {
|
|
5258
|
+
for (let i = 0; i < values.length; i++) if (values[i] !== 0) return false;
|
|
5259
|
+
return true;
|
|
5260
|
+
}
|
|
5261
|
+
function formatSeconds(value) {
|
|
5262
|
+
if (!Number.isFinite(value)) return "\u2014";
|
|
5263
|
+
if (value >= 1e-3) return `${(value * 1e3).toFixed(2)} ms`;
|
|
5264
|
+
return `${(value * 1e6).toFixed(1)} \xB5s`;
|
|
5265
|
+
}
|
|
5050
5266
|
|
|
5051
5267
|
// src/pulseq/gradientSound.ts
|
|
5052
5268
|
var DEFAULT_AUDIO_SAMPLE_RATE = 44100;
|
|
@@ -5121,17 +5337,78 @@ var Pulseq = (() => {
|
|
|
5121
5337
|
if (l > rawPeak) rawPeak = l;
|
|
5122
5338
|
if (r > rawPeak) rawPeak = r;
|
|
5123
5339
|
}
|
|
5124
|
-
const
|
|
5340
|
+
const rfMix = clamp01(Number.isFinite(options.rfMix) ? options.rfMix : 0.5);
|
|
5341
|
+
let rfRawPeak = 0;
|
|
5342
|
+
let rfMono = null;
|
|
5343
|
+
if (options.includeRf && rfMix > 0) {
|
|
5344
|
+
const sources = resampleRfAcousticSources(blocks, {
|
|
5345
|
+
startSec,
|
|
5346
|
+
dt,
|
|
5347
|
+
sampleCount: n,
|
|
5348
|
+
rfScale: options.rfScale,
|
|
5349
|
+
edgeMode: options.rfEdgeMode
|
|
5350
|
+
});
|
|
5351
|
+
if (!sources.silent) {
|
|
5352
|
+
const combined = combineRfSources(
|
|
5353
|
+
sources,
|
|
5354
|
+
Number.isFinite(options.rfThermoWeight) ? options.rfThermoWeight : 1,
|
|
5355
|
+
Number.isFinite(options.rfControlWeight) ? options.rfControlWeight : 1
|
|
5356
|
+
);
|
|
5357
|
+
rfMono = convolveSame(combined, kernel);
|
|
5358
|
+
for (let i = 0; i < n; i++) {
|
|
5359
|
+
const v = Math.abs(rfMono[i]);
|
|
5360
|
+
if (v > rfRawPeak) rfRawPeak = v;
|
|
5361
|
+
}
|
|
5362
|
+
}
|
|
5363
|
+
}
|
|
5364
|
+
const rfIncluded = !!(rfMono && rfRawPeak > 0);
|
|
5365
|
+
if (rfIncluded) {
|
|
5366
|
+
const gradGain = rawPeak > 0 ? (1 - rfMix) / rawPeak : 0;
|
|
5367
|
+
const rfGain = rfMix / rfRawPeak;
|
|
5368
|
+
for (let i = 0; i < n; i++) {
|
|
5369
|
+
const rfSample = rfMono[i] * rfGain;
|
|
5370
|
+
left[i] = left[i] * gradGain + rfSample;
|
|
5371
|
+
right[i] = right[i] * gradGain + rfSample;
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
let mixedPeak = 0;
|
|
5375
|
+
for (let i = 0; i < n; i++) {
|
|
5376
|
+
const l = Math.abs(left[i]);
|
|
5377
|
+
const r = Math.abs(right[i]);
|
|
5378
|
+
if (l > mixedPeak) mixedPeak = l;
|
|
5379
|
+
if (r > mixedPeak) mixedPeak = r;
|
|
5380
|
+
}
|
|
5381
|
+
const silent = !(mixedPeak > 0);
|
|
5125
5382
|
if (silent) {
|
|
5126
|
-
warnings.push("No gradient activity in this window \u2014 nothing to play.");
|
|
5383
|
+
warnings.push(options.includeRf ? "Nothing to play in this window \u2014 no gradient activity and no RF events." : "No gradient activity in this window \u2014 nothing to play.");
|
|
5127
5384
|
} else {
|
|
5128
|
-
const scale = AUDIO_PEAK /
|
|
5385
|
+
const scale = AUDIO_PEAK / mixedPeak;
|
|
5129
5386
|
for (let i = 0; i < n; i++) {
|
|
5130
5387
|
left[i] *= scale;
|
|
5131
5388
|
right[i] *= scale;
|
|
5132
5389
|
}
|
|
5390
|
+
if (options.includeRf && !rfIncluded && rfMix > 0) {
|
|
5391
|
+
warnings.push("No RF events in this window; you are hearing the gradients only.");
|
|
5392
|
+
} else if (rfIncluded && rawPeak === 0) {
|
|
5393
|
+
warnings.push("No gradient activity in this window; you are hearing the RF proxy only.");
|
|
5394
|
+
}
|
|
5133
5395
|
}
|
|
5134
|
-
return {
|
|
5396
|
+
return {
|
|
5397
|
+
sampleRate,
|
|
5398
|
+
n,
|
|
5399
|
+
left,
|
|
5400
|
+
right,
|
|
5401
|
+
startSec,
|
|
5402
|
+
endSec,
|
|
5403
|
+
rawPeak,
|
|
5404
|
+
rfRawPeak,
|
|
5405
|
+
rfIncluded,
|
|
5406
|
+
silent,
|
|
5407
|
+
warnings
|
|
5408
|
+
};
|
|
5409
|
+
}
|
|
5410
|
+
function clamp01(value) {
|
|
5411
|
+
return value < 0 ? 0 : value > 1 ? 1 : value;
|
|
5135
5412
|
}
|
|
5136
5413
|
|
|
5137
5414
|
// src/pulseq/derivedWindow.ts
|
|
@@ -279,18 +279,26 @@ function decodeB64F32(b64,n){
|
|
|
279
279
|
function deserializeSpectrogram(payload){
|
|
280
280
|
if(!payload)return null;
|
|
281
281
|
var cells=payload.nTime*payload.nFreq;
|
|
282
|
+
var data={
|
|
283
|
+
gx:decodeB64F32(payload.gxB64,cells),
|
|
284
|
+
gy:decodeB64F32(payload.gyB64,cells),
|
|
285
|
+
gz:decodeB64F32(payload.gzB64,cells),
|
|
286
|
+
rss:decodeB64F32(payload.rssB64,cells)
|
|
287
|
+
};
|
|
288
|
+
/* RF blobs travel only when the RF proxy was computed. */
|
|
289
|
+
if(payload.rfB64&&payload.rfThermoB64&&payload.rfControlB64){
|
|
290
|
+
data.rfThermo=decodeB64F32(payload.rfThermoB64,cells);
|
|
291
|
+
data.rfControl=decodeB64F32(payload.rfControlB64,cells);
|
|
292
|
+
data.rf=decodeB64F32(payload.rfB64,cells);
|
|
293
|
+
}
|
|
282
294
|
return{
|
|
283
295
|
nTime:payload.nTime,nFreq:payload.nFreq,
|
|
284
296
|
tStartSec:payload.tStartSec,tStepSec:payload.tStepSec,
|
|
285
297
|
fStartHz:payload.fStartHz,fStepHz:payload.fStepHz,
|
|
286
298
|
dtResolutionSec:payload.dtResolutionSec,dfResolutionHz:payload.dfResolutionHz,
|
|
287
299
|
unit:payload.unit,source:payload.source,
|
|
288
|
-
data:
|
|
289
|
-
|
|
290
|
-
gy:decodeB64F32(payload.gyB64,cells),
|
|
291
|
-
gz:decodeB64F32(payload.gzB64,cells),
|
|
292
|
-
rss:decodeB64F32(payload.rssB64,cells)
|
|
293
|
-
},
|
|
300
|
+
data:data,
|
|
301
|
+
rfIncluded:!!payload.rfIncluded,rfMaxValue:payload.rfMaxValue||0,
|
|
294
302
|
minValue:payload.minValue,maxValue:payload.maxValue,
|
|
295
303
|
decimationFactor:payload.decimationFactor,decimatedRateHz:payload.decimatedRateHz,
|
|
296
304
|
windowSamples:payload.windowSamples,hopSamples:payload.hopSamples,fftPoints:payload.fftPoints,
|
|
@@ -308,6 +316,8 @@ function deserializeGradientSound(payload){
|
|
|
308
316
|
startSec:payload.startSec,
|
|
309
317
|
endSec:payload.endSec,
|
|
310
318
|
silent:!!payload.silent,
|
|
319
|
+
rfIncluded:!!payload.rfIncluded,
|
|
320
|
+
warnings:payload.warnings||[],
|
|
311
321
|
left:decodeB64F32(payload.leftB64,payload.n),
|
|
312
322
|
right:decodeB64F32(payload.rightB64,payload.n)
|
|
313
323
|
};
|
|
@@ -1794,7 +1804,11 @@ function drawMinimap(){
|
|
|
1794
1804
|
|
|
1795
1805
|
// ── Blit cached block bands ──
|
|
1796
1806
|
if(mmCache){
|
|
1797
|
-
|
|
1807
|
+
// The cache is built at device resolution while this context carries a
|
|
1808
|
+
// scale(dpr) transform, so it must be given the CSS-pixel destination
|
|
1809
|
+
// size. Without it the cache draws dpr times too wide and only the
|
|
1810
|
+
// leading 1/dpr of the sequence stays on the strip.
|
|
1811
|
+
mmCtx.drawImage(mmCache,0,0,W,H);
|
|
1798
1812
|
}else{
|
|
1799
1813
|
mmCtx.fillStyle=s.getPropertyValue('--trbg').trim();mmCtx.fillRect(0,0,W,H);
|
|
1800
1814
|
}
|
|
@@ -1976,10 +1990,18 @@ window.SeqEyesPanelHost={
|
|
|
1976
1990
|
},
|
|
1977
1991
|
requestAudio:function(requestId,startSec,endSec,options){
|
|
1978
1992
|
if(!vscApi)return;
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1993
|
+
/* The whole options object crosses, the way requestSpectrogram already
|
|
1994
|
+
ships `params`. Enumerating fields here meant every audio option added to
|
|
1995
|
+
panel.js was silently dropped on this lane only: the RF proxy reached the
|
|
1996
|
+
extension as `undefined`, so it synthesised no RF and fell back to its
|
|
1997
|
+
default mix. There is no per-field list left to drift. */
|
|
1998
|
+
var message={};
|
|
1999
|
+
for(var key in options)if(Object.prototype.hasOwnProperty.call(options,key))message[key]=options[key];
|
|
2000
|
+
message.command='synthesizeGradientSound';
|
|
2001
|
+
message.requestId=requestId;
|
|
2002
|
+
message.startSec=startSec;
|
|
2003
|
+
message.endSec=endSec;
|
|
2004
|
+
vscApi.postMessage(message);
|
|
1983
2005
|
}
|
|
1984
2006
|
};
|
|
1985
2007
|
|
|
@@ -3992,6 +4014,33 @@ function sgValueFloor(spec) {
|
|
|
3992
4014
|
return m > 0 ? m * 1e-6 : 1e-12;
|
|
3993
4015
|
}
|
|
3994
4016
|
|
|
4017
|
+
var SG_RF_CHANNELS = { rf: 1, rfThermo: 1, rfControl: 1 };
|
|
4018
|
+
|
|
4019
|
+
function sgIsRfChannel(key) { return SG_RF_CHANNELS[key] === 1; }
|
|
4020
|
+
|
|
4021
|
+
/**
|
|
4022
|
+
* dB reference for one channel.
|
|
4023
|
+
*
|
|
4024
|
+
* Gradient channels are absolute — dB re 1 mT/m (or 1 T/m/s) — so their
|
|
4025
|
+
* reference is 1 and the number means the same thing in every view. The RF
|
|
4026
|
+
* proxy has no unit at all, so it is referenced to its own peak in the current
|
|
4027
|
+
* view: 0 dB is "the loudest thing the RF proxy does here". Those two readings
|
|
4028
|
+
* are not comparable, which is the honest situation; pretending otherwise would
|
|
4029
|
+
* need a coil transfer function nobody has measured.
|
|
4030
|
+
*/
|
|
4031
|
+
function sgChannelRefDb(spec, key) {
|
|
4032
|
+
if (!sgIsRfChannel(key)) return 0;
|
|
4033
|
+
var peak = spec && spec.rfMaxValue > 0 ? spec.rfMaxValue : 0;
|
|
4034
|
+
return peak > 0 ? 20 * Math.log10(peak) : 0;
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
/** Per-channel magnitude floor, scaled to that channel's own peak. */
|
|
4038
|
+
function sgValueFloorFor(spec, key) {
|
|
4039
|
+
if (!sgIsRfChannel(key)) return sgValueFloor(spec);
|
|
4040
|
+
var peak = spec && spec.rfMaxValue > 0 ? spec.rfMaxValue : 0;
|
|
4041
|
+
return peak > 0 ? peak * 1e-6 : 1e-12;
|
|
4042
|
+
}
|
|
4043
|
+
|
|
3995
4044
|
function sgFmtDb(v) {
|
|
3996
4045
|
if (!isFinite(v)) return '--';
|
|
3997
4046
|
return (v >= 0 ? '+' : '') + v.toFixed(1);
|
|
@@ -4053,10 +4102,11 @@ function sgAutoWindowLevel(spec, key, previous) {
|
|
|
4053
4102
|
var n = data.length;
|
|
4054
4103
|
if (!n) return fallback;
|
|
4055
4104
|
|
|
4056
|
-
var floorValue =
|
|
4105
|
+
var floorValue = sgValueFloorFor(spec, key);
|
|
4106
|
+
var refDb = sgChannelRefDb(spec, key);
|
|
4057
4107
|
var stride = Math.max(1, Math.floor(n / 4096));
|
|
4058
4108
|
var samples = [];
|
|
4059
|
-
for (var i = 0; i < n; i += stride) samples.push(sgToDb(data[i], floorValue));
|
|
4109
|
+
for (var i = 0; i < n; i += stride) samples.push(sgToDb(data[i], floorValue) - refDb);
|
|
4060
4110
|
if (!samples.length) return fallback;
|
|
4061
4111
|
samples.sort(function (a, b) { return a - b; });
|
|
4062
4112
|
|
|
@@ -4149,8 +4199,10 @@ function sgBuildImageData(context) {
|
|
|
4149
4199
|
var data = spec.data[context.channel] || spec.data.rss;
|
|
4150
4200
|
var lut = context.lut;
|
|
4151
4201
|
var wl = context.windowLevel;
|
|
4152
|
-
var floorValue =
|
|
4153
|
-
|
|
4202
|
+
var floorValue = sgValueFloorFor(spec, context.channel);
|
|
4203
|
+
// The channel reference folds into the window's lower edge, so a per-cell
|
|
4204
|
+
// subtraction never enters the inner loop.
|
|
4205
|
+
var low = wl.level - wl.width / 2 + sgChannelRefDb(spec, context.channel);
|
|
4154
4206
|
var invWidth = wl.width > 0 ? 1 / wl.width : 1;
|
|
4155
4207
|
var logScale = 20 / Math.LN10;
|
|
4156
4208
|
|
|
@@ -4538,13 +4590,19 @@ function sgDrawSpectrum(context) {
|
|
|
4538
4590
|
ctx.strokeStyle = trace.color;
|
|
4539
4591
|
ctx.lineWidth = trace.key === 'rss' ? 1.6 : 1.1;
|
|
4540
4592
|
ctx.globalAlpha = trace.key === 'rss' ? 1 : 0.9;
|
|
4593
|
+
// Dashed for the RF proxies: the same dB axis carries two different
|
|
4594
|
+
// references, and a reader should not have to consult the legend to see
|
|
4595
|
+
// which traces are the uncalibrated ones.
|
|
4596
|
+
ctx.setLineDash(sgIsRfChannel(trace.key) ? [4, 3] : []);
|
|
4597
|
+
var traceFloor = sgValueFloorFor(spec, trace.key);
|
|
4598
|
+
var traceRefDb = sgChannelRefDb(spec, trace.key);
|
|
4541
4599
|
ctx.beginPath();
|
|
4542
4600
|
var started = false;
|
|
4543
4601
|
for (var row = 0; row < values.length; row++) {
|
|
4544
4602
|
var f = spec.fStartHz + row * spec.fStepHz;
|
|
4545
4603
|
if (f < range.fMin || f > range.fMax) continue;
|
|
4546
4604
|
var fp = freqPos(f);
|
|
4547
|
-
var mp = magPos(sgToDb(values[row],
|
|
4605
|
+
var mp = magPos(sgToDb(values[row], traceFloor) - traceRefDb);
|
|
4548
4606
|
var xx = rotated ? mp : fp;
|
|
4549
4607
|
var yy = rotated ? fp : mp;
|
|
4550
4608
|
if (!started) { ctx.moveTo(xx, yy); started = true; } else ctx.lineTo(xx, yy);
|
|
@@ -4552,6 +4610,7 @@ function sgDrawSpectrum(context) {
|
|
|
4552
4610
|
ctx.stroke();
|
|
4553
4611
|
}
|
|
4554
4612
|
ctx.globalAlpha = 1;
|
|
4613
|
+
ctx.setLineDash([]);
|
|
4555
4614
|
ctx.restore();
|
|
4556
4615
|
return rect;
|
|
4557
4616
|
}
|
|
@@ -4562,7 +4621,6 @@ function sgSpectrumScale(spec, slice, windowLevel, freeY, traces) {
|
|
|
4562
4621
|
return { min: windowLevel.level - windowLevel.width / 2, max: windowLevel.level + windowLevel.width / 2 };
|
|
4563
4622
|
}
|
|
4564
4623
|
if (!spec || !slice) return { min: -80, max: 0 };
|
|
4565
|
-
var floorValue = sgValueFloor(spec);
|
|
4566
4624
|
var max = -Infinity;
|
|
4567
4625
|
var keys = [];
|
|
4568
4626
|
for (var t = 0; t < traces.length; t++) if (traces[t].visible) keys.push(traces[t].key);
|
|
@@ -4570,8 +4628,10 @@ function sgSpectrumScale(spec, slice, windowLevel, freeY, traces) {
|
|
|
4570
4628
|
for (var k = 0; k < keys.length; k++) {
|
|
4571
4629
|
var values = slice[keys[k]];
|
|
4572
4630
|
if (!values) continue;
|
|
4631
|
+
var floorValue = sgValueFloorFor(spec, keys[k]);
|
|
4632
|
+
var refDb = sgChannelRefDb(spec, keys[k]);
|
|
4573
4633
|
for (var i = 0; i < values.length; i++) {
|
|
4574
|
-
var db = sgToDb(values[i], floorValue);
|
|
4634
|
+
var db = sgToDb(values[i], floorValue) - refDb;
|
|
4575
4635
|
if (db > max) max = db;
|
|
4576
4636
|
}
|
|
4577
4637
|
}
|
|
@@ -4603,8 +4663,8 @@ function sgDetectHotBands(spec, bands, windowLevel, channel) {
|
|
|
4603
4663
|
var flags = [];
|
|
4604
4664
|
if (!spec || !spec.nTime || !bands || !bands.length) return flags;
|
|
4605
4665
|
var data = spec.data[channel] || spec.data.rss;
|
|
4606
|
-
var floorValue =
|
|
4607
|
-
var threshold = windowLevel.level + windowLevel.width / 2;
|
|
4666
|
+
var floorValue = sgValueFloorFor(spec, channel);
|
|
4667
|
+
var threshold = windowLevel.level + windowLevel.width / 2 + sgChannelRefDb(spec, channel);
|
|
4608
4668
|
for (var i = 0; i < bands.length; i++) {
|
|
4609
4669
|
var half = (bands[i].bwHz || 0) / 2;
|
|
4610
4670
|
var rowLow = Math.max(0, Math.floor((bands[i].freqHz - half - spec.fStartHz) / spec.fStepHz));
|
|
@@ -4636,7 +4696,12 @@ function sgSampleCell(spec, channel, timeSec, freqHz) {
|
|
|
4636
4696
|
gx: spec.data.gx[index],
|
|
4637
4697
|
gy: spec.data.gy[index],
|
|
4638
4698
|
gz: spec.data.gz[index],
|
|
4639
|
-
rss: spec.data[channel] ? spec.data[channel][index] : spec.data.rss[index]
|
|
4699
|
+
rss: spec.data[channel] ? spec.data[channel][index] : spec.data.rss[index],
|
|
4700
|
+
/* null, not 0, when RF was not computed: the readout must be able to tell
|
|
4701
|
+
"no RF proxy in this matrix" from "the RF proxy is silent here". */
|
|
4702
|
+
rfThermo: spec.data.rfThermo ? spec.data.rfThermo[index] : null,
|
|
4703
|
+
rfControl: spec.data.rfControl ? spec.data.rfControl[index] : null,
|
|
4704
|
+
rf: spec.data.rf ? spec.data.rf[index] : null
|
|
4640
4705
|
};
|
|
4641
4706
|
}
|
|
4642
4707
|
|
|
@@ -4989,7 +5054,30 @@ var SeqEyesPanel = (function () {
|
|
|
4989
5054
|
overlap: getNum('seqeyes.spectrogram.overlap', 0.75),
|
|
4990
5055
|
oversample: getNum('seqeyes.spectrogram.oversample', 3),
|
|
4991
5056
|
targetColumns: 256,
|
|
4992
|
-
normalize: true
|
|
5057
|
+
normalize: true,
|
|
5058
|
+
/* RF acoustic proxy (rfAcoustic.ts). Off by default and opt-in: these
|
|
5059
|
+
channels are uncalibrated research proxies with no unit, unlike every
|
|
5060
|
+
other channel in this panel. The two mechanism weights stay at the
|
|
5061
|
+
upstream 1:1 default — that ratio is a placeholder until it is fitted to
|
|
5062
|
+
measurements, so exposing it as a dial would suggest a meaning it has
|
|
5063
|
+
not earned. */
|
|
5064
|
+
includeRf: getBool('seqeyes.spectrogram.rf', false),
|
|
5065
|
+
rfScale: getNum('seqeyes.spectrogram.rfscale', 1),
|
|
5066
|
+
rfThermoWeight: 1,
|
|
5067
|
+
rfControlWeight: 1,
|
|
5068
|
+
rfEdgeMode: 'signed',
|
|
5069
|
+
/* Playback only: crossfade between the gradient sound and the RF proxy,
|
|
5070
|
+
each normalised to its own peak first. There is no correct default —
|
|
5071
|
+
the two have no common unit — so an even blend is the honest one. */
|
|
5072
|
+
rfMix: getNum('seqeyes.spectrogram.rfmix', 0.5)
|
|
5073
|
+
};
|
|
5074
|
+
/* Which matrix the image layer paints. The spectrum pane still draws every
|
|
5075
|
+
visible trace; this only picks the colourmapped one. */
|
|
5076
|
+
var imageChannel = get('seqeyes.spectrogram.imagechannel') || 'rss';
|
|
5077
|
+
var SG_RF_SLICE_KEYS = ['rfThermo', 'rfControl', 'rf'];
|
|
5078
|
+
var SG_IMAGE_CHANNEL_LABELS = {
|
|
5079
|
+
rss: 'Σ', gx: 'Gx', gy: 'Gy', gz: 'Gz',
|
|
5080
|
+
rf: 'RF proxy', rfThermo: 'RF thermo', rfControl: 'RF ctrl'
|
|
4993
5081
|
};
|
|
4994
5082
|
var colormapName = get('seqeyes.spectrogram.colormap') || 'viridis';
|
|
4995
5083
|
if (SG_COLORMAP_NAMES.indexOf(colormapName) < 0) colormapName = 'viridis';
|
|
@@ -4998,7 +5086,10 @@ var SeqEyesPanel = (function () {
|
|
|
4998
5086
|
{ key: 'gx', label: 'Gx', varName: '--gx', visible: getBool('seqeyes.spectrogram.trace.gx', true) },
|
|
4999
5087
|
{ key: 'gy', label: 'Gy', varName: '--gy', visible: getBool('seqeyes.spectrogram.trace.gy', true) },
|
|
5000
5088
|
{ key: 'gz', label: 'Gz', varName: '--gz', visible: getBool('seqeyes.spectrogram.trace.gz', true) },
|
|
5001
|
-
{ key: 'rss', label: 'Σ', varName: '--fg', visible: getBool('seqeyes.spectrogram.trace.rss', true) }
|
|
5089
|
+
{ key: 'rss', label: 'Σ', varName: '--fg', visible: getBool('seqeyes.spectrogram.trace.rss', true) },
|
|
5090
|
+
/* Drawn only while the matrix carries them, i.e. while RF is enabled. */
|
|
5091
|
+
{ key: 'rfThermo', label: 'RF thermo', varName: '--rf', rf: true, visible: getBool('seqeyes.spectrogram.trace.rfThermo', true) },
|
|
5092
|
+
{ key: 'rfControl', label: 'RF ctrl', varName: '--tr', rf: true, visible: getBool('seqeyes.spectrogram.trace.rfControl', true) }
|
|
5002
5093
|
];
|
|
5003
5094
|
var freeY = getBool('seqeyes.spectrogram.freeY', false);
|
|
5004
5095
|
var bandsVisible = getBool('seqeyes.spectrogram.bands', true);
|
|
@@ -5031,6 +5122,11 @@ var SeqEyesPanel = (function () {
|
|
|
5031
5122
|
var audioActivationId = 0;
|
|
5032
5123
|
var audioActivationPending = false;
|
|
5033
5124
|
var audioWindow = null; // buffered range plus the first-pass offset
|
|
5125
|
+
/* What the host actually put in the last audio buffer. The panel asks for
|
|
5126
|
+
RF, but only the reply says whether any arrived, and the two lanes reach
|
|
5127
|
+
the synthesiser by different routes — so this is reported rather than
|
|
5128
|
+
assumed from `params.includeRf`. */
|
|
5129
|
+
var audioRfIncluded = null;
|
|
5034
5130
|
var playheadFrame = 0;
|
|
5035
5131
|
|
|
5036
5132
|
var AUDIO_SAMPLE_RATE = 44100;
|
|
@@ -5237,7 +5333,9 @@ var SeqEyesPanel = (function () {
|
|
|
5237
5333
|
|
|
5238
5334
|
function paramsSignature() {
|
|
5239
5335
|
return [params.source, params.fMaxHz, params.windowSamples, params.overlap,
|
|
5240
|
-
params.oversample, params.targetColumns, params.normalize ? 1 : 0
|
|
5336
|
+
params.oversample, params.targetColumns, params.normalize ? 1 : 0,
|
|
5337
|
+
params.includeRf ? 1 : 0, params.rfScale, params.rfThermoWeight,
|
|
5338
|
+
params.rfControlWeight, params.rfEdgeMode].join('|');
|
|
5241
5339
|
}
|
|
5242
5340
|
|
|
5243
5341
|
function cacheKey(startSec, endSec) {
|
|
@@ -5299,6 +5397,11 @@ var SeqEyesPanel = (function () {
|
|
|
5299
5397
|
oversample: params.oversample,
|
|
5300
5398
|
targetColumns: params.targetColumns,
|
|
5301
5399
|
normalize: params.normalize,
|
|
5400
|
+
includeRf: params.includeRf,
|
|
5401
|
+
rfScale: params.rfScale,
|
|
5402
|
+
rfThermoWeight: params.rfThermoWeight,
|
|
5403
|
+
rfControlWeight: params.rfControlWeight,
|
|
5404
|
+
rfEdgeMode: params.rfEdgeMode,
|
|
5302
5405
|
trTimeSec: h.getTrTimeSec ? h.getTrTimeSec() : 0
|
|
5303
5406
|
});
|
|
5304
5407
|
}
|
|
@@ -5335,13 +5438,29 @@ var SeqEyesPanel = (function () {
|
|
|
5335
5438
|
render();
|
|
5336
5439
|
}
|
|
5337
5440
|
|
|
5441
|
+
/**
|
|
5442
|
+
* Image channel that is actually present in the current matrix.
|
|
5443
|
+
*
|
|
5444
|
+
* A remembered `rf` preference outlives the RF toggle and survives a reload
|
|
5445
|
+
* into a matrix that has no RF channels, so the fallback is not defensive
|
|
5446
|
+
* padding — it is the normal path the first time the panel opens with RF off.
|
|
5447
|
+
*/
|
|
5448
|
+
function activeImageChannel() {
|
|
5449
|
+
if (currentSpec && currentSpec.data && currentSpec.data[imageChannel]) return imageChannel;
|
|
5450
|
+
return 'rss';
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5338
5453
|
function applySpectrogram(spec) {
|
|
5339
5454
|
currentSpec = spec;
|
|
5340
5455
|
currentView = { startSec: spec.requestedStartSec, endSec: spec.requestedEndSec };
|
|
5341
|
-
if (windowLevelAuto) windowLevel = sgAutoWindowLevel(spec,
|
|
5456
|
+
if (windowLevelAuto) windowLevel = sgAutoWindowLevel(spec, activeImageChannel(), windowLevel);
|
|
5342
5457
|
clampFrequencyRange();
|
|
5343
5458
|
imageDirty = true;
|
|
5344
|
-
hotBands = sgDetectHotBands(spec, acousticBands, windowLevel,
|
|
5459
|
+
hotBands = sgDetectHotBands(spec, acousticBands, windowLevel, activeImageChannel());
|
|
5460
|
+
// The RF chips and the image-channel options both depend on whether this
|
|
5461
|
+
// matrix carries RF, which is only known once it arrives.
|
|
5462
|
+
buildLegend();
|
|
5463
|
+
syncControls();
|
|
5345
5464
|
publishNotices();
|
|
5346
5465
|
render();
|
|
5347
5466
|
if (requestStartedAt) {
|
|
@@ -5384,9 +5503,18 @@ var SeqEyesPanel = (function () {
|
|
|
5384
5503
|
var anyHot = false;
|
|
5385
5504
|
for (var k = 0; k < hotBands.length; k++) if (hotBands[k]) anyHot = true;
|
|
5386
5505
|
if (anyHot) {
|
|
5387
|
-
|
|
5506
|
+
// The detection runs on whichever channel the image shows, so the wording
|
|
5507
|
+
// has to name that channel rather than always claiming "gradient".
|
|
5508
|
+
messages.push((sgIsRfChannel(activeImageChannel()) ? 'RF proxy energy' : 'Gradient energy')
|
|
5509
|
+
+ ' falls inside a forbidden acoustic band (advisory: '
|
|
5388
5510
|
+ 'this compares against the current display window, and is not a compliance check).');
|
|
5389
5511
|
}
|
|
5512
|
+
if (currentSpec && currentSpec.rfIncluded) {
|
|
5513
|
+
messages.push('RF acoustic channels are a reduced-order research proxy '
|
|
5514
|
+
+ '(thermoacoustic d|B1|²/dt plus a hypothesised TX-switching term), with no calibrated '
|
|
5515
|
+
+ 'level and no scanner resonance model. They are shown on their own relative scale and '
|
|
5516
|
+
+ 'must not be compared with, or added to, the gradient channels.');
|
|
5517
|
+
}
|
|
5390
5518
|
notice('spectrogram', messages.length ? messages : null);
|
|
5391
5519
|
}
|
|
5392
5520
|
|
|
@@ -5417,7 +5545,7 @@ var SeqEyesPanel = (function () {
|
|
|
5417
5545
|
if (imageDirty || !imageCache || imageCache.width !== pixelW || imageCache.height !== pixelH) {
|
|
5418
5546
|
imageCache = sgBuildImageData({
|
|
5419
5547
|
spectrogram: currentSpec,
|
|
5420
|
-
channel:
|
|
5548
|
+
channel: activeImageChannel(),
|
|
5421
5549
|
lut: lut,
|
|
5422
5550
|
windowLevel: windowLevel,
|
|
5423
5551
|
view: currentView,
|
|
@@ -5512,9 +5640,22 @@ var SeqEyesPanel = (function () {
|
|
|
5512
5640
|
out.gz[row] = spec.data.gz[index];
|
|
5513
5641
|
out.rss[row] = spec.data.rss[index];
|
|
5514
5642
|
}
|
|
5643
|
+
addRfSliceColumns(out, spec, col);
|
|
5515
5644
|
return out;
|
|
5516
5645
|
}
|
|
5517
5646
|
|
|
5647
|
+
/** Copy the RF rows of one column into a slice, when the matrix has them. */
|
|
5648
|
+
function addRfSliceColumns(out, spec, col) {
|
|
5649
|
+
for (var i = 0; i < SG_RF_SLICE_KEYS.length; i++) {
|
|
5650
|
+
var key = SG_RF_SLICE_KEYS[i];
|
|
5651
|
+
var source = spec.data[key];
|
|
5652
|
+
if (!source) continue;
|
|
5653
|
+
var target = new Float32Array(spec.nFreq);
|
|
5654
|
+
for (var row = 0; row < spec.nFreq; row++) target[row] = source[row * spec.nTime + col];
|
|
5655
|
+
out[key] = target;
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5518
5659
|
var averageCache = null;
|
|
5519
5660
|
function sliceAverage() {
|
|
5520
5661
|
var spec = currentSpec;
|
|
@@ -5538,6 +5679,21 @@ var SeqEyesPanel = (function () {
|
|
|
5538
5679
|
out.gx[row] = rx; out.gy[row] = ry; out.gz[row] = rz;
|
|
5539
5680
|
out.rss[row] = Math.sqrt(rx * rx + ry * ry + rz * rz);
|
|
5540
5681
|
}
|
|
5682
|
+
// The RF channels average on their own. `rss` is rebuilt from the per-axis
|
|
5683
|
+
// means above because it is a combination of three signals; `rf` is already
|
|
5684
|
+
// the transform of one combined signal, so it is simply averaged.
|
|
5685
|
+
for (var i = 0; i < SG_RF_SLICE_KEYS.length; i++) {
|
|
5686
|
+
var key = SG_RF_SLICE_KEYS[i];
|
|
5687
|
+
var source = spec.data[key];
|
|
5688
|
+
if (!source) continue;
|
|
5689
|
+
var target = new Float32Array(spec.nFreq);
|
|
5690
|
+
for (var r = 0; r < spec.nFreq; r++) {
|
|
5691
|
+
var b = r * spec.nTime, acc = 0;
|
|
5692
|
+
for (var c = 0; c < spec.nTime; c++) { var v = source[b + c]; acc += v * v; }
|
|
5693
|
+
target[r] = Math.sqrt(acc / spec.nTime);
|
|
5694
|
+
}
|
|
5695
|
+
out[key] = target;
|
|
5696
|
+
}
|
|
5541
5697
|
averageCache = { spec: spec, slice: out };
|
|
5542
5698
|
return out;
|
|
5543
5699
|
}
|
|
@@ -5553,7 +5709,9 @@ var SeqEyesPanel = (function () {
|
|
|
5553
5709
|
for (var i = 0; i < traces.length; i++) {
|
|
5554
5710
|
resolved.push({
|
|
5555
5711
|
key: traces[i].key,
|
|
5556
|
-
|
|
5712
|
+
// An RF trace the matrix does not carry is not drawn, whatever the
|
|
5713
|
+
// remembered legend state says.
|
|
5714
|
+
visible: traces[i].visible && (!traces[i].rf || !!(slice && slice[traces[i].key])),
|
|
5557
5715
|
color: (style.getPropertyValue(traces[i].varName) || '#888').trim()
|
|
5558
5716
|
});
|
|
5559
5717
|
}
|
|
@@ -5582,13 +5740,25 @@ var SeqEyesPanel = (function () {
|
|
|
5582
5740
|
if (currentSpec && currentSpec.nTime) {
|
|
5583
5741
|
parts.push('dt ' + sgFmtResolution(currentSpec.dtResolutionSec));
|
|
5584
5742
|
parts.push('df ' + sgFmtHz(currentSpec.dfResolutionHz) + ' Hz');
|
|
5585
|
-
|
|
5743
|
+
var channel = activeImageChannel();
|
|
5744
|
+
parts.push('img ' + (SG_IMAGE_CHANNEL_LABELS[channel] || channel));
|
|
5745
|
+
// The unit belongs to the channel on screen, so an RF image must not be
|
|
5746
|
+
// labelled mT/m.
|
|
5747
|
+
parts.push('unit ' + (sgIsRfChannel(channel) ? 'relative (0 dB = RF peak in view)' : currentSpec.unit));
|
|
5586
5748
|
parts.push('D ' + currentSpec.decimationFactor + '×');
|
|
5587
5749
|
parts.push(currentSpec.nTime + '×' + currentSpec.nFreq);
|
|
5588
5750
|
parts.push('W ' + windowLevel.width.toFixed(1) + ' dB L ' + sgFmtDb(windowLevel.level) + ' dB');
|
|
5589
5751
|
} else if (busy) {
|
|
5590
5752
|
parts.push('Calculating…');
|
|
5591
5753
|
}
|
|
5754
|
+
if (params.includeRf && audioRfIncluded !== null) {
|
|
5755
|
+
// Says whether the buffer being played actually contains RF, which is not
|
|
5756
|
+
// the same question as whether the RF channels were requested — and at
|
|
5757
|
+
// what share, since a low mix is the other way RF goes unnoticed.
|
|
5758
|
+
parts.push(audioRfIncluded
|
|
5759
|
+
? 'audio RF ✓ ' + Math.round(params.rfMix * 100) + '%'
|
|
5760
|
+
: 'audio RF ✗ (none reached the buffer)');
|
|
5761
|
+
}
|
|
5592
5762
|
if (isFinite(playheadTimeSec)) parts.push('▶ ' + sgFmtTime(playheadTimeSec, timeUnit()));
|
|
5593
5763
|
else if (isFinite(markerTimeSec)) parts.push('marker ' + sgFmtTime(markerTimeSec, timeUnit()));
|
|
5594
5764
|
else if (currentSpec && currentSpec.nTime) {
|
|
@@ -5596,7 +5766,11 @@ var SeqEyesPanel = (function () {
|
|
|
5596
5766
|
}
|
|
5597
5767
|
if (hoverReadout) parts.push(hoverReadout);
|
|
5598
5768
|
out.textContent = parts.join(' ');
|
|
5599
|
-
out.title =
|
|
5769
|
+
out.title = currentSpec && currentSpec.rfIncluded
|
|
5770
|
+
? 'Simulated gradient spectral content — not calibrated sound pressure level. '
|
|
5771
|
+
+ 'The RF channels are an uncalibrated research proxy on a separate relative scale; '
|
|
5772
|
+
+ 'they cannot be compared with the gradient channels or summed with them.'
|
|
5773
|
+
: 'Simulated gradient spectral content — not calibrated sound pressure level.';
|
|
5600
5774
|
}
|
|
5601
5775
|
|
|
5602
5776
|
var hoverReadout = '';
|
|
@@ -5631,15 +5805,15 @@ var SeqEyesPanel = (function () {
|
|
|
5631
5805
|
};
|
|
5632
5806
|
windowLevelAuto = false;
|
|
5633
5807
|
imageDirty = true;
|
|
5634
|
-
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel,
|
|
5808
|
+
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel());
|
|
5635
5809
|
render();
|
|
5636
5810
|
}
|
|
5637
5811
|
|
|
5638
5812
|
function resetWindowLevel() {
|
|
5639
5813
|
windowLevelAuto = true;
|
|
5640
|
-
if (currentSpec) windowLevel = sgAutoWindowLevel(currentSpec,
|
|
5814
|
+
if (currentSpec) windowLevel = sgAutoWindowLevel(currentSpec, activeImageChannel(), windowLevel);
|
|
5641
5815
|
imageDirty = true;
|
|
5642
|
-
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel,
|
|
5816
|
+
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel());
|
|
5643
5817
|
render();
|
|
5644
5818
|
}
|
|
5645
5819
|
|
|
@@ -5724,7 +5898,13 @@ var SeqEyesPanel = (function () {
|
|
|
5724
5898
|
h.requestAudio(pendingAudioId, range.startSec, range.endSec, {
|
|
5725
5899
|
sampleRate: 44100,
|
|
5726
5900
|
source: params.source,
|
|
5727
|
-
channelWeights: [1, 1, 1]
|
|
5901
|
+
channelWeights: [1, 1, 1],
|
|
5902
|
+
includeRf: params.includeRf,
|
|
5903
|
+
rfScale: params.rfScale,
|
|
5904
|
+
rfThermoWeight: params.rfThermoWeight,
|
|
5905
|
+
rfControlWeight: params.rfControlWeight,
|
|
5906
|
+
rfEdgeMode: params.rfEdgeMode,
|
|
5907
|
+
rfMix: params.rfMix
|
|
5728
5908
|
});
|
|
5729
5909
|
syncTransport();
|
|
5730
5910
|
}
|
|
@@ -5777,16 +5957,28 @@ var SeqEyesPanel = (function () {
|
|
|
5777
5957
|
function deliverAudio(id, payload) {
|
|
5778
5958
|
if (id !== pendingAudioId) return;
|
|
5779
5959
|
pendingAudioId = 0;
|
|
5960
|
+
// The host knows which sources were enabled, so it words the refusal; the
|
|
5961
|
+
// panel used to say "no gradient activity" even when RF was the source the
|
|
5962
|
+
// user was listening for.
|
|
5963
|
+
var emptyMessage = params.includeRf
|
|
5964
|
+
? 'Nothing to play in this window — no gradient activity and no RF events.'
|
|
5965
|
+
: 'No gradient activity in this window — nothing to play.';
|
|
5780
5966
|
if (!payload || !payload.left || !payload.left.length) {
|
|
5781
|
-
notice('gradientSound',
|
|
5967
|
+
notice('gradientSound', (payload && payload.warnings && payload.warnings.length)
|
|
5968
|
+
? payload.warnings : emptyMessage);
|
|
5782
5969
|
syncTransport();
|
|
5783
5970
|
return;
|
|
5784
5971
|
}
|
|
5785
5972
|
if (payload.silent) {
|
|
5786
|
-
notice('gradientSound',
|
|
5973
|
+
notice('gradientSound', (payload.warnings && payload.warnings.length)
|
|
5974
|
+
? payload.warnings : emptyMessage);
|
|
5787
5975
|
syncTransport();
|
|
5788
5976
|
return;
|
|
5789
5977
|
}
|
|
5978
|
+
// Partial-source notes ("hearing the RF proxy only") are advisory, not a
|
|
5979
|
+
// refusal, so they are published without stopping playback.
|
|
5980
|
+
notice('gradientSound', (payload.warnings && payload.warnings.length) ? payload.warnings : null);
|
|
5981
|
+
audioRfIncluded = !!payload.rfIncluded;
|
|
5790
5982
|
if (!SeqEyesAudio.load(payload.sampleRate, payload.left, payload.right, payload.startSec)) {
|
|
5791
5983
|
if (SeqEyesAudio.contextState() !== 'running') audioActivationFailure();
|
|
5792
5984
|
else notice('gradientSound', 'This host could not create an audio buffer.');
|
|
@@ -5846,6 +6038,7 @@ var SeqEyesPanel = (function () {
|
|
|
5846
6038
|
playheadTimeSec = NaN;
|
|
5847
6039
|
pendingAudioId = 0;
|
|
5848
6040
|
audioWindow = null;
|
|
6041
|
+
audioRfIncluded = null;
|
|
5849
6042
|
syncTransport();
|
|
5850
6043
|
render();
|
|
5851
6044
|
}
|
|
@@ -5901,7 +6094,7 @@ var SeqEyesPanel = (function () {
|
|
|
5901
6094
|
|
|
5902
6095
|
function setAcousticBands(bands) {
|
|
5903
6096
|
acousticBands = Array.isArray(bands) ? bands.slice() : [];
|
|
5904
|
-
hotBands = currentSpec ? sgDetectHotBands(currentSpec, acousticBands, windowLevel,
|
|
6097
|
+
hotBands = currentSpec ? sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel()) : [];
|
|
5905
6098
|
buildLegend();
|
|
5906
6099
|
publishNotices();
|
|
5907
6100
|
render();
|
|
@@ -5916,10 +6109,15 @@ var SeqEyesPanel = (function () {
|
|
|
5916
6109
|
var style = getComputedStyle(document.body);
|
|
5917
6110
|
|
|
5918
6111
|
for (var i = 0; i < traces.length; i++) {
|
|
6112
|
+
// RF chips appear only once the matrix carries RF, so the legend never
|
|
6113
|
+
// offers a toggle that would do nothing.
|
|
6114
|
+
if (traces[i].rf && !(currentSpec && currentSpec.data && currentSpec.data[traces[i].key])) continue;
|
|
5919
6115
|
(function (trace) {
|
|
5920
6116
|
var chip = document.createElement('div');
|
|
5921
6117
|
chip.className = 'li' + (trace.visible ? '' : ' off');
|
|
5922
|
-
chip.title =
|
|
6118
|
+
chip.title = trace.rf
|
|
6119
|
+
? 'Toggle the ' + trace.label + ' trace (uncalibrated proxy, dB relative to its own peak in this view)'
|
|
6120
|
+
: 'Toggle the ' + trace.label + ' spectrum trace';
|
|
5923
6121
|
var swatch = document.createElement('div');
|
|
5924
6122
|
swatch.className = 'ld';
|
|
5925
6123
|
swatch.style.background = (style.getPropertyValue(trace.varName) || '#888').trim();
|
|
@@ -5990,6 +6188,32 @@ var SeqEyesPanel = (function () {
|
|
|
5990
6188
|
setValue('sgWin', String(params.windowSamples));
|
|
5991
6189
|
setValue('sgOverlap', String(params.overlap));
|
|
5992
6190
|
setValue('sgOversample', String(params.oversample));
|
|
6191
|
+
|
|
6192
|
+
var rfToggle = el('sgRf');
|
|
6193
|
+
if (rfToggle) {
|
|
6194
|
+
rfToggle.setAttribute('aria-pressed', params.includeRf ? 'true' : 'false');
|
|
6195
|
+
rfToggle.classList.toggle('on', params.includeRf);
|
|
6196
|
+
}
|
|
6197
|
+
var rfScale = el('sgRfScale');
|
|
6198
|
+
if (rfScale) {
|
|
6199
|
+
setValue('sgRfScale', String(params.rfScale));
|
|
6200
|
+
rfScale.disabled = !params.includeRf;
|
|
6201
|
+
}
|
|
6202
|
+
var rfMix = el('sgRfMix');
|
|
6203
|
+
if (rfMix) {
|
|
6204
|
+
setValue('sgRfMix', String(Math.round(params.rfMix * 100)));
|
|
6205
|
+
rfMix.disabled = !params.includeRf;
|
|
6206
|
+
}
|
|
6207
|
+
var image = el('sgImageChannel');
|
|
6208
|
+
if (image) {
|
|
6209
|
+
// RF options are removed rather than disabled: a disabled option that the
|
|
6210
|
+
// select is currently showing is a state the user cannot get out of.
|
|
6211
|
+
for (var o = image.options.length - 1; o >= 0; o--) {
|
|
6212
|
+
if (sgIsRfChannel(image.options[o].value)) image.options[o].hidden = !params.includeRf;
|
|
6213
|
+
}
|
|
6214
|
+
image.value = activeImageChannel();
|
|
6215
|
+
}
|
|
6216
|
+
|
|
5993
6217
|
var clear = el('sgMarkerClear');
|
|
5994
6218
|
if (clear) clear.disabled = !isFinite(markerTimeSec);
|
|
5995
6219
|
}
|
|
@@ -6103,6 +6327,56 @@ var SeqEyesPanel = (function () {
|
|
|
6103
6327
|
onParamChanged(true);
|
|
6104
6328
|
};
|
|
6105
6329
|
|
|
6330
|
+
var image = el('sgImageChannel');
|
|
6331
|
+
if (image) image.onchange = function () {
|
|
6332
|
+
imageChannel = this.value;
|
|
6333
|
+
set('seqeyes.spectrogram.imagechannel', imageChannel);
|
|
6334
|
+
// Channels live on different scales, so a channel change invalidates the
|
|
6335
|
+
// contrast as thoroughly as new data would.
|
|
6336
|
+
windowLevelAuto = true;
|
|
6337
|
+
if (currentSpec) windowLevel = sgAutoWindowLevel(currentSpec, activeImageChannel(), windowLevel);
|
|
6338
|
+
if (currentSpec) hotBands = sgDetectHotBands(currentSpec, acousticBands, windowLevel, activeImageChannel());
|
|
6339
|
+
imageDirty = true;
|
|
6340
|
+
publishNotices();
|
|
6341
|
+
render();
|
|
6342
|
+
};
|
|
6343
|
+
|
|
6344
|
+
// Each RF control changes what is being synthesised, so a buffer already
|
|
6345
|
+
// playing is stale the moment one moves.
|
|
6346
|
+
var rfToggle = el('sgRf');
|
|
6347
|
+
if (rfToggle) rfToggle.onclick = function () {
|
|
6348
|
+
stopPlayback();
|
|
6349
|
+
params.includeRf = !params.includeRf;
|
|
6350
|
+
set('seqeyes.spectrogram.rf', params.includeRf ? '1' : '0');
|
|
6351
|
+
if (!params.includeRf && sgIsRfChannel(imageChannel)) {
|
|
6352
|
+
imageChannel = 'rss';
|
|
6353
|
+
set('seqeyes.spectrogram.imagechannel', imageChannel);
|
|
6354
|
+
}
|
|
6355
|
+
onParamChanged(true);
|
|
6356
|
+
buildLegend();
|
|
6357
|
+
};
|
|
6358
|
+
|
|
6359
|
+
var rfScale = el('sgRfScale');
|
|
6360
|
+
if (rfScale) rfScale.onchange = function () {
|
|
6361
|
+
stopPlayback();
|
|
6362
|
+
var value = parseFloat(this.value);
|
|
6363
|
+
params.rfScale = isFinite(value) && value >= 0 ? value : 1;
|
|
6364
|
+
set('seqeyes.spectrogram.rfscale', String(params.rfScale));
|
|
6365
|
+
onParamChanged(true);
|
|
6366
|
+
};
|
|
6367
|
+
|
|
6368
|
+
var rfMix = el('sgRfMix');
|
|
6369
|
+
if (rfMix) rfMix.oninput = function () {
|
|
6370
|
+
stopPlayback();
|
|
6371
|
+
var value = parseFloat(this.value);
|
|
6372
|
+
params.rfMix = isFinite(value) ? Math.max(0, Math.min(1, value / 100)) : 0.5;
|
|
6373
|
+
set('seqeyes.spectrogram.rfmix', String(params.rfMix));
|
|
6374
|
+
// Playback only: the spectrogram keeps the mechanisms on separate
|
|
6375
|
+
// channels, so this must not trigger a recompute.
|
|
6376
|
+
syncControls();
|
|
6377
|
+
syncTransport();
|
|
6378
|
+
};
|
|
6379
|
+
|
|
6106
6380
|
var win = el('sgWin');
|
|
6107
6381
|
if (win) win.onchange = function () { params.windowSamples = parseFloat(this.value) || 0; onParamChanged(true); };
|
|
6108
6382
|
var overlap = el('sgOverlap');
|
|
@@ -6351,6 +6625,15 @@ var SeqEyesPanel = (function () {
|
|
|
6351
6625
|
+ ' Gy ' + sgFmtDb(sgToDb(cell.gy, floorValue))
|
|
6352
6626
|
+ ' Gz ' + sgFmtDb(sgToDb(cell.gz, floorValue))
|
|
6353
6627
|
+ ' Σ ' + sgFmtDb(sgToDb(cell.rss, floorValue)) + ' dB';
|
|
6628
|
+
if (cell.rf !== null) {
|
|
6629
|
+
// Reported against the RF peak, matching the axis the RF traces are drawn
|
|
6630
|
+
// on, and flagged `rel` so it is never read as dB re 1 mT/m.
|
|
6631
|
+
var rfFloor = sgValueFloorFor(currentSpec, 'rf');
|
|
6632
|
+
var rfRef = sgChannelRefDb(currentSpec, 'rf');
|
|
6633
|
+
text += ' RF ' + sgFmtDb(sgToDb(cell.rf, rfFloor) - rfRef)
|
|
6634
|
+
+ ' (th ' + sgFmtDb(sgToDb(cell.rfThermo, rfFloor) - rfRef)
|
|
6635
|
+
+ ', ct ' + sgFmtDb(sgToDb(cell.rfControl, rfFloor) - rfRef) + ') dB rel';
|
|
6636
|
+
}
|
|
6354
6637
|
var bandIndex = sgBandAtFrequency(acousticBands, cell.freqHz);
|
|
6355
6638
|
if (bandIndex >= 0) {
|
|
6356
6639
|
var band = acousticBands[bandIndex];
|
|
@@ -6591,6 +6874,13 @@ var SeqEyesPanel = (function () {
|
|
|
6591
6874
|
dtResolutionSec: currentSpec ? currentSpec.dtResolutionSec : null,
|
|
6592
6875
|
dfResolutionHz: currentSpec ? currentSpec.dfResolutionHz : null,
|
|
6593
6876
|
decimationFactor: currentSpec ? currentSpec.decimationFactor : null,
|
|
6877
|
+
includeRf: params.includeRf,
|
|
6878
|
+
rfScale: params.rfScale,
|
|
6879
|
+
rfMix: params.rfMix,
|
|
6880
|
+
audioRfIncluded: audioRfIncluded,
|
|
6881
|
+
imageChannel: activeImageChannel(),
|
|
6882
|
+
rfIncluded: !!(currentSpec && currentSpec.rfIncluded),
|
|
6883
|
+
rfMaxValue: currentSpec ? (currentSpec.rfMaxValue || 0) : 0,
|
|
6594
6884
|
warnings: currentSpec ? currentSpec.warnings.slice() : [],
|
|
6595
6885
|
error: lastError
|
|
6596
6886
|
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|