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

Project-OSmOSE / OSEkit / 22178248750

19 Feb 2026 10:35AM UTC coverage: 98.755% (-0.1%) from 98.862%
22178248750

Pull #341

github

web-flow
Merge 12fe6be46 into acb64173f
Pull Request #341: [DRAFT] Soxr resample stream

103 of 105 new or added lines in 7 files covered. (98.1%)

7 existing lines in 3 files now uncovered.

4838 of 4899 relevant lines covered (98.75%)

0.99 hits per line

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

97.9
/tests/test_serialization.py
1
from __future__ import annotations
1✔
2

3
from pathlib import Path, PureWindowsPath
1✔
4

5
import numpy as np
1✔
6
import pytest
1✔
7
from pandas import Timedelta, Timestamp
1✔
8
from scipy.signal import ShortTimeFFT
1✔
9
from scipy.signal.windows import hamming, hann
1✔
10

11
from osekit.config import (
1✔
12
    TIMESTAMP_FORMAT_EXPORTED_FILES_LOCALIZED,
13
    TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED,
14
    TIMESTAMP_FORMATS_EXPORTED_FILES,
15
)
16
from osekit.core_api.audio_data import AudioData
1✔
17
from osekit.core_api.audio_dataset import AudioDataset
1✔
18
from osekit.core_api.audio_file import AudioFile
1✔
19
from osekit.core_api.frequency_scale import Scale, ScalePart
1✔
20
from osekit.core_api.instrument import Instrument
1✔
21
from osekit.core_api.json_serializer import (
1✔
22
    absolute_to_relative,
23
    relative_to_absolute,
24
    set_path_reference,
25
)
26
from osekit.core_api.spectro_data import SpectroData
1✔
27
from osekit.core_api.spectro_dataset import SpectroDataset
1✔
28
from osekit.core_api.spectro_file import SpectroFile
1✔
29
from osekit.utils.audio_utils import Normalization
1✔
30

31

32
@pytest.mark.parametrize(
1✔
33
    ("audio_files", "begin", "end", "sample_rate", "normalization"),
34
    [
35
        pytest.param(
36
            {
37
                "duration": 1,
38
                "sample_rate": 48_000,
39
                "nb_files": 1,
40
                "date_begin": Timestamp("2024-01-01 12:00:00"),
41
            },
42
            None,
43
            None,
44
            48_000,
45
            Normalization.RAW,
46
            id="full_file_no_resample",
47
        ),
48
        pytest.param(
49
            {
50
                "duration": 1,
51
                "sample_rate": 48_000,
52
                "nb_files": 1,
53
                "date_begin": Timestamp("2024-01-01 12:00:00"),
54
            },
55
            None,
56
            None,
57
            48_000,
58
            Normalization.ZSCORE,
59
            id="normalized_audio",
60
        ),
61
        pytest.param(
62
            {
63
                "duration": 1,
64
                "sample_rate": 48_000,
65
                "nb_files": 1,
66
                "date_begin": Timestamp("2024-01-01 12:00:00"),
67
            },
68
            None,
69
            None,
70
            24_000,
71
            Normalization.RAW,
72
            id="full_file_downsample",
73
        ),
74
        pytest.param(
75
            {
76
                "duration": 1,
77
                "sample_rate": 48_000,
78
                "nb_files": 1,
79
                "date_begin": Timestamp("2024-01-01 12:00:00"),
80
            },
81
            None,
82
            None,
83
            96_000,
84
            Normalization.RAW,
85
            id="full_file_upsample",
86
        ),
87
        pytest.param(
88
            {
89
                "duration": 3,
90
                "sample_rate": 48_000,
91
                "nb_files": 1,
92
                "date_begin": Timestamp("2024-01-01 12:00:00"),
93
            },
94
            Timestamp("2024-01-01 12:00:01"),
95
            Timestamp("2024-01-01 12:00:02"),
96
            48_000,
97
            Normalization.RAW,
98
            id="file_part",
99
        ),
100
        pytest.param(
101
            {
102
                "duration": 1.5,
103
                "sample_rate": 48_000,
104
                "nb_files": 2,
105
                "date_begin": Timestamp("2024-01-01 12:00:00"),
106
            },
107
            Timestamp("2024-01-01 12:00:01"),
108
            Timestamp("2024-01-01 12:00:02"),
109
            24_000,
110
            Normalization.RAW,
111
            id="two_files_with_resample",
112
        ),
113
        pytest.param(
114
            {
115
                "duration": 2,
116
                "sample_rate": 48_000,
117
                "nb_files": 2,
118
                "inter_file_duration": 1,
119
                "date_begin": Timestamp("2024-01-01 12:00:00"),
120
            },
121
            Timestamp("2024-01-01 12:00:01"),
122
            Timestamp("2024-01-01 12:00:04"),
123
            48_000,
124
            Normalization.RAW,
125
            id="two_files_with_gap",
126
        ),
127
        pytest.param(
128
            {
129
                "duration": 2,
130
                "sample_rate": 48_000,
131
                "nb_files": 2,
132
                "inter_file_duration": 1,
133
                "date_begin": Timestamp("2024-01-01 12:00:00+0200"),
134
            },
135
            Timestamp("2024-01-01 12:00:01+0200"),
136
            Timestamp("2024-01-01 12:00:04+0200"),
137
            48_000,
138
            Normalization.RAW,
139
            id="localized_files",
140
        ),
141
        pytest.param(
142
            {
143
                "duration": 2,
144
                "sample_rate": 48_000,
145
                "nb_files": 2,
146
                "inter_file_duration": 1,
147
                "date_begin": Timestamp("2024-01-01 12:00:00+0200"),
148
            },
149
            Timestamp("2024-01-01 12:00:01+0200"),
150
            Timestamp("2024-01-01 12:00:04+0200"),
151
            48_000,
152
            Normalization.DC_REJECT,
153
            id="localized_normalized_files",
154
        ),
155
    ],
156
    indirect=["audio_files"],
157
)
158
def test_audio_data_serialization(
1✔
159
    tmp_path: Path,
160
    audio_files: tuple[list[AudioFile], pytest.fixtures.Subrequest],
161
    begin: Timestamp | None,
162
    end: Timestamp | None,
163
    sample_rate: float,
164
    normalization: Normalization,
165
) -> None:
166
    audio_files, _ = audio_files
1✔
167

168
    ad = AudioData.from_files(
1✔
169
        audio_files,
170
        begin=begin,
171
        end=end,
172
        sample_rate=sample_rate,
173
        normalization=normalization,
174
    )
175

176
    assert np.array_equal(ad.get_value(), AudioData.from_dict(ad.to_dict()).get_value())
1✔
177

178

179
@pytest.mark.parametrize(
1✔
180
    ("audio_files", "data_duration", "sample_rate", "normalization", "name"),
181
    [
182
        pytest.param(
183
            {
184
                "duration": 1,
185
                "sample_rate": 48_000,
186
                "nb_files": 1,
187
            },
188
            Timedelta(seconds=1),
189
            48_000,
190
            Normalization.RAW,
191
            None,
192
            id="one_audio_data_one_file_no_resample",
193
        ),
194
        pytest.param(
195
            {
196
                "duration": 1,
197
                "sample_rate": 48_000,
198
                "nb_files": 2,
199
            },
200
            Timedelta(seconds=2),
201
            48_000,
202
            Normalization.RAW,
203
            None,
204
            id="one_audio_data_two_files_no_resample",
205
        ),
206
        pytest.param(
207
            {
208
                "duration": 1,
209
                "sample_rate": 48_000,
210
                "nb_files": 2,
211
            },
212
            Timedelta(seconds=2),
213
            24_000,
214
            Normalization.RAW,
215
            None,
216
            id="one_audio_data_two_files_downsample",
217
        ),
218
        pytest.param(
219
            {
220
                "duration": 1,
221
                "sample_rate": 48_000,
222
                "nb_files": 4,
223
            },
224
            Timedelta(seconds=1),
225
            [12_000, 24_000, 48_000, 96_000],
226
            Normalization.RAW,
227
            None,
228
            id="multiple_audio_data_different_sample_rates",
229
        ),
230
        pytest.param(
231
            {
232
                "duration": 1,
233
                "sample_rate": 48_000,
234
                "nb_files": 1,
235
            },
236
            Timedelta(seconds=1),
237
            48_000,
238
            Normalization.RAW,
239
            "merriweather post pavilion",
240
            id="named_ads",
241
        ),
242
        pytest.param(
243
            {
244
                "duration": 1,
245
                "sample_rate": 48_000,
246
                "nb_files": 1,
247
                "date_begin": Timestamp("2024-01-01 12:00:00+0200"),
248
            },
249
            Timedelta(seconds=1),
250
            48_000,
251
            Normalization.RAW,
252
            "merriweather post pavilion",
253
            id="localized_ads",
254
        ),
255
    ],
256
    indirect=["audio_files"],
257
)
258
def test_audio_dataset_serialization(
1✔
259
    tmp_path: Path,
260
    audio_files: tuple[list[AudioFile], pytest.fixtures.Subrequest],
261
    data_duration: Timestamp | None,
262
    sample_rate: float | list[float],
263
    normalization: Normalization,
264
    name: str | None,
265
) -> None:
266
    audio_files, _ = audio_files
1✔
267
    begin = min(af.begin for af in audio_files)
1✔
268

269
    strptime_format = (
1✔
270
        TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED
271
        if begin.tzinfo is None
272
        else TIMESTAMP_FORMAT_EXPORTED_FILES_LOCALIZED
273
    )
274

275
    ads = AudioDataset.from_folder(
1✔
276
        tmp_path,
277
        strptime_format=strptime_format,
278
        data_duration=data_duration,
279
        normalization=normalization,
280
        name=name,
281
    )
282

283
    assert ads.begin == begin
1✔
284

285
    if type(sample_rate) is list:
1✔
286
        for data, sr in zip(ads.data, sample_rate, strict=False):
1✔
287
            data.sample_rate = sr
1✔
288
    else:
289
        ads.sample_rate = sample_rate
1✔
290

291
    assert all(
1✔
292
        np.array_equal(ad.get_value(), ad2.get_value())
293
        for ad, ad2 in zip(
294
            ads.data,
295
            AudioDataset.from_dict(ads.to_dict()).data,
296
            strict=False,
297
        )
298
    )
299

300
    ads.write_json(tmp_path)
1✔
301

302
    ads2 = AudioDataset.from_json(tmp_path / f"{ads}.json")
1✔
303

304
    assert str(ads) == str(ads2)
1✔
305
    assert ads.name == ads2.name
1✔
306
    assert ads.has_default_name == ads2.has_default_name
1✔
307
    assert ads.sample_rate == ads2.sample_rate
1✔
308
    assert ads.begin == ads2.begin
1✔
309
    assert ads.normalization == ads2.normalization
1✔
310

311
    assert all(
1✔
312
        np.array_equal(ad.get_value(), ad2.get_value())
313
        for ad, ad2 in zip(ads.data, ads2.data, strict=False)
314
    )
315

316

317
@pytest.mark.parametrize(
1✔
318
    (
319
        "audio_files",
320
        "begin",
321
        "end",
322
        "sft",
323
        "sample_rate",
324
        "instrument",
325
        "v_lim",
326
        "colormap",
327
    ),
328
    [
329
        pytest.param(
330
            {
331
                "duration": 1,
332
                "sample_rate": 48_000,
333
                "nb_files": 1,
334
                "date_begin": Timestamp("2024-01-01 12:00:00"),
335
            },
336
            None,
337
            None,
338
            ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024),
339
            48_000,
340
            None,
341
            None,
342
            None,
343
            id="full_file_no_resample",
344
        ),
345
        pytest.param(
346
            {
347
                "duration": 1,
348
                "sample_rate": 48_000,
349
                "nb_files": 1,
350
                "date_begin": Timestamp("2024-01-01 12:00:00"),
351
            },
352
            None,
353
            None,
354
            ShortTimeFFT(win=hamming(1024, sym=False), hop=1024, fs=24_000, mfft=1024),
355
            24_000,
356
            None,
357
            None,
358
            None,
359
            id="non_symetric_hamming_window",
360
        ),
361
        pytest.param(
362
            {
363
                "duration": 1,
364
                "sample_rate": 48_000,
365
                "nb_files": 1,
366
                "date_begin": Timestamp("2024-01-01 12:00:00"),
367
            },
368
            None,
369
            None,
370
            ShortTimeFFT(win=hann(1024), hop=1024, fs=24_000, mfft=1024),
371
            24_000,
372
            None,
373
            None,
374
            None,
375
            id="hann_window",
376
        ),
377
        pytest.param(
378
            {
379
                "duration": 1,
380
                "sample_rate": 48_000,
381
                "nb_files": 1,
382
                "date_begin": Timestamp("2024-01-01 12:00:00"),
383
            },
384
            None,
385
            None,
386
            ShortTimeFFT(win=hamming(1024), hop=512, fs=24_000, mfft=1024),
387
            24_000,
388
            None,
389
            None,
390
            None,
391
            id="full_file_downsample_and_overlap",
392
        ),
393
        pytest.param(
394
            {
395
                "duration": 1,
396
                "sample_rate": 48_000,
397
                "nb_files": 1,
398
                "date_begin": Timestamp("2024-01-01 12:00:00"),
399
            },
400
            None,
401
            None,
402
            ShortTimeFFT(win=hamming(1024), hop=1024, fs=96_000, mfft=2048),
403
            96_000,
404
            None,
405
            None,
406
            None,
407
            id="full_file_upsample_and_mfft_padding",
408
        ),
409
        pytest.param(
410
            {
411
                "duration": 3,
412
                "sample_rate": 48_000,
413
                "nb_files": 1,
414
                "date_begin": Timestamp("2024-01-01 12:00:00"),
415
            },
416
            Timestamp("2024-01-01 12:00:01"),
417
            Timestamp("2024-01-01 12:00:02"),
418
            ShortTimeFFT(win=hamming(1024), hop=512, fs=48_000, mfft=2048),
419
            48_000,
420
            None,
421
            None,
422
            None,
423
            id="file_part_and_overlap_and_padding",
424
        ),
425
        pytest.param(
426
            {
427
                "duration": 1.5,
428
                "sample_rate": 48_000,
429
                "nb_files": 2,
430
                "date_begin": Timestamp("2024-01-01 12:00:00"),
431
            },
432
            Timestamp("2024-01-01 12:00:01"),
433
            Timestamp("2024-01-01 12:00:02"),
434
            ShortTimeFFT(win=hamming(1024), hop=1024, fs=24_000, mfft=1024),
435
            24_000,
436
            None,
437
            None,
438
            None,
439
            id="two_files_with_resample",
440
        ),
441
        pytest.param(
442
            {
443
                "duration": 2,
444
                "sample_rate": 48_000,
445
                "nb_files": 2,
446
                "inter_file_duration": 1,
447
                "date_begin": Timestamp("2024-01-01 12:00:00"),
448
            },
449
            Timestamp("2024-01-01 12:00:01"),
450
            Timestamp("2024-01-01 12:00:04"),
451
            ShortTimeFFT(win=hamming(1024), hop=1024, fs=24_000, mfft=1024),
452
            48_000,
453
            None,
454
            None,
455
            None,
456
            id="two_files_with_gap",
457
        ),
458
        pytest.param(
459
            {
460
                "duration": 1,
461
                "sample_rate": 48_000,
462
                "nb_files": 1,
463
                "date_begin": Timestamp("2024-01-01 12:00:00"),
464
            },
465
            None,
466
            None,
467
            ShortTimeFFT(
468
                win=hamming(1024),
469
                hop=1024,
470
                fs=48_000,
471
                mfft=1024,
472
                scale_to="magnitude",
473
            ),
474
            48_000,
475
            None,
476
            None,
477
            None,
478
            id="magnitude_spectrum",
479
        ),
480
        pytest.param(
481
            {
482
                "duration": 1,
483
                "sample_rate": 48_000,
484
                "nb_files": 1,
485
                "date_begin": Timestamp("2024-01-01 12:00:00"),
486
            },
487
            None,
488
            None,
489
            ShortTimeFFT(
490
                win=hamming(1024),
491
                hop=1024,
492
                fs=48_000,
493
                mfft=1024,
494
                scale_to="magnitude",
495
            ),
496
            48_000,
497
            Instrument(end_to_end_db=150.0),
498
            None,
499
            None,
500
            id="specified_instrument",
501
        ),
502
        pytest.param(
503
            {
504
                "duration": 1,
505
                "sample_rate": 48_000,
506
                "nb_files": 1,
507
                "date_begin": Timestamp("2024-01-01 12:00:00"),
508
            },
509
            None,
510
            None,
511
            ShortTimeFFT(
512
                win=hamming(1024),
513
                hop=1024,
514
                fs=48_000,
515
                mfft=1024,
516
                scale_to="magnitude",
517
            ),
518
            48_000,
519
            None,
520
            (-50.0, 0.0),
521
            None,
522
            id="specified_v_lim",
523
        ),
524
        pytest.param(
525
            {
526
                "duration": 1,
527
                "sample_rate": 48_000,
528
                "nb_files": 1,
529
                "date_begin": Timestamp("2024-01-01 12:00:00"),
530
            },
531
            None,
532
            None,
533
            ShortTimeFFT(
534
                win=hamming(1024),
535
                hop=1024,
536
                fs=48_000,
537
                mfft=1024,
538
                scale_to="magnitude",
539
            ),
540
            48_000,
541
            Instrument(end_to_end_db=150.0),
542
            (0.0, 150.0),
543
            None,
544
            id="specified_v_lim_and_instrument",
545
        ),
546
        pytest.param(
547
            {
548
                "duration": 1,
549
                "sample_rate": 48_000,
550
                "nb_files": 1,
551
                "date_begin": Timestamp("2024-01-01 12:00:00"),
552
            },
553
            None,
554
            None,
555
            ShortTimeFFT(
556
                win=hamming(1024),
557
                hop=1024,
558
                fs=48_000,
559
                mfft=1024,
560
                scale_to="psd",
561
            ),
562
            48_000,
563
            None,
564
            None,
565
            None,
566
            id="psd_spectrum",
567
        ),
568
        pytest.param(
569
            {
570
                "duration": 1,
571
                "sample_rate": 48_000,
572
                "nb_files": 1,
573
                "date_begin": Timestamp("2024-01-01 12:00:00"),
574
            },
575
            None,
576
            None,
577
            ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024),
578
            48_000,
579
            None,
580
            None,
581
            "inferno",
582
            id="different_colormap",
583
        ),
584
        pytest.param(
585
            {
586
                "duration": 1,
587
                "sample_rate": 48_000,
588
                "nb_files": 1,
589
                "date_begin": Timestamp("2024-01-01 12:00:00+0200"),
590
            },
591
            None,
592
            None,
593
            ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024),
594
            48_000,
595
            None,
596
            None,
597
            None,
598
            id="timezone_aware",
599
        ),
600
    ],
601
    indirect=["audio_files"],
602
)
603
def test_spectro_data_serialization(
1✔
604
    tmp_path: Path,
605
    audio_files: tuple[list[AudioFile], pytest.fixtures.Subrequest],
606
    begin: Timestamp | None,
607
    end: Timestamp | None,
608
    sft: ShortTimeFFT,
609
    sample_rate: float,
610
    instrument: Instrument | None,
611
    v_lim: tuple[float, float] | None,
612
    colormap: str | None,
613
) -> None:
614
    af, _ = audio_files
1✔
615

616
    ad = AudioData.from_files(
1✔
617
        af,
618
        begin=begin,
619
        end=end,
620
        sample_rate=sample_rate,
621
        instrument=instrument,
622
    )
623

624
    # SpectroData linked to AudioData
625

626
    sd = SpectroData.from_audio_data(ad, sft, colormap=colormap)
1✔
627
    sd2 = SpectroData.from_dict(sd.to_dict(embed_sft=True))
1✔
628

629
    assert np.allclose(
1✔
630
        sd.get_value(),
631
        sd2.get_value(),
632
    )
633

634
    assert np.allclose(
1✔
635
        sd.to_db(sd.get_value()),
636
        sd2.to_db(sd2.get_value()),
637
    )
638

639
    assert sd.db_ref == sd2.db_ref
1✔
640
    assert sd.v_lim == sd2.v_lim
1✔
641

642
    assert sd.colormap == sd2.colormap
1✔
643

644
    # SpectroData linked to SpectroFiles
645

646
    sd.write(tmp_path, link=True)
1✔
647

648
    sfs = [
1✔
649
        SpectroFile(file, strptime_format=TIMESTAMP_FORMATS_EXPORTED_FILES)
650
        for file in tmp_path.glob("*.npz")
651
    ]
652

653
    sd2 = SpectroData.from_files(sfs)
1✔
654
    sd2 = SpectroData.from_dict(sd2.to_dict(embed_sft=True))
1✔
655

656
    assert np.array_equal(
1✔
657
        sd.get_value(),
658
        sd2.get_value(),
659
    )
660

661
    assert np.array_equal(
1✔
662
        sd.to_db(sd.get_value()),
663
        sd2.to_db(sd2.get_value()),
664
    )
665

666
    assert sd.db_ref == sd2.db_ref
1✔
667
    assert sd.v_lim == sd2.v_lim
1✔
668

669
    # Linked file from dict
670

671
    assert SpectroData.from_dict(sd.to_dict(embed_sft=True)).files == sd.files
1✔
672

673

674
@pytest.mark.parametrize(
1✔
675
    (
676
        "audio_files",
677
        "data_duration",
678
        "sample_rate",
679
        "sfts",
680
        "instrument",
681
        "v_lim",
682
        "colormap",
683
        "frequency_scale",
684
        "name",
685
    ),
686
    [
687
        pytest.param(
688
            {
689
                "duration": 1,
690
                "sample_rate": 48_000,
691
                "nb_files": 1,
692
            },
693
            Timedelta(seconds=1),
694
            48_000,
695
            [ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024)],
696
            None,
697
            None,
698
            None,
699
            None,
700
            None,
701
            id="one_spectro_data",
702
        ),
703
        pytest.param(
704
            {
705
                "duration": 1,
706
                "sample_rate": 48_000,
707
                "nb_files": 1,
708
            },
709
            Timedelta(seconds=0.1),
710
            48_000,
711
            [ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024)],
712
            None,
713
            None,
714
            None,
715
            None,
716
            None,
717
            id="ten_spectro_data_one_sft",
718
        ),
719
        pytest.param(
720
            {
721
                "duration": 1,
722
                "sample_rate": 48_000,
723
                "nb_files": 1,
724
            },
725
            Timedelta(seconds=0.1),
726
            48_000,
727
            [ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024)],
728
            Instrument(end_to_end_db=150.0),
729
            None,
730
            None,
731
            None,
732
            None,
733
            id="specified_instrument",
734
        ),
735
        pytest.param(
736
            {
737
                "duration": 1,
738
                "sample_rate": 48_000,
739
                "nb_files": 1,
740
            },
741
            Timedelta(seconds=0.1),
742
            48_000,
743
            [ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024)],
744
            None,
745
            (-100.0, 0.0),
746
            None,
747
            None,
748
            None,
749
            id="specified_v_lim",
750
        ),
751
        pytest.param(
752
            {
753
                "duration": 1,
754
                "sample_rate": 48_000,
755
                "nb_files": 1,
756
            },
757
            Timedelta(seconds=0.1),
758
            48_000,
759
            [ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024)],
760
            Instrument(end_to_end_db=150.0),
761
            (0.0, 150.0),
762
            None,
763
            None,
764
            None,
765
            id="specified_instrument_and_v_lim",
766
        ),
767
        pytest.param(
768
            {
769
                "duration": 1,
770
                "sample_rate": 48_000,
771
                "nb_files": 1,
772
            },
773
            Timedelta(seconds=0.1),
774
            48_000,
775
            [
776
                ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024),
777
                ShortTimeFFT(win=hann(1024), hop=512, fs=48_000, mfft=2048),
778
            ],
779
            None,
780
            None,
781
            None,
782
            None,
783
            None,
784
            id="ten_spectro_data_two_sfts",
785
        ),
786
        pytest.param(
787
            {
788
                "duration": 1,
789
                "sample_rate": 48_000,
790
                "nb_files": 1,
791
            },
792
            Timedelta(seconds=1),
793
            48_000,
794
            [ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024)],
795
            None,
796
            None,
797
            "inferno",
798
            None,
799
            None,
800
            id="specified_colormap",
801
        ),
802
        pytest.param(
803
            {
804
                "duration": 1,
805
                "sample_rate": 48_000,
806
                "nb_files": 1,
807
            },
808
            Timedelta(seconds=1),
809
            48_000,
810
            [ShortTimeFFT(win=hamming(1024), hop=1024, fs=48_000, mfft=1024)],
811
            None,
812
            None,
813
            None,
814
            Scale(
815
                [
816
                    ScalePart(0.0, 0.5, 100, 48_000, "log"),
817
                    ScalePart(0.0, 0.5, 0, 24_000, "lin"),
818
                ],
819
            ),
820
            None,
821
            id="specified_scale",
822
        ),
823
    ],
824
    indirect=["audio_files"],
825
)
826
def test_spectro_dataset_serialization(
1✔
827
    tmp_path: Path,
828
    audio_files: tuple[list[AudioFile], pytest.fixtures.Subrequest],
829
    data_duration: Timestamp | None,
830
    sample_rate: float | list[float],
831
    sfts: list[ShortTimeFFT],
832
    instrument: Instrument | None,
833
    v_lim: tuple[float, float] | None,
834
    colormap: str | None,
835
    frequency_scale: Scale | None,
836
    name: str | None,
837
) -> None:
838
    audio_files, _ = audio_files
1✔
839
    begin = min(af.begin for af in audio_files)
1✔
840

841
    strptime_format = (
1✔
842
        TIMESTAMP_FORMAT_EXPORTED_FILES_UNLOCALIZED
843
        if begin.tzinfo is None
844
        else TIMESTAMP_FORMAT_EXPORTED_FILES_LOCALIZED
845
    )
846

847
    ads = AudioDataset.from_folder(
1✔
848
        tmp_path,
849
        strptime_format=strptime_format,
850
        data_duration=data_duration,
851
        instrument=instrument,
852
    )
853

854
    ads.sample_rate = sample_rate
1✔
855

856
    sds = SpectroDataset.from_audio_dataset(
1✔
857
        audio_dataset=ads,
858
        fft=sfts[0],
859
        name=name,
860
        colormap=colormap,
861
        v_lim=v_lim,
862
        scale=frequency_scale,
863
    )
864
    for idx, sd in enumerate(sds.data):  # Apply different SFTs to the sds data
1✔
865
        sd.fft = sfts[idx // (len(sds.data) // len(sfts))]
1✔
866

867
    sds.write_json(tmp_path)
1✔
868

869
    sds2 = SpectroDataset.from_json(tmp_path / f"{sds}.json")
1✔
870

871
    assert str(sds) == str(sds2)
1✔
872
    assert sds.name == sds2.name
1✔
873
    assert sds.colormap == sds2.colormap
1✔
874
    assert sds.scale == sds2.scale
1✔
875
    assert sds.has_default_name == sds2.has_default_name
1✔
876
    assert sds.begin == sds2.begin
1✔
877
    assert all(
1✔
878
        np.array_equal(sd.get_value(), sd2.get_value())
879
        for sd, sd2 in zip(sds.data, sds2.data, strict=False)
880
    )
881
    assert all(
1✔
882
        sd.db_ref == sd2.db_ref for sd, sd2 in zip(sds.data, sds2.data, strict=False)
883
    )
884
    assert all(
1✔
885
        np.array_equal(sd.to_db(sd.get_value()), sd2.to_db(sd2.get_value()))
886
        for sd, sd2 in zip(sds.data, sds2.data, strict=False)
887
    )
888

889
    # Deserialized spectro data that share a same SFT should point to the same instance
890
    if len(sds2.data) > 1:
1✔
891
        assert sds2.data[0].fft == sds2.data[1].fft
1✔
892

893
    # SpectroDataset from npz files
894

895
    for sd in sds.data:
1✔
896
        sd.write(tmp_path)
1✔
897

898
    sds3 = SpectroDataset.from_folder(
1✔
899
        tmp_path,
900
        strptime_format=strptime_format,
901
        data_duration=data_duration,
902
    )
903

904
    assert all(
1✔
905
        np.array_equal(sd.get_value(), sd3.get_value())
906
        for sd, sd3 in zip(sds.data, sds3.data, strict=False)
907
    )
908
    assert all(
1✔
909
        sd.db_ref == sd3.db_ref for sd, sd3 in zip(sds.data, sds3.data, strict=False)
910
    )
911
    assert all(
1✔
912
        np.array_equal(sd.to_db(sd.get_value()), sd3.to_db(sd3.get_value()))
913
        for sd, sd3 in zip(sds.data, sds3.data, strict=False)
914
    )
915
    assert sds.begin == sds3.begin
1✔
916

917
    # Linked SpectroDataset JSON serialization
918

919
    sds.write(tmp_path / "linked", link=True)
1✔
920

921
    assert not any(sd.is_empty for sd in sds.data)
1✔
922

923
    sds.write_json(tmp_path / "linked")
1✔
924
    sds4 = SpectroDataset.from_json(tmp_path / "linked" / f"{sds}.json")
1✔
925

926
    assert all(
1✔
927
        sd1.files == sd2.files for sd1, sd2 in zip(sds.data, sds4.data, strict=True)
928
    )
929
    assert str(sds) == str(sds4)
1✔
930
    assert sds.name == sds4.name
1✔
931
    assert sds.colormap == sds4.colormap
1✔
932
    assert sds.scale == sds4.scale
1✔
933
    assert sds.has_default_name == sds4.has_default_name
1✔
934
    assert sds.begin == sds4.begin
1✔
935

936

937
"""
1✔
938
    >>> str(PureWindowsPath(relative_to_absolute(target_path=r'relative\target', root_path=r'C:\absolute\root')))
939
    'C:\\absolute\\root\\relative\\target'
940
    >>> str(PureWindowsPath(relative_to_absolute(target_path=r'D:\absolute\\path\root\target', root_path=r'C:\absolute\root')))
941
    'C:\\absolute\\path\\root\\target'
942
    >>> str(PurePosixPath(relative_to_absolute(target_path=r'C:/user/cool/data/audio/fun.wav', root_path=r'/home/dataset/cool/processed/stuff')))
943
    '/home/dataset/cool/data/audio/fun.wav'
944
"""
945

946

947
@pytest.mark.parametrize(
1✔
948
    ("target", "root", "expected"),
949
    [
950
        pytest.param(
951
            "saphir.wav",
952
            "C:/baston",
953
            "C:/baston/saphir.wav",
954
            id="relative_to_windows_root",
955
        ),
956
        pytest.param(
957
            "i/want/sky.wav",
958
            "C:/what/would",
959
            "C:/what/would/i/want/sky.wav",
960
            id="relative_tree_to_windows_root",
961
        ),
962
        pytest.param(
963
            "agency/group",
964
            "/the",
965
            "/the/agency/group",
966
            id="relative_tree_to_posix_root",
967
        ),
968
        pytest.param(
969
            "C:/user/dataset/file.csv",
970
            "C:/root/to/dataset",
971
            "C:/root/to/dataset/file.csv",
972
            id="absolute_windows_path_to_windows_root",
973
        ),
974
        pytest.param(
975
            "C:/user/dataset/file.csv",
976
            "C:/root/to/dataset/with/more",
977
            "C:/root/to/dataset/file.csv",
978
            id="absolute_windows_path_to_windows_root_takes_first_common_folder",
979
        ),
980
        pytest.param(
981
            "C:/user/datasets/audio/analysis/audio/audio/audio.wav",
982
            "C:/root/to/my_datasets/audio/some/stuff",
983
            "C:/root/to/my_datasets/audio/analysis/audio/audio/audio.wav",
984
            id="absolute_windows_path_to_windows_root_with_repeated_folders",
985
        ),
986
        pytest.param(
987
            "C:/user/datasets/audio/analysis/audio/audio/audio.wav",
988
            "/root/to/my_datasets/audio/some/stuff",
989
            "/root/to/my_datasets/audio/analysis/audio/audio/audio.wav",
990
            id="absolute_windows_path_to_posix_root_with_repeated_folders",
991
        ),
992
        pytest.param(
993
            "audio/analysis/audio/audio/audio.wav",
994
            "/root/to/my_datasets/audio/some/stuff",
995
            "/root/to/my_datasets/audio/some/stuff/audio/analysis/audio/audio/audio.wav",
996
            id="relative_path_to_posix_root_with_repeated_folders",
997
        ),
998
    ],
999
)
1000
def test_relative_to_absolute(target: str, root: str, expected: str) -> None:
1✔
1001
    path: Path = relative_to_absolute(target_path=target, root_path=root)
1✔
1002
    assert path.resolve() == Path(PureWindowsPath(expected)).resolve()
1✔
1003

1004

1005
def can_symlink(tmp_path: Path) -> bool:
1✔
1006
    try:
1✔
1007
        test_link = tmp_path / "link_test"
1✔
1008
        test_link.symlink_to(tmp_path)
1✔
1009
        test_link.unlink()
1✔
UNCOV
1010
    except OSError:
×
UNCOV
1011
        return False
×
1012
    return True
1✔
1013

1014

1015
@pytest.mark.parametrize(
1✔
1016
    ("target", "use_alias"),
1017
    [
1018
        pytest.param(
1019
            ".",
1020
            False,
1021
            id="same_path",
1022
        ),
1023
        pytest.param(
1024
            "../",
1025
            False,
1026
            id="parent",
1027
        ),
1028
        pytest.param(
1029
            "../milkkisses/serpentskirt",
1030
            False,
1031
            id="folder_in_parent",
1032
        ),
1033
        pytest.param(
1034
            "milkkisses/serpentskirt",
1035
            False,
1036
            id="folder_in_child",
1037
        ),
1038
        pytest.param(
1039
            "serpentskirt",
1040
            True,
1041
            id="drive_alias",
1042
        ),
1043
    ],
1044
)
1045
def test_absolute_ro_relative(
1✔
1046
    tmp_path: Path,
1047
    target: str,
1048
    use_alias: bool,
1049
) -> None:
1050
    if use_alias and not can_symlink(tmp_path):
1✔
UNCOV
1051
        pytest.skip("Symlink not permitted on this system.")
×
1052

1053
    real_root = tmp_path / "cocteau"
1✔
1054
    real_root.mkdir(parents=True, exist_ok=True)
1✔
1055

1056
    if use_alias:
1✔
1057
        alias_root = tmp_path / "twins"
1✔
1058
        alias_root.symlink_to(real_root)
1✔
1059
        root = alias_root
1✔
1060
    else:
1061
        root = real_root
1✔
1062

1063
    expected = Path(target)
1✔
1064

1065
    target = (real_root / target).resolve()
1✔
1066
    result = absolute_to_relative(target_path=target, root_path=root)
1✔
1067

1068
    assert result == expected
1✔
1069

1070

1071
def test_relative_paths_serialization(tmp_path: Path) -> None:
1✔
1072
    dictionary = {
1✔
1073
        "path": str(tmp_path / "user" / "cool"),
1074
        "folder": str(tmp_path / "user" / "cool"),
1075
        "json": str(tmp_path / "user" / "cool"),
1076
        "ignored": str(tmp_path / "user" / "cool"),
1077
        "not_a_path": "hello",
1078
        "nested_dict": {
1079
            "path": str(tmp_path / "user" / "cool"),
1080
            "folder": str(tmp_path / "user" / "cool"),
1081
            "json": str(tmp_path / "user" / "cool"),
1082
            "ignored": str(tmp_path / "user" / "cool"),
1083
            "not_a_path": "hello",
1084
        },
1085
    }
1086

1087
    relative_to_user_folder = {
1✔
1088
        "path": str(Path("cool")),
1089
        "folder": str(Path("cool")),
1090
        "json": str(Path("cool")),
1091
        "ignored": str(tmp_path / "user" / "cool"),
1092
        "not_a_path": "hello",
1093
        "nested_dict": {
1094
            "path": str(Path("cool")),
1095
            "folder": str(Path("cool")),
1096
            "json": str(Path("cool")),
1097
            "ignored": str(tmp_path / "user" / "cool"),
1098
            "not_a_path": "hello",
1099
        },
1100
    }
1101

1102
    dict_copy = dict(dictionary)
1✔
1103

1104
    # Absolute to relative
1105
    set_path_reference(
1✔
1106
        serialized_dict=dict_copy,
1107
        root_path=tmp_path / "user",
1108
        reference="relative",
1109
    )
1110
    assert dict_copy == relative_to_user_folder
1✔
1111

1112
    # Relative to absolute
1113
    set_path_reference(
1✔
1114
        serialized_dict=dict_copy,
1115
        root_path=tmp_path / "user",
1116
        reference="absolute",
1117
    )
1118
    assert dict_copy == dictionary
1✔
1119

1120
    # Absolute to absolute
1121
    set_path_reference(
1✔
1122
        serialized_dict=dict_copy,
1123
        root_path=tmp_path / "user",
1124
        reference="absolute",
1125
    )
1126
    assert dict_copy == dictionary
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