• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Project-OSmOSE / OSEkit / 19831033427

01 Dec 2025 05:08PM UTC coverage: 96.981% (+0.03%) from 96.955%
19831033427

Pull #307

github

web-flow
Merge 92d3f5588 into 27d210a34
Pull Request #307: [DRAFT] Relative paths

41 of 42 new or added lines in 6 files covered. (97.62%)

13 existing lines in 5 files now uncovered.

3951 of 4074 relevant lines covered (96.98%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.33
/src/osekit/core_api/spectro_data.py
1
"""SpectroData represent spectrogram data retrieved from SpectroFiles.
2

3
The SpectroData has a collection of SpectroItem.
4
The data is accessed via a SpectroItem object per SpectroFile.
5
"""
6

7
from __future__ import annotations
1✔
8

9
import gc
1✔
10
from typing import TYPE_CHECKING, Literal
1✔
11

12
import matplotlib.pyplot as plt
1✔
13
import numpy as np
1✔
14
import pandas as pd
1✔
15
from matplotlib.dates import date2num
1✔
16
from scipy.signal import ShortTimeFFT, welch
1✔
17

18
from osekit.config import (
1✔
19
    TIMESTAMP_FORMATS_EXPORTED_FILES,
20
)
21
from osekit.core_api.audio_data import AudioData
1✔
22
from osekit.core_api.base_data import BaseData
1✔
23
from osekit.core_api.spectro_file import SpectroFile
1✔
24
from osekit.core_api.spectro_item import SpectroItem
1✔
25

26
if TYPE_CHECKING:
27
    from pathlib import Path
28

29
    from pandas import Timestamp
30

31
    from osekit.core_api.frequency_scale import Scale
32

33

34
class SpectroData(BaseData[SpectroItem, SpectroFile]):
1✔
35
    """SpectroData represent Spectro data scattered through different SpectroFiles.
36

37
    The SpectroData has a collection of SpectroItem.
38
    The data is accessed via a SpectroItem object per SpectroFile.
39
    """
40

41
    def __init__(
1✔
42
        self,
43
        items: list[SpectroItem] | None = None,
44
        audio_data: AudioData = None,
45
        begin: Timestamp | None = None,
46
        end: Timestamp | None = None,
47
        fft: ShortTimeFFT | None = None,
48
        db_ref: float | None = None,
49
        v_lim: tuple[float, float] | None = None,
50
        colormap: str | None = None,
51
    ) -> None:
52
        """Initialize a SpectroData from a list of SpectroItems.
53

54
        Parameters
55
        ----------
56
        items: list[SpectroItem]
57
            List of the SpectroItem constituting the SpectroData.
58
        audio_data: AudioData
59
            The audio data from which to compute the spectrogram.
60
        begin: Timestamp | None
61
            Only effective if items is None.
62
            Set the begin of the empty data.
63
        end: Timestamp | None
64
            Only effective if items is None.
65
            Set the end of the empty data.
66
        fft: ShortTimeFFT
67
            The short time FFT used for computing the spectrogram.
68
        db_ref: float | None
69
            Reference value for computing sx values in decibel.
70
        v_lim: tuple[float,float]
71
            Lower and upper limits (in dB) of the colormap used
72
            for plotting the spectrogram.
73
        colormap: str
74
            Colormap to use for plotting the spectrogram.
75

76
        """
77
        super().__init__(items=items, begin=begin, end=end)
1✔
78
        self.audio_data = audio_data
1✔
79
        self.fft = fft
1✔
80
        self._sx_dtype = complex
1✔
81
        self._db_ref = db_ref
1✔
82
        self.v_lim = v_lim
1✔
83
        self.colormap = "viridis" if colormap is None else colormap
1✔
84

85
    @staticmethod
1✔
86
    def get_default_ax() -> plt.Axes:
1✔
87
        """Return a default-formatted Axes on a new figure.
88

89
        The default osekit spectrograms are plotted on wide, borderless spectrograms.
90
        This method set the default figure and axes parameters.
91

92
        Returns
93
        -------
94
        plt.Axes:
95
            The default Axes on a new figure.
96

97
        """
98
        # Legacy OSEkit behaviour.
99
        _, ax = plt.subplots(
1✔
100
            nrows=1,
101
            ncols=1,
102
            figsize=(1.3 * 1800 / 100, 1.3 * 512 / 100),
103
            dpi=100,
104
        )
105

106
        ax.get_xaxis().set_visible(False)
1✔
107
        ax.get_yaxis().set_visible(False)
1✔
108
        ax.set_frame_on(False)
1✔
109
        ax.spines["right"].set_visible(False)
1✔
110
        ax.spines["left"].set_visible(False)
1✔
111
        ax.spines["bottom"].set_visible(False)
1✔
112
        ax.spines["top"].set_visible(False)
1✔
113
        plt.axis("off")
1✔
114
        plt.subplots_adjust(
1✔
115
            top=1,
116
            bottom=0,
117
            right=1,
118
            left=0,
119
            hspace=0,
120
            wspace=0,
121
        )
122
        return ax
1✔
123

124
    @property
1✔
125
    def shape(self) -> tuple[int, ...]:
1✔
126
        """Shape of the Spectro data."""
127
        return self.fft.f_pts, self.fft.p_num(
1✔
128
            int(self.fft.fs * self.duration.total_seconds()),
129
        )
130

131
    @property
1✔
132
    def nb_bytes(self) -> int:
1✔
133
        """Total bytes consumed by the spectro values."""
134
        nb_bytes_per_cell = 16 if self.sx_dtype is complex else 8
×
135
        return self.shape[0] * self.shape[1] * nb_bytes_per_cell
×
136

137
    @property
1✔
138
    def sx_dtype(self) -> type[complex]:
1✔
139
        """Data type used to represent the sx values. Should either be float or complex.
140

141
        If complex, the phase info will be included in the computed spectrum.
142
        If float, only the absolute value of the spectrum will be kept.
143

144
        """
145
        return self._sx_dtype
1✔
146

147
    @sx_dtype.setter
1✔
148
    def sx_dtype(self, dtype: type[complex]) -> [complex, float]:
1✔
149
        if dtype not in (complex, float):
1✔
150
            raise ValueError("dtype must be complex or float.")
×
151
        self._sx_dtype = dtype
1✔
152

153
    @property
1✔
154
    def db_ref(self) -> float:
1✔
155
        """Reference value for computing sx values in decibel.
156

157
        If no reference is specified (self._db_ref is None), the
158
        sx db values will be given in dB FS.
159
        """
160
        db_type = self.db_type
1✔
161
        if db_type == "SPL_parameter":
1✔
162
            return self._db_ref
1✔
163
        if db_type == "SPL_instrument":
1✔
164
            return self.audio_data.instrument.P_REF
1✔
165
        return 1.0
1✔
166

167
    @db_ref.setter
1✔
168
    def db_ref(self, db_ref: float) -> None:
1✔
169
        self._db_ref = db_ref
×
170

171
    @property
1✔
172
    def db_type(self) -> Literal["FS", "SPL_instrument", "SPL_parameter"]:
1✔
173
        """Return whether the spectrogram dB values are in dB FS or dB SPL.
174

175
        Returns
176
        -------
177
        Literal["FS", "SPL_instrument", "SPL_parameter"]:
178
            "FS": The values are expressed in dB FS.
179
            "SPL_instrument": The values are expressed in dB SPL relative to the
180
                linked AudioData instrument P_REF property.
181
            "SPL_parameter": The values are expressed in dB SPL relative to the
182
                self._db_ref field.
183

184
        """
185
        if self._db_ref is not None:
1✔
186
            return "SPL_parameter"
1✔
187
        if self.audio_data is not None and self.audio_data.instrument is not None:
1✔
188
            return "SPL_instrument"
1✔
189
        return "FS"
1✔
190

191
    @property
1✔
192
    def v_lim(self) -> tuple[float, float]:
1✔
193
        """Limits (in dB) of the colormap used for plotting the spectrogram."""
194
        return self._v_lim
1✔
195

196
    @v_lim.setter
1✔
197
    def v_lim(self, v_lim: tuple[float, float] | None) -> None:
1✔
198
        v_lim = (
1✔
199
            v_lim
200
            if v_lim is not None
201
            else (-120.0, 0.0)
202
            if self.db_type == "FS"
203
            else (0.0, 170.0)
204
        )
205
        self._v_lim = v_lim
1✔
206

207
    def get_value(self) -> np.ndarray:
1✔
208
        """Return the Sx matrix of the spectrogram.
209

210
        The Sx matrix contains the absolute square of the STFT.
211
        """
212
        if not all(item.is_empty for item in self.items):
1✔
213
            return self._get_value_from_items(self.items)
1✔
214
        if not self.audio_data or not self.fft:
1✔
215
            raise ValueError("SpectroData should have either items or audio_data.")
×
216

217
        sx = self.fft.stft(
1✔
218
            self.audio_data.get_value_calibrated(),
219
            padding="zeros",
220
        )
221

222
        if self.sx_dtype is float:
1✔
223
            sx = abs(sx) ** 2
1✔
224

225
        return sx
1✔
226

227
    def get_welch(
1✔
228
        self,
229
        nperseg: int | None = None,
230
        detrend: str | callable | False = "constant",
231
        return_onesided: bool = True,
232
        scaling: Literal["density", "spectrum"] = "density",
233
        average: Literal["mean", "median"] = "mean",
234
    ) -> np.ndarray:
235
        """Estimate power spectral density of the SpectroData using Welch's method.
236

237
        This method uses the scipy.signal.welch function.
238
        The window, sample rate, overlap and mfft are taken from the
239
        SpectroData.fft property.
240

241
        Parameters
242
        ----------
243
        nperseg: int|None
244
            Length of each segment. Defaults to None, but if window is str or tuple, is set to 256, and if window is array_like, is set to the length of the window.
245
        detrend: str | callable | False
246
            Specifies how to detrend each segment. If detrend is a string, it is passed as the type argument to the detrend function. If it is a function, it takes a segment and returns a detrended segment. If detrend is False, no detrending is done. Defaults to ‘constant’.
247
        return_onesided: bool
248
            If True, return a one-sided spectrum for real data. If False return a two-sided spectrum. Defaults to True, but for complex data, a two-sided spectrum is always returned.
249
        scaling: Literal["density", "spectrum"]
250
            Selects between computing the power spectral density (‘density’) where Pxx has units of V**2/Hz and computing the squared magnitude spectrum (‘spectrum’) where Pxx has units of V**2, if x is measured in V and fs is measured in Hz. Defaults to ‘density’
251
        average: Literal["mean", "median"]
252
            Method to use when averaging periodograms. Defaults to ‘mean’.
253

254
        Returns
255
        -------
256
        np.ndarray
257
            Power spectral density or power spectrum of the SpectroData.
258

259
        """
260
        window = self.fft.win
1✔
261
        noverlap = self.fft.hop
1✔
262
        if noverlap == window.shape[0]:
1✔
263
            noverlap //= 2
1✔
264
        nfft = self.fft.mfft
1✔
265

266
        _, sx = welch(
1✔
267
            self.audio_data.get_value_calibrated(),
268
            fs=self.audio_data.sample_rate,
269
            window=window,
270
            nperseg=nperseg,
271
            noverlap=noverlap,
272
            nfft=nfft,
273
            detrend=detrend,
274
            return_onesided=return_onesided,
275
            scaling=scaling,
276
            average=average,
277
        )
278

279
        return sx
1✔
280

281
    def write_welch(
1✔
282
        self,
283
        folder: Path,
284
        px: np.ndarray | None = None,
285
        nperseg: int | None = None,
286
        detrend: str | callable | False = "constant",
287
        return_onesided: bool = True,
288
        scaling: Literal["density", "spectrum"] = "density",
289
        average: Literal["mean", "median"] = "mean",
290
    ) -> None:
291
        """Write the psd (welch) of the SpectroData to a npz file.
292

293
        Parameters
294
        ----------
295
        folder: pathlib.Path
296
            Folder in which to write the Spectro file.
297
        px: np.ndarray | None
298
            Welch px values. Will be computed if not provided.
299
        nperseg: int|None
300
            Length of each segment. Defaults to None, but if window is str or tuple, is set to 256, and if window is array_like, is set to the length of the window.
301
        detrend: str | callable | False
302
            Specifies how to detrend each segment. If detrend is a string, it is passed as the type argument to the detrend function. If it is a function, it takes a segment and returns a detrended segment. If detrend is False, no detrending is done. Defaults to ‘constant’.
303
        return_onesided: bool
304
            If True, return a one-sided spectrum for real data. If False return a two-sided spectrum. Defaults to True, but for complex data, a two-sided spectrum is always returned.
305
        scaling: Literal["density", "spectrum"]
306
            Selects between computing the power spectral density (‘density’) where Pxx has units of V**2/Hz and computing the squared magnitude spectrum (‘spectrum’) where Pxx has units of V**2, if x is measured in V and fs is measured in Hz. Defaults to ‘density’
307
        average: Literal["mean", "median"]
308
            Method to use when averaging periodograms. Defaults to ‘mean’.
309

310
        """
311
        super().create_directories(path=folder)
×
312
        px = (
×
313
            self.get_welch(
314
                nperseg=nperseg,
315
                detrend=detrend,
316
                return_onesided=return_onesided,
317
                scaling=scaling,
318
                average=average,
319
            )
320
            if px is None
321
            else px
322
        )
323
        freq = self.fft.f
×
324
        timestamps = (str(t) for t in (self.begin, self.end))
×
325
        np.savez(
×
326
            file=folder / f"{self}.npz",
327
            timestamps="_".join(timestamps),
328
            freq=freq,
329
            px=px,
330
        )
331

332
    def plot(
1✔
333
        self,
334
        ax: plt.Axes | None = None,
335
        sx: np.ndarray | None = None,
336
        scale: Scale | None = None,
337
    ) -> None:
338
        """Plot the spectrogram on a specific Axes.
339

340
        Parameters
341
        ----------
342
        ax: plt.axes | None
343
            Axes on which the spectrogram should be plotted.
344
            Defaulted as the SpectroData.get_default_ax Axes.
345
        sx: np.ndarray | None
346
            Spectrogram sx values. Will be computed if not provided.
347
        scale: osekit.core_api.frequecy_scale.Scale
348
            Custom frequency scale to use for plotting the spectrogram.
349

350
        """
351
        ax = ax if ax is not None else SpectroData.get_default_ax()
1✔
352
        sx = self.get_value() if sx is None else sx
1✔
353

354
        sx = self.to_db(sx)
1✔
355

356
        time = pd.date_range(start=self.begin, end=self.end, periods=sx.shape[1])
1✔
357
        freq = self.fft.f
1✔
358

359
        sx = sx if scale is None else scale.rescale(sx_matrix=sx, original_scale=freq)
1✔
360

361
        ax.xaxis_date()
1✔
362
        ax.imshow(
1✔
363
            sx,
364
            vmin=self._v_lim[0],
365
            vmax=self._v_lim[1],
366
            cmap=self.colormap,
367
            origin="lower",
368
            aspect="auto",
369
            interpolation="none",
370
            extent=(date2num(time[0]), date2num(time[-1]), freq[0], freq[-1]),
371
        )
372

373
    def to_db(self, sx: np.ndarray) -> np.ndarray:
1✔
374
        """Convert the sx values to dB.
375

376
        If the linked audio data has an Instrument parameter, the values are
377
        converted to dB SPL (re Instrument.P_REF).
378
        Otherwise, the values are converted to dB FS.
379

380
        Parameters
381
        ----------
382
        sx: np.ndarray
383
            Sx values of the spectrum.
384

385
        Returns
386
        -------
387
        np.ndarray
388
            Converted Sx values.
389

390
        """
391
        if self.sx_dtype is complex:
1✔
392
            sx = abs(sx) ** 2
1✔
393

394
        # sx has already been squared up, hence the 10*log for sx and 20*log for the ref
395
        return 10 * np.log10(sx + np.nextafter(0, 1)) - 20 * np.log10(self.db_ref)
1✔
396

397
    def save_spectrogram(
1✔
398
        self,
399
        folder: Path,
400
        ax: plt.Axes | None = None,
401
        sx: np.ndarray | None = None,
402
        scale: Scale | None = None,
403
    ) -> None:
404
        """Export the spectrogram as a png image.
405

406
        Parameters
407
        ----------
408
        folder: Path
409
            Folder in which the spectrogram should be saved.
410
        ax: plt.Axes | None
411
            Axes on which the spectrogram should be plotted.
412
            Defaulted as the SpectroData.get_default_ax Axes.
413
        sx: np.ndarray | None
414
            Spectrogram sx values. Will be computed if not provided.
415
        scale: osekit.core_api.frequecy_scale.Scale
416
            Custom frequency scale to use for plotting the spectrogram.
417

418
        """
419
        super().create_directories(path=folder)
1✔
420
        self.plot(ax=ax, sx=sx, scale=scale)
1✔
421
        plt.savefig(f"{folder / str(self)}", bbox_inches="tight", pad_inches=0)
1✔
422
        plt.close()
1✔
423
        gc.collect()
1✔
424

425
    def write(
1✔
426
        self,
427
        folder: Path,
428
        sx: np.ndarray | None = None,
429
        link: bool = False,
430
    ) -> None:
431
        """Write the Spectro data to file.
432

433
        Parameters
434
        ----------
435
        folder: pathlib.Path
436
            Folder in which to write the Spectro file.
437
        sx: np.ndarray | None
438
            Spectrogram sx values. Will be computed if not provided.
439
        link: bool
440
            If True, the SpectroData will be bound to the written npz file.
441
            Its items will be replaced with a single item, which will match the whole
442
            new SpectroFile.
443

444
        """
445
        super().create_directories(path=folder)
1✔
446
        sx = self.get_value() if sx is None else sx
1✔
447
        time = np.arange(sx.shape[1]) * self.duration.total_seconds() / sx.shape[1]
1✔
448
        freq = self.fft.f
1✔
449
        window = self.fft.win
1✔
450
        hop = [self.fft.hop]
1✔
451
        fs = [self.fft.fs]
1✔
452
        mfft = [self.fft.mfft]
1✔
453
        db_ref = [self.db_ref]
1✔
454
        v_lim = self.v_lim
1✔
455
        timestamps = (str(t) for t in (self.begin, self.end))
1✔
456
        np.savez(
1✔
457
            file=folder / f"{self}.npz",
458
            fs=fs,
459
            time=time,
460
            freq=freq,
461
            window=window,
462
            hop=hop,
463
            sx=sx,
464
            mfft=mfft,
465
            db_ref=db_ref,
466
            v_lim=v_lim,
467
            timestamps="_".join(timestamps),
468
        )
469
        if link:
1✔
470
            self.link(folder=folder)
1✔
471

472
    def link(self, folder: Path) -> None:
1✔
473
        """Link the SpectroData to a SpectroFile in the folder.
474

475
        The given folder should contain a file named "str(self).npz".
476
        Linking is intended for SpectroData objects that have already been
477
        written to disk.
478
        After linking, the SpectroData will have a single item with the same
479
        properties of the target SpectroFile.
480

481
        Parameters
482
        ----------
483
        folder: Path
484
            Folder in which is located the SpectroFile to which the SpectroData
485
            instance should be linked.
486

487
        """
488
        file = SpectroFile(
1✔
489
            path=folder / f"{self}.npz",
490
            strptime_format=TIMESTAMP_FORMATS_EXPORTED_FILES,
491
        )
492
        self.items = SpectroData.from_files([file]).items
1✔
493

494
    def link_audio_data(self, audio_data: AudioData) -> None:
1✔
495
        """Link the SpectroData to a given AudioData.
496

497
        Parameters
498
        ----------
499
        audio_data: AudioData
500
            The AudioData to which this SpectroData will be linked.
501

502
        """
503
        if self.begin != audio_data.begin:
1✔
504
            raise ValueError("The begin of the audio data doesn't match.")
1✔
505
        if self.end != audio_data.end:
1✔
506
            raise ValueError("The end of the audio data doesn't match.")
1✔
507
        if self.fft.fs != audio_data.sample_rate:
1✔
508
            raise ValueError("The sample rate of the audio data doesn't match.")
1✔
509
        self.audio_data = audio_data
1✔
510

511
    def split(self, nb_subdata: int = 2) -> list[SpectroData]:
1✔
512
        """Split the spectro data object in the specified number of spectro subdata.
513

514
        Parameters
515
        ----------
516
        nb_subdata: int
517
            Number of subdata in which to split the data.
518

519
        Returns
520
        -------
521
        list[SpectroData]
522
            The list of SpectroData subdata objects.
523

524
        """
525
        split_frames = list(
1✔
526
            np.linspace(0, self.audio_data.shape, nb_subdata + 1, dtype=int),
527
        )
528
        split_frames = [
1✔
529
            self.fft.nearest_k_p(frame) if idx < (len(split_frames) - 1) else frame
530
            for idx, frame in enumerate(split_frames)
531
        ]
532

533
        ad_split = [
1✔
534
            self.audio_data.split_frames(start_frame=a, stop_frame=b)
535
            for a, b in zip(split_frames, split_frames[1:], strict=False)
536
        ]
537
        return [
1✔
538
            SpectroData.from_audio_data(
539
                data=ad,
540
                fft=self.fft,
541
                v_lim=self.v_lim,
542
                colormap=self.colormap,
543
            )
544
            for ad in ad_split
545
        ]
546

547
    def _get_value_from_items(self, items: list[SpectroItem]) -> np.ndarray:
1✔
548
        if not all(
1✔
549
            np.array_equal(items[0].file.freq, i.file.freq)
550
            for i in items[1:]
551
            if not i.is_empty
552
        ):
553
            raise ValueError("Items don't have the same frequency bins.")
×
554

555
        if len({i.file.get_fft().delta_t for i in items if not i.is_empty}) > 1:
1✔
556
            raise ValueError("Items don't have the same time resolution.")
×
557

558
        output = items[0].get_value(fft=self.fft, sx_dtype=self.sx_dtype)
1✔
559
        for item in items[1:]:
1✔
560
            p1_le = self.fft.lower_border_end[1] - self.fft.p_min
1✔
561
            output = np.hstack(
1✔
562
                (
563
                    output[:, :-p1_le],
564
                    (
565
                        output[:, -p1_le:]
566
                        + item.get_value(fft=self.fft, sx_dtype=self.sx_dtype)[
567
                            :,
568
                            :p1_le,
569
                        ]
570
                    ),
571
                    item.get_value(fft=self.fft, sx_dtype=self.sx_dtype)[:, p1_le:],
572
                ),
573
            )
574
        return output
1✔
575

576
    @classmethod
1✔
577
    def from_files(
1✔
578
        cls,
579
        files: list[SpectroFile],
580
        begin: Timestamp | None = None,
581
        end: Timestamp | None = None,
582
    ) -> SpectroData:
583
        """Return a SpectroData object from a list of SpectroFiles.
584

585
        Parameters
586
        ----------
587
        files: list[SpectroFile]
588
            List of SpectroFiles containing the data.
589
        begin: Timestamp | None
590
            Begin of the data object.
591
            Defaulted to the begin of the first file.
592
        end: Timestamp | None
593
            End of the data object.
594
            Defaulted to the end of the last file.
595

596
        Returns
597
        -------
598
        SpectroData:
599
            The SpectroData object.
600

601
        """
602
        instance = cls.from_base_data(
1✔
603
            BaseData.from_files(files, begin, end),
604
            fft=files[0].get_fft(),
605
        )
606
        if not any(file.sx_dtype is complex for file in files):
1✔
607
            instance.sx_dtype = float
1✔
608
        return instance
1✔
609

610
    @classmethod
1✔
611
    def from_base_data(
1✔
612
        cls,
613
        data: BaseData,
614
        fft: ShortTimeFFT,
615
        colormap: str | None = None,
616
    ) -> SpectroData:
617
        """Return an SpectroData object from a BaseData object.
618

619
        Parameters
620
        ----------
621
        data: BaseData
622
            BaseData object to convert to SpectroData.
623
        fft: ShortTimeFFT
624
            The ShortTimeFFT used to compute the spectrogram.
625
        colormap: str
626
            The colormap used to plot the spectrogram.
627

628
        Returns
629
        -------
630
        SpectroData:
631
            The SpectroData object.
632

633
        """
634
        items = [SpectroItem.from_base_item(item) for item in data.items]
1✔
635
        db_ref = next((f.file.db_ref for f in items if f.file.db_ref is not None), None)
1✔
636
        v_lim = next((f.file.v_lim for f in items if f.file.v_lim is not None), None)
1✔
637
        return cls(
1✔
638
            [SpectroItem.from_base_item(item) for item in data.items],
639
            fft=fft,
640
            db_ref=db_ref,
641
            v_lim=v_lim,
642
            colormap=colormap,
643
        )
644

645
    @classmethod
1✔
646
    def from_audio_data(
1✔
647
        cls,
648
        data: AudioData,
649
        fft: ShortTimeFFT,
650
        v_lim: tuple[float, float] | None = None,
651
        colormap: str | None = None,
652
    ) -> SpectroData:
653
        """Instantiate a SpectroData object from a AudioData object.
654

655
        Parameters
656
        ----------
657
        data: AudioData
658
            Audio data from which the SpectroData should be computed.
659
        fft: ShortTimeFFT
660
            The ShortTimeFFT used to compute the spectrogram.
661
        v_lim: tuple[float,float]
662
            Lower and upper limits (in dB) of the colormap used
663
            for plotting the spectrogram.
664
        colormap: str
665
            Colormap to use for plotting the spectrogram.
666

667
        Returns
668
        -------
669
        SpectroData:
670
            The SpectroData object.
671

672
        """
673
        return cls(
1✔
674
            audio_data=data,
675
            fft=fft,
676
            begin=data.begin,
677
            end=data.end,
678
            v_lim=v_lim,
679
            colormap=colormap,
680
        )
681

682
    def to_dict(self, embed_sft: bool = True) -> dict:
1✔
683
        """Serialize a SpectroData to a dictionary.
684

685
        Parameters
686
        ----------
687
        embed_sft: bool
688
            If True, the SFT parameters will be included in the dictionary.
689
            In a case where multiple SpectroData that share a same SFT are serialized,
690
            SFT parameters shouldn't be included in the dictionary, as the window
691
            values might lead to large redundant data.
692
            Rather, the SFT parameters should be serialized in
693
            a SpectroDataset dictionary so that it can be only stored once
694
            for all SpectroData instances.
695

696
        Returns
697
        -------
698
        dict:
699
            The serialized dictionary representing the SpectroData.
700

701

702
        """
703
        base_dict = super().to_dict()
1✔
704
        audio_dict = {
1✔
705
            "audio_data": (
706
                None if self.audio_data is None else self.audio_data.to_dict()
707
            ),
708
        }
709
        sft_dict = {
1✔
710
            "sft": (
711
                {
712
                    "win": list(self.fft.win),
713
                    "hop": self.fft.hop,
714
                    "fs": self.fft.fs,
715
                    "mfft": self.fft.mfft,
716
                    "scale_to": self.fft.scaling,
717
                }
718
                if embed_sft
719
                else None
720
            ),
721
        }
722
        return (
1✔
723
            base_dict
724
            | audio_dict
725
            | sft_dict
726
            | {"v_lim": self.v_lim, "colormap": self.colormap}
727
        )
728

729
    @classmethod
1✔
730
    def from_dict(
1✔
731
        cls,
732
        dictionary: dict,
733
        sft: ShortTimeFFT | None = None,
734
    ) -> SpectroData:
735
        """Deserialize a SpectroData from a dictionary.
736

737
        Parameters
738
        ----------
739
        dictionary: dict
740
            The serialized dictionary representing the AudioData.
741
        sft: ShortTimeFFT | None
742
            The ShortTimeFFT used to compute the spectrogram.
743
            If not provided, the SFT parameters must be included in the dictionary.
744

745
        Returns
746
        -------
747
        SpectroData
748
            The deserialized SpectroData.
749

750
        """
751
        if sft is None and dictionary["sft"] is None:
1✔
UNCOV
752
            raise ValueError("Missing sft")
×
753
        if sft is None:
1✔
754
            dictionary["sft"]["win"] = np.array(dictionary["sft"]["win"])
1✔
755
            sft = ShortTimeFFT(**dictionary["sft"])
1✔
756

757
        if dictionary["audio_data"] is None:
1✔
758
            base_data = BaseData.from_dict(dictionary)
1✔
759
            return cls.from_base_data(
1✔
760
                data=base_data,
761
                fft=sft,
762
                colormap=dictionary["colormap"],
763
            )
764

765
        audio_data = AudioData.from_dict(dictionary["audio_data"])
1✔
766
        v_lim = (
1✔
767
            None if type(dictionary["v_lim"]) is object else tuple(dictionary["v_lim"])
768
        )
769
        spectro_data = cls.from_audio_data(
1✔
770
            audio_data,
771
            sft,
772
            v_lim=v_lim,
773
            colormap=dictionary["colormap"],
774
        )
775

776
        if dictionary["files"]:
1✔
777
            spectro_files = [
1✔
778
                SpectroFile.from_dict(sf) for sf in dictionary["files"].values()
779
            ]
780
            spectro_data.items = SpectroData.from_files(spectro_files).items
1✔
781

782
        return spectro_data
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc