oscura 0.0.1__py3-none-any.whl → 0.1.1__py3-none-any.whl
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.
- oscura/__init__.py +813 -8
- oscura/__main__.py +392 -0
- oscura/analyzers/__init__.py +37 -0
- oscura/analyzers/digital/__init__.py +177 -0
- oscura/analyzers/digital/bus.py +691 -0
- oscura/analyzers/digital/clock.py +805 -0
- oscura/analyzers/digital/correlation.py +720 -0
- oscura/analyzers/digital/edges.py +632 -0
- oscura/analyzers/digital/extraction.py +413 -0
- oscura/analyzers/digital/quality.py +878 -0
- oscura/analyzers/digital/signal_quality.py +877 -0
- oscura/analyzers/digital/thresholds.py +708 -0
- oscura/analyzers/digital/timing.py +1104 -0
- oscura/analyzers/eye/__init__.py +46 -0
- oscura/analyzers/eye/diagram.py +434 -0
- oscura/analyzers/eye/metrics.py +555 -0
- oscura/analyzers/jitter/__init__.py +83 -0
- oscura/analyzers/jitter/ber.py +333 -0
- oscura/analyzers/jitter/decomposition.py +759 -0
- oscura/analyzers/jitter/measurements.py +413 -0
- oscura/analyzers/jitter/spectrum.py +220 -0
- oscura/analyzers/measurements.py +40 -0
- oscura/analyzers/packet/__init__.py +171 -0
- oscura/analyzers/packet/daq.py +1077 -0
- oscura/analyzers/packet/metrics.py +437 -0
- oscura/analyzers/packet/parser.py +327 -0
- oscura/analyzers/packet/payload.py +2156 -0
- oscura/analyzers/packet/payload_analysis.py +1312 -0
- oscura/analyzers/packet/payload_extraction.py +236 -0
- oscura/analyzers/packet/payload_patterns.py +670 -0
- oscura/analyzers/packet/stream.py +359 -0
- oscura/analyzers/patterns/__init__.py +266 -0
- oscura/analyzers/patterns/clustering.py +1036 -0
- oscura/analyzers/patterns/discovery.py +539 -0
- oscura/analyzers/patterns/learning.py +797 -0
- oscura/analyzers/patterns/matching.py +1091 -0
- oscura/analyzers/patterns/periodic.py +650 -0
- oscura/analyzers/patterns/sequences.py +767 -0
- oscura/analyzers/power/__init__.py +116 -0
- oscura/analyzers/power/ac_power.py +391 -0
- oscura/analyzers/power/basic.py +383 -0
- oscura/analyzers/power/conduction.py +314 -0
- oscura/analyzers/power/efficiency.py +297 -0
- oscura/analyzers/power/ripple.py +356 -0
- oscura/analyzers/power/soa.py +372 -0
- oscura/analyzers/power/switching.py +479 -0
- oscura/analyzers/protocol/__init__.py +150 -0
- oscura/analyzers/protocols/__init__.py +150 -0
- oscura/analyzers/protocols/base.py +500 -0
- oscura/analyzers/protocols/can.py +620 -0
- oscura/analyzers/protocols/can_fd.py +448 -0
- oscura/analyzers/protocols/flexray.py +405 -0
- oscura/analyzers/protocols/hdlc.py +399 -0
- oscura/analyzers/protocols/i2c.py +368 -0
- oscura/analyzers/protocols/i2s.py +296 -0
- oscura/analyzers/protocols/jtag.py +393 -0
- oscura/analyzers/protocols/lin.py +445 -0
- oscura/analyzers/protocols/manchester.py +333 -0
- oscura/analyzers/protocols/onewire.py +501 -0
- oscura/analyzers/protocols/spi.py +334 -0
- oscura/analyzers/protocols/swd.py +325 -0
- oscura/analyzers/protocols/uart.py +393 -0
- oscura/analyzers/protocols/usb.py +495 -0
- oscura/analyzers/signal_integrity/__init__.py +63 -0
- oscura/analyzers/signal_integrity/embedding.py +294 -0
- oscura/analyzers/signal_integrity/equalization.py +370 -0
- oscura/analyzers/signal_integrity/sparams.py +484 -0
- oscura/analyzers/spectral/__init__.py +53 -0
- oscura/analyzers/spectral/chunked.py +273 -0
- oscura/analyzers/spectral/chunked_fft.py +571 -0
- oscura/analyzers/spectral/chunked_wavelet.py +391 -0
- oscura/analyzers/spectral/fft.py +92 -0
- oscura/analyzers/statistical/__init__.py +250 -0
- oscura/analyzers/statistical/checksum.py +923 -0
- oscura/analyzers/statistical/chunked_corr.py +228 -0
- oscura/analyzers/statistical/classification.py +778 -0
- oscura/analyzers/statistical/entropy.py +1113 -0
- oscura/analyzers/statistical/ngrams.py +614 -0
- oscura/analyzers/statistics/__init__.py +119 -0
- oscura/analyzers/statistics/advanced.py +885 -0
- oscura/analyzers/statistics/basic.py +263 -0
- oscura/analyzers/statistics/correlation.py +630 -0
- oscura/analyzers/statistics/distribution.py +298 -0
- oscura/analyzers/statistics/outliers.py +463 -0
- oscura/analyzers/statistics/streaming.py +93 -0
- oscura/analyzers/statistics/trend.py +520 -0
- oscura/analyzers/validation.py +598 -0
- oscura/analyzers/waveform/__init__.py +36 -0
- oscura/analyzers/waveform/measurements.py +943 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
- oscura/analyzers/waveform/spectral.py +1689 -0
- oscura/analyzers/waveform/wavelets.py +298 -0
- oscura/api/__init__.py +62 -0
- oscura/api/dsl.py +538 -0
- oscura/api/fluent.py +571 -0
- oscura/api/operators.py +498 -0
- oscura/api/optimization.py +392 -0
- oscura/api/profiling.py +396 -0
- oscura/automotive/__init__.py +73 -0
- oscura/automotive/can/__init__.py +52 -0
- oscura/automotive/can/analysis.py +356 -0
- oscura/automotive/can/checksum.py +250 -0
- oscura/automotive/can/correlation.py +212 -0
- oscura/automotive/can/discovery.py +355 -0
- oscura/automotive/can/message_wrapper.py +375 -0
- oscura/automotive/can/models.py +385 -0
- oscura/automotive/can/patterns.py +381 -0
- oscura/automotive/can/session.py +452 -0
- oscura/automotive/can/state_machine.py +300 -0
- oscura/automotive/can/stimulus_response.py +461 -0
- oscura/automotive/dbc/__init__.py +15 -0
- oscura/automotive/dbc/generator.py +156 -0
- oscura/automotive/dbc/parser.py +146 -0
- oscura/automotive/dtc/__init__.py +30 -0
- oscura/automotive/dtc/database.py +3036 -0
- oscura/automotive/j1939/__init__.py +14 -0
- oscura/automotive/j1939/decoder.py +745 -0
- oscura/automotive/loaders/__init__.py +35 -0
- oscura/automotive/loaders/asc.py +98 -0
- oscura/automotive/loaders/blf.py +77 -0
- oscura/automotive/loaders/csv_can.py +136 -0
- oscura/automotive/loaders/dispatcher.py +136 -0
- oscura/automotive/loaders/mdf.py +331 -0
- oscura/automotive/loaders/pcap.py +132 -0
- oscura/automotive/obd/__init__.py +14 -0
- oscura/automotive/obd/decoder.py +707 -0
- oscura/automotive/uds/__init__.py +48 -0
- oscura/automotive/uds/decoder.py +265 -0
- oscura/automotive/uds/models.py +64 -0
- oscura/automotive/visualization.py +369 -0
- oscura/batch/__init__.py +55 -0
- oscura/batch/advanced.py +627 -0
- oscura/batch/aggregate.py +300 -0
- oscura/batch/analyze.py +139 -0
- oscura/batch/logging.py +487 -0
- oscura/batch/metrics.py +556 -0
- oscura/builders/__init__.py +41 -0
- oscura/builders/signal_builder.py +1131 -0
- oscura/cli/__init__.py +14 -0
- oscura/cli/batch.py +339 -0
- oscura/cli/characterize.py +273 -0
- oscura/cli/compare.py +775 -0
- oscura/cli/decode.py +551 -0
- oscura/cli/main.py +247 -0
- oscura/cli/shell.py +350 -0
- oscura/comparison/__init__.py +66 -0
- oscura/comparison/compare.py +397 -0
- oscura/comparison/golden.py +487 -0
- oscura/comparison/limits.py +391 -0
- oscura/comparison/mask.py +434 -0
- oscura/comparison/trace_diff.py +30 -0
- oscura/comparison/visualization.py +481 -0
- oscura/compliance/__init__.py +70 -0
- oscura/compliance/advanced.py +756 -0
- oscura/compliance/masks.py +363 -0
- oscura/compliance/reporting.py +483 -0
- oscura/compliance/testing.py +298 -0
- oscura/component/__init__.py +38 -0
- oscura/component/impedance.py +365 -0
- oscura/component/reactive.py +598 -0
- oscura/component/transmission_line.py +312 -0
- oscura/config/__init__.py +191 -0
- oscura/config/defaults.py +254 -0
- oscura/config/loader.py +348 -0
- oscura/config/memory.py +271 -0
- oscura/config/migration.py +458 -0
- oscura/config/pipeline.py +1077 -0
- oscura/config/preferences.py +530 -0
- oscura/config/protocol.py +875 -0
- oscura/config/schema.py +713 -0
- oscura/config/settings.py +420 -0
- oscura/config/thresholds.py +599 -0
- oscura/convenience.py +457 -0
- oscura/core/__init__.py +299 -0
- oscura/core/audit.py +457 -0
- oscura/core/backend_selector.py +405 -0
- oscura/core/cache.py +590 -0
- oscura/core/cancellation.py +439 -0
- oscura/core/confidence.py +225 -0
- oscura/core/config.py +506 -0
- oscura/core/correlation.py +216 -0
- oscura/core/cross_domain.py +422 -0
- oscura/core/debug.py +301 -0
- oscura/core/edge_cases.py +541 -0
- oscura/core/exceptions.py +535 -0
- oscura/core/gpu_backend.py +523 -0
- oscura/core/lazy.py +832 -0
- oscura/core/log_query.py +540 -0
- oscura/core/logging.py +931 -0
- oscura/core/logging_advanced.py +952 -0
- oscura/core/memoize.py +171 -0
- oscura/core/memory_check.py +274 -0
- oscura/core/memory_guard.py +290 -0
- oscura/core/memory_limits.py +336 -0
- oscura/core/memory_monitor.py +453 -0
- oscura/core/memory_progress.py +465 -0
- oscura/core/memory_warnings.py +315 -0
- oscura/core/numba_backend.py +362 -0
- oscura/core/performance.py +352 -0
- oscura/core/progress.py +524 -0
- oscura/core/provenance.py +358 -0
- oscura/core/results.py +331 -0
- oscura/core/types.py +504 -0
- oscura/core/uncertainty.py +383 -0
- oscura/discovery/__init__.py +52 -0
- oscura/discovery/anomaly_detector.py +672 -0
- oscura/discovery/auto_decoder.py +415 -0
- oscura/discovery/comparison.py +497 -0
- oscura/discovery/quality_validator.py +528 -0
- oscura/discovery/signal_detector.py +769 -0
- oscura/dsl/__init__.py +73 -0
- oscura/dsl/commands.py +246 -0
- oscura/dsl/interpreter.py +455 -0
- oscura/dsl/parser.py +689 -0
- oscura/dsl/repl.py +172 -0
- oscura/exceptions.py +59 -0
- oscura/exploratory/__init__.py +111 -0
- oscura/exploratory/error_recovery.py +642 -0
- oscura/exploratory/fuzzy.py +513 -0
- oscura/exploratory/fuzzy_advanced.py +786 -0
- oscura/exploratory/legacy.py +831 -0
- oscura/exploratory/parse.py +358 -0
- oscura/exploratory/recovery.py +275 -0
- oscura/exploratory/sync.py +382 -0
- oscura/exploratory/unknown.py +707 -0
- oscura/export/__init__.py +25 -0
- oscura/export/wireshark/README.md +265 -0
- oscura/export/wireshark/__init__.py +47 -0
- oscura/export/wireshark/generator.py +312 -0
- oscura/export/wireshark/lua_builder.py +159 -0
- oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
- oscura/export/wireshark/type_mapping.py +165 -0
- oscura/export/wireshark/validator.py +105 -0
- oscura/exporters/__init__.py +94 -0
- oscura/exporters/csv.py +303 -0
- oscura/exporters/exporters.py +44 -0
- oscura/exporters/hdf5.py +219 -0
- oscura/exporters/html_export.py +701 -0
- oscura/exporters/json_export.py +291 -0
- oscura/exporters/markdown_export.py +367 -0
- oscura/exporters/matlab_export.py +354 -0
- oscura/exporters/npz_export.py +219 -0
- oscura/exporters/spice_export.py +210 -0
- oscura/extensibility/__init__.py +131 -0
- oscura/extensibility/docs.py +752 -0
- oscura/extensibility/extensions.py +1125 -0
- oscura/extensibility/logging.py +259 -0
- oscura/extensibility/measurements.py +485 -0
- oscura/extensibility/plugins.py +414 -0
- oscura/extensibility/registry.py +346 -0
- oscura/extensibility/templates.py +913 -0
- oscura/extensibility/validation.py +651 -0
- oscura/filtering/__init__.py +89 -0
- oscura/filtering/base.py +563 -0
- oscura/filtering/convenience.py +564 -0
- oscura/filtering/design.py +725 -0
- oscura/filtering/filters.py +32 -0
- oscura/filtering/introspection.py +605 -0
- oscura/guidance/__init__.py +24 -0
- oscura/guidance/recommender.py +429 -0
- oscura/guidance/wizard.py +518 -0
- oscura/inference/__init__.py +251 -0
- oscura/inference/active_learning/README.md +153 -0
- oscura/inference/active_learning/__init__.py +38 -0
- oscura/inference/active_learning/lstar.py +257 -0
- oscura/inference/active_learning/observation_table.py +230 -0
- oscura/inference/active_learning/oracle.py +78 -0
- oscura/inference/active_learning/teachers/__init__.py +15 -0
- oscura/inference/active_learning/teachers/simulator.py +192 -0
- oscura/inference/adaptive_tuning.py +453 -0
- oscura/inference/alignment.py +653 -0
- oscura/inference/bayesian.py +943 -0
- oscura/inference/binary.py +1016 -0
- oscura/inference/crc_reverse.py +711 -0
- oscura/inference/logic.py +288 -0
- oscura/inference/message_format.py +1305 -0
- oscura/inference/protocol.py +417 -0
- oscura/inference/protocol_dsl.py +1084 -0
- oscura/inference/protocol_library.py +1230 -0
- oscura/inference/sequences.py +809 -0
- oscura/inference/signal_intelligence.py +1509 -0
- oscura/inference/spectral.py +215 -0
- oscura/inference/state_machine.py +634 -0
- oscura/inference/stream.py +918 -0
- oscura/integrations/__init__.py +59 -0
- oscura/integrations/llm.py +1827 -0
- oscura/jupyter/__init__.py +32 -0
- oscura/jupyter/display.py +268 -0
- oscura/jupyter/magic.py +334 -0
- oscura/loaders/__init__.py +526 -0
- oscura/loaders/binary.py +69 -0
- oscura/loaders/configurable.py +1255 -0
- oscura/loaders/csv.py +26 -0
- oscura/loaders/csv_loader.py +473 -0
- oscura/loaders/hdf5.py +9 -0
- oscura/loaders/hdf5_loader.py +510 -0
- oscura/loaders/lazy.py +370 -0
- oscura/loaders/mmap_loader.py +583 -0
- oscura/loaders/numpy_loader.py +436 -0
- oscura/loaders/pcap.py +432 -0
- oscura/loaders/preprocessing.py +368 -0
- oscura/loaders/rigol.py +287 -0
- oscura/loaders/sigrok.py +321 -0
- oscura/loaders/tdms.py +367 -0
- oscura/loaders/tektronix.py +711 -0
- oscura/loaders/validation.py +584 -0
- oscura/loaders/vcd.py +464 -0
- oscura/loaders/wav.py +233 -0
- oscura/math/__init__.py +45 -0
- oscura/math/arithmetic.py +824 -0
- oscura/math/interpolation.py +413 -0
- oscura/onboarding/__init__.py +39 -0
- oscura/onboarding/help.py +498 -0
- oscura/onboarding/tutorials.py +405 -0
- oscura/onboarding/wizard.py +466 -0
- oscura/optimization/__init__.py +19 -0
- oscura/optimization/parallel.py +440 -0
- oscura/optimization/search.py +532 -0
- oscura/pipeline/__init__.py +43 -0
- oscura/pipeline/base.py +338 -0
- oscura/pipeline/composition.py +242 -0
- oscura/pipeline/parallel.py +448 -0
- oscura/pipeline/pipeline.py +375 -0
- oscura/pipeline/reverse_engineering.py +1119 -0
- oscura/plugins/__init__.py +122 -0
- oscura/plugins/base.py +272 -0
- oscura/plugins/cli.py +497 -0
- oscura/plugins/discovery.py +411 -0
- oscura/plugins/isolation.py +418 -0
- oscura/plugins/lifecycle.py +959 -0
- oscura/plugins/manager.py +493 -0
- oscura/plugins/registry.py +421 -0
- oscura/plugins/versioning.py +372 -0
- oscura/py.typed +0 -0
- oscura/quality/__init__.py +65 -0
- oscura/quality/ensemble.py +740 -0
- oscura/quality/explainer.py +338 -0
- oscura/quality/scoring.py +616 -0
- oscura/quality/warnings.py +456 -0
- oscura/reporting/__init__.py +248 -0
- oscura/reporting/advanced.py +1234 -0
- oscura/reporting/analyze.py +448 -0
- oscura/reporting/argument_preparer.py +596 -0
- oscura/reporting/auto_report.py +507 -0
- oscura/reporting/batch.py +615 -0
- oscura/reporting/chart_selection.py +223 -0
- oscura/reporting/comparison.py +330 -0
- oscura/reporting/config.py +615 -0
- oscura/reporting/content/__init__.py +39 -0
- oscura/reporting/content/executive.py +127 -0
- oscura/reporting/content/filtering.py +191 -0
- oscura/reporting/content/minimal.py +257 -0
- oscura/reporting/content/verbosity.py +162 -0
- oscura/reporting/core.py +508 -0
- oscura/reporting/core_formats/__init__.py +17 -0
- oscura/reporting/core_formats/multi_format.py +210 -0
- oscura/reporting/engine.py +836 -0
- oscura/reporting/export.py +366 -0
- oscura/reporting/formatting/__init__.py +129 -0
- oscura/reporting/formatting/emphasis.py +81 -0
- oscura/reporting/formatting/numbers.py +403 -0
- oscura/reporting/formatting/standards.py +55 -0
- oscura/reporting/formatting.py +466 -0
- oscura/reporting/html.py +578 -0
- oscura/reporting/index.py +590 -0
- oscura/reporting/multichannel.py +296 -0
- oscura/reporting/output.py +379 -0
- oscura/reporting/pdf.py +373 -0
- oscura/reporting/plots.py +731 -0
- oscura/reporting/pptx_export.py +360 -0
- oscura/reporting/renderers/__init__.py +11 -0
- oscura/reporting/renderers/pdf.py +94 -0
- oscura/reporting/sections.py +471 -0
- oscura/reporting/standards.py +680 -0
- oscura/reporting/summary_generator.py +368 -0
- oscura/reporting/tables.py +397 -0
- oscura/reporting/template_system.py +724 -0
- oscura/reporting/templates/__init__.py +15 -0
- oscura/reporting/templates/definition.py +205 -0
- oscura/reporting/templates/index.html +649 -0
- oscura/reporting/templates/index.md +173 -0
- oscura/schemas/__init__.py +158 -0
- oscura/schemas/bus_configuration.json +322 -0
- oscura/schemas/device_mapping.json +182 -0
- oscura/schemas/packet_format.json +418 -0
- oscura/schemas/protocol_definition.json +363 -0
- oscura/search/__init__.py +16 -0
- oscura/search/anomaly.py +292 -0
- oscura/search/context.py +149 -0
- oscura/search/pattern.py +160 -0
- oscura/session/__init__.py +34 -0
- oscura/session/annotations.py +289 -0
- oscura/session/history.py +313 -0
- oscura/session/session.py +445 -0
- oscura/streaming/__init__.py +43 -0
- oscura/streaming/chunked.py +611 -0
- oscura/streaming/progressive.py +393 -0
- oscura/streaming/realtime.py +622 -0
- oscura/testing/__init__.py +54 -0
- oscura/testing/synthetic.py +808 -0
- oscura/triggering/__init__.py +68 -0
- oscura/triggering/base.py +229 -0
- oscura/triggering/edge.py +353 -0
- oscura/triggering/pattern.py +344 -0
- oscura/triggering/pulse.py +581 -0
- oscura/triggering/window.py +453 -0
- oscura/ui/__init__.py +48 -0
- oscura/ui/formatters.py +526 -0
- oscura/ui/progressive_display.py +340 -0
- oscura/utils/__init__.py +99 -0
- oscura/utils/autodetect.py +338 -0
- oscura/utils/buffer.py +389 -0
- oscura/utils/lazy.py +407 -0
- oscura/utils/lazy_imports.py +147 -0
- oscura/utils/memory.py +836 -0
- oscura/utils/memory_advanced.py +1326 -0
- oscura/utils/memory_extensions.py +465 -0
- oscura/utils/progressive.py +352 -0
- oscura/utils/windowing.py +362 -0
- oscura/visualization/__init__.py +321 -0
- oscura/visualization/accessibility.py +526 -0
- oscura/visualization/annotations.py +374 -0
- oscura/visualization/axis_scaling.py +305 -0
- oscura/visualization/colors.py +453 -0
- oscura/visualization/digital.py +337 -0
- oscura/visualization/eye.py +420 -0
- oscura/visualization/histogram.py +281 -0
- oscura/visualization/interactive.py +858 -0
- oscura/visualization/jitter.py +702 -0
- oscura/visualization/keyboard.py +394 -0
- oscura/visualization/layout.py +365 -0
- oscura/visualization/optimization.py +1028 -0
- oscura/visualization/palettes.py +446 -0
- oscura/visualization/plot.py +92 -0
- oscura/visualization/power.py +290 -0
- oscura/visualization/power_extended.py +626 -0
- oscura/visualization/presets.py +467 -0
- oscura/visualization/protocols.py +932 -0
- oscura/visualization/render.py +207 -0
- oscura/visualization/rendering.py +444 -0
- oscura/visualization/reverse_engineering.py +791 -0
- oscura/visualization/signal_integrity.py +808 -0
- oscura/visualization/specialized.py +553 -0
- oscura/visualization/spectral.py +811 -0
- oscura/visualization/styles.py +381 -0
- oscura/visualization/thumbnails.py +311 -0
- oscura/visualization/time_axis.py +351 -0
- oscura/visualization/waveform.py +367 -0
- oscura/workflow/__init__.py +13 -0
- oscura/workflow/dag.py +377 -0
- oscura/workflows/__init__.py +58 -0
- oscura/workflows/compliance.py +280 -0
- oscura/workflows/digital.py +272 -0
- oscura/workflows/multi_trace.py +502 -0
- oscura/workflows/power.py +178 -0
- oscura/workflows/protocol.py +492 -0
- oscura/workflows/reverse_engineering.py +639 -0
- oscura/workflows/signal_integrity.py +227 -0
- oscura-0.1.1.dist-info/METADATA +300 -0
- oscura-0.1.1.dist-info/RECORD +463 -0
- oscura-0.1.1.dist-info/entry_points.txt +2 -0
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/licenses/LICENSE +1 -1
- oscura-0.0.1.dist-info/METADATA +0 -63
- oscura-0.0.1.dist-info/RECORD +0 -5
- {oscura-0.0.1.dist-info → oscura-0.1.1.dist-info}/WHEEL +0 -0
oscura/loaders/vcd.py
ADDED
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
"""IEEE 1364 VCD (Value Change Dump) file loader.
|
|
2
|
+
|
|
3
|
+
This module provides loading of VCD files, which are commonly used
|
|
4
|
+
for digital waveform data from logic analyzers and simulators.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from oscura.loaders.vcd import load_vcd
|
|
9
|
+
>>> trace = load_vcd("simulation.vcd")
|
|
10
|
+
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
from numpy.typing import NDArray
|
|
22
|
+
|
|
23
|
+
from oscura.core.exceptions import FormatError, LoaderError
|
|
24
|
+
from oscura.core.types import DigitalTrace, TraceMetadata
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from os import PathLike
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class VCDVariable:
|
|
32
|
+
"""VCD variable definition.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
var_type: Variable type (wire, reg, etc.).
|
|
36
|
+
size: Bit width of the variable.
|
|
37
|
+
identifier: Single-character identifier code.
|
|
38
|
+
name: Human-readable variable name.
|
|
39
|
+
scope: Hierarchical scope path.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
var_type: str
|
|
43
|
+
size: int
|
|
44
|
+
identifier: str
|
|
45
|
+
name: str
|
|
46
|
+
scope: str = ""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class VCDHeader:
|
|
51
|
+
"""Parsed VCD file header information.
|
|
52
|
+
|
|
53
|
+
Attributes:
|
|
54
|
+
timescale: Timescale in seconds (e.g., 1e-9 for 1ns).
|
|
55
|
+
variables: Dictionary mapping identifier to VCDVariable.
|
|
56
|
+
date: Date string from header.
|
|
57
|
+
version: VCD version string.
|
|
58
|
+
comment: Comment from header.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
timescale: float = 1e-9 # Default 1ns
|
|
62
|
+
variables: dict[str, VCDVariable] = field(default_factory=dict)
|
|
63
|
+
date: str = ""
|
|
64
|
+
version: str = ""
|
|
65
|
+
comment: str = ""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def load_vcd(
|
|
69
|
+
path: str | PathLike[str],
|
|
70
|
+
*,
|
|
71
|
+
signal: str | None = None,
|
|
72
|
+
sample_rate: float | None = None,
|
|
73
|
+
) -> DigitalTrace:
|
|
74
|
+
"""Load an IEEE 1364 VCD (Value Change Dump) file.
|
|
75
|
+
|
|
76
|
+
VCD files contain digital waveform data with value changes and
|
|
77
|
+
timestamps. This loader converts the event-based format to a
|
|
78
|
+
sampled digital trace.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
path: Path to the VCD file.
|
|
82
|
+
signal: Optional signal name to load. If None, loads the
|
|
83
|
+
first signal found.
|
|
84
|
+
sample_rate: Sample rate for conversion to sampled data.
|
|
85
|
+
If None, automatically determined from timescale.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
DigitalTrace containing the digital signal data and metadata.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
LoaderError: If the file cannot be loaded.
|
|
92
|
+
FormatError: If the file is not a valid VCD file.
|
|
93
|
+
|
|
94
|
+
Example:
|
|
95
|
+
>>> trace = load_vcd("simulation.vcd", signal="clk")
|
|
96
|
+
>>> print(f"Duration: {trace.duration:.6f} seconds")
|
|
97
|
+
>>> print(f"Edges: {len(trace.edges or [])}")
|
|
98
|
+
|
|
99
|
+
References:
|
|
100
|
+
IEEE 1364-2005: Verilog Hardware Description Language
|
|
101
|
+
"""
|
|
102
|
+
path = Path(path)
|
|
103
|
+
|
|
104
|
+
if not path.exists():
|
|
105
|
+
raise LoaderError(
|
|
106
|
+
"File not found",
|
|
107
|
+
file_path=str(path),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
try:
|
|
111
|
+
with open(path, encoding="utf-8", errors="replace") as f:
|
|
112
|
+
content = f.read()
|
|
113
|
+
|
|
114
|
+
# Parse header
|
|
115
|
+
header = _parse_vcd_header(content, path)
|
|
116
|
+
|
|
117
|
+
if not header.variables:
|
|
118
|
+
raise FormatError(
|
|
119
|
+
"No variables found in VCD file",
|
|
120
|
+
file_path=str(path),
|
|
121
|
+
expected="At least one $var definition",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Select signal to load
|
|
125
|
+
if signal is not None:
|
|
126
|
+
# Find by name
|
|
127
|
+
target_var = None
|
|
128
|
+
for var in header.variables.values():
|
|
129
|
+
if signal in (var.name, var.identifier):
|
|
130
|
+
target_var = var
|
|
131
|
+
break
|
|
132
|
+
if target_var is None:
|
|
133
|
+
available = [v.name for v in header.variables.values()]
|
|
134
|
+
raise LoaderError(
|
|
135
|
+
f"Signal '{signal}' not found",
|
|
136
|
+
file_path=str(path),
|
|
137
|
+
details=f"Available signals: {available}",
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
# Use first variable
|
|
141
|
+
target_var = next(iter(header.variables.values()))
|
|
142
|
+
|
|
143
|
+
# Parse value changes
|
|
144
|
+
changes = _parse_value_changes(content, target_var.identifier)
|
|
145
|
+
|
|
146
|
+
if not changes:
|
|
147
|
+
raise FormatError(
|
|
148
|
+
f"No value changes found for signal '{target_var.name}'",
|
|
149
|
+
file_path=str(path),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Determine sample rate and convert to sampled data
|
|
153
|
+
if sample_rate is None:
|
|
154
|
+
# Auto-determine from timescale and value changes
|
|
155
|
+
sample_rate = _determine_sample_rate(changes, header.timescale)
|
|
156
|
+
|
|
157
|
+
# Convert to sampled digital trace
|
|
158
|
+
data, edges = _changes_to_samples(
|
|
159
|
+
changes,
|
|
160
|
+
header.timescale,
|
|
161
|
+
sample_rate,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Build metadata
|
|
165
|
+
metadata = TraceMetadata(
|
|
166
|
+
sample_rate=sample_rate,
|
|
167
|
+
source_file=str(path),
|
|
168
|
+
channel_name=target_var.name,
|
|
169
|
+
trigger_info={
|
|
170
|
+
"timescale": header.timescale,
|
|
171
|
+
"var_type": target_var.var_type,
|
|
172
|
+
"bit_width": target_var.size,
|
|
173
|
+
},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
return DigitalTrace(
|
|
177
|
+
data=data.astype(np.bool_), # type: ignore[arg-type]
|
|
178
|
+
metadata=metadata,
|
|
179
|
+
edges=edges,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except UnicodeDecodeError as e:
|
|
183
|
+
raise FormatError(
|
|
184
|
+
"VCD file contains invalid characters",
|
|
185
|
+
file_path=str(path),
|
|
186
|
+
expected="UTF-8 or ASCII text",
|
|
187
|
+
) from e
|
|
188
|
+
except Exception as e:
|
|
189
|
+
if isinstance(e, LoaderError | FormatError):
|
|
190
|
+
raise
|
|
191
|
+
raise LoaderError(
|
|
192
|
+
"Failed to load VCD file",
|
|
193
|
+
file_path=str(path),
|
|
194
|
+
details=str(e),
|
|
195
|
+
fix_hint="Ensure the file is a valid IEEE 1364 VCD format.",
|
|
196
|
+
) from e
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _parse_vcd_header(content: str, path: Path) -> VCDHeader:
|
|
200
|
+
"""Parse VCD file header section.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
content: Full VCD file content.
|
|
204
|
+
path: Path for error messages.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Parsed VCDHeader object.
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
FormatError: If VCD header is invalid.
|
|
211
|
+
"""
|
|
212
|
+
header = VCDHeader()
|
|
213
|
+
current_scope: list[str] = []
|
|
214
|
+
|
|
215
|
+
# Find header section (before $enddefinitions)
|
|
216
|
+
end_def_match = re.search(r"\$enddefinitions\s+\$end", content)
|
|
217
|
+
if not end_def_match:
|
|
218
|
+
raise FormatError(
|
|
219
|
+
"Invalid VCD file: missing $enddefinitions",
|
|
220
|
+
file_path=str(path),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
header_content = content[: end_def_match.end()]
|
|
224
|
+
|
|
225
|
+
# Parse timescale
|
|
226
|
+
timescale_match = re.search(r"\$timescale\s+(\d+)\s*(s|ms|us|ns|ps|fs)\s+\$end", header_content)
|
|
227
|
+
if timescale_match:
|
|
228
|
+
value = int(timescale_match.group(1))
|
|
229
|
+
unit = timescale_match.group(2)
|
|
230
|
+
unit_multipliers = {
|
|
231
|
+
"s": 1.0,
|
|
232
|
+
"ms": 1e-3,
|
|
233
|
+
"us": 1e-6,
|
|
234
|
+
"ns": 1e-9,
|
|
235
|
+
"ps": 1e-12,
|
|
236
|
+
"fs": 1e-15,
|
|
237
|
+
}
|
|
238
|
+
header.timescale = value * unit_multipliers.get(unit, 1e-9)
|
|
239
|
+
|
|
240
|
+
# Parse date
|
|
241
|
+
date_match = re.search(r"\$date\s+(.*?)\s*\$end", header_content, re.DOTALL)
|
|
242
|
+
if date_match:
|
|
243
|
+
header.date = date_match.group(1).strip()
|
|
244
|
+
|
|
245
|
+
# Parse version
|
|
246
|
+
version_match = re.search(r"\$version\s+(.*?)\s*\$end", header_content, re.DOTALL)
|
|
247
|
+
if version_match:
|
|
248
|
+
header.version = version_match.group(1).strip()
|
|
249
|
+
|
|
250
|
+
# Parse comment
|
|
251
|
+
comment_match = re.search(r"\$comment\s+(.*?)\s*\$end", header_content, re.DOTALL)
|
|
252
|
+
if comment_match:
|
|
253
|
+
header.comment = comment_match.group(1).strip()
|
|
254
|
+
|
|
255
|
+
# Parse scopes and variables
|
|
256
|
+
scope_pattern = re.compile(r"\$scope\s+(\w+)\s+(\w+)\s+\$end")
|
|
257
|
+
upscope_pattern = re.compile(r"\$upscope\s+\$end")
|
|
258
|
+
var_pattern = re.compile(r"\$var\s+(\w+)\s+(\d+)\s+(\S+)\s+(\S+)(?:\s+\[.*?\])?\s+\$end")
|
|
259
|
+
|
|
260
|
+
pos = 0
|
|
261
|
+
while pos < len(header_content):
|
|
262
|
+
# Check for scope
|
|
263
|
+
scope_match = scope_pattern.match(header_content, pos)
|
|
264
|
+
if scope_match:
|
|
265
|
+
current_scope.append(scope_match.group(2))
|
|
266
|
+
pos = scope_match.end()
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
# Check for upscope
|
|
270
|
+
upscope_match = upscope_pattern.match(header_content, pos)
|
|
271
|
+
if upscope_match:
|
|
272
|
+
if current_scope:
|
|
273
|
+
current_scope.pop()
|
|
274
|
+
pos = upscope_match.end()
|
|
275
|
+
continue
|
|
276
|
+
|
|
277
|
+
# Check for variable
|
|
278
|
+
var_match = var_pattern.match(header_content, pos)
|
|
279
|
+
if var_match:
|
|
280
|
+
var = VCDVariable(
|
|
281
|
+
var_type=var_match.group(1),
|
|
282
|
+
size=int(var_match.group(2)),
|
|
283
|
+
identifier=var_match.group(3),
|
|
284
|
+
name=var_match.group(4),
|
|
285
|
+
scope=".".join(current_scope),
|
|
286
|
+
)
|
|
287
|
+
header.variables[var.identifier] = var
|
|
288
|
+
pos = var_match.end()
|
|
289
|
+
continue
|
|
290
|
+
|
|
291
|
+
pos += 1
|
|
292
|
+
|
|
293
|
+
return header
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _parse_value_changes(
|
|
297
|
+
content: str,
|
|
298
|
+
identifier: str,
|
|
299
|
+
) -> list[tuple[int, str]]:
|
|
300
|
+
"""Parse value changes for a specific signal.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
content: Full VCD file content.
|
|
304
|
+
identifier: Signal identifier to track.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
List of (timestamp, value) tuples.
|
|
308
|
+
"""
|
|
309
|
+
changes: list[tuple[int, str]] = []
|
|
310
|
+
current_time = 0
|
|
311
|
+
|
|
312
|
+
# Find data section (after $enddefinitions)
|
|
313
|
+
end_def_match = re.search(r"\$enddefinitions\s+\$end", content)
|
|
314
|
+
if not end_def_match:
|
|
315
|
+
return changes
|
|
316
|
+
|
|
317
|
+
data_content = content[end_def_match.end() :]
|
|
318
|
+
|
|
319
|
+
# Parse line by line
|
|
320
|
+
for line in data_content.split("\n"):
|
|
321
|
+
line = line.strip()
|
|
322
|
+
if not line:
|
|
323
|
+
continue
|
|
324
|
+
|
|
325
|
+
# Timestamp
|
|
326
|
+
if line.startswith("#"):
|
|
327
|
+
try:
|
|
328
|
+
current_time = int(line[1:])
|
|
329
|
+
except ValueError:
|
|
330
|
+
continue
|
|
331
|
+
|
|
332
|
+
# Binary value change: 0x, 1x, xx, zx (single bit)
|
|
333
|
+
elif line[0] in "01xXzZ" and len(line) >= 2:
|
|
334
|
+
value = line[0]
|
|
335
|
+
var_id = line[1:]
|
|
336
|
+
if var_id == identifier:
|
|
337
|
+
changes.append((current_time, value))
|
|
338
|
+
|
|
339
|
+
# Multi-bit value: bVALUE IDENTIFIER or BVALUE IDENTIFIER
|
|
340
|
+
elif line[0] in "bB" or line[0] in "rR":
|
|
341
|
+
parts = line[1:].split()
|
|
342
|
+
if len(parts) >= 2:
|
|
343
|
+
value = parts[0]
|
|
344
|
+
var_id = parts[1]
|
|
345
|
+
if var_id == identifier:
|
|
346
|
+
changes.append((current_time, value))
|
|
347
|
+
|
|
348
|
+
return changes
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _determine_sample_rate(
|
|
352
|
+
changes: list[tuple[int, str]],
|
|
353
|
+
timescale: float,
|
|
354
|
+
) -> float:
|
|
355
|
+
"""Determine appropriate sample rate from value changes.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
changes: List of (timestamp, value) tuples.
|
|
359
|
+
timescale: VCD timescale in seconds.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Sample rate in Hz.
|
|
363
|
+
"""
|
|
364
|
+
if len(changes) < 2:
|
|
365
|
+
# Default to 1 MHz if not enough data
|
|
366
|
+
return 1e6
|
|
367
|
+
|
|
368
|
+
# Calculate minimum time interval between changes
|
|
369
|
+
timestamps = sorted({t for t, _ in changes})
|
|
370
|
+
if len(timestamps) < 2:
|
|
371
|
+
return 1e6
|
|
372
|
+
|
|
373
|
+
min_interval = min(timestamps[i + 1] - timestamps[i] for i in range(len(timestamps) - 1))
|
|
374
|
+
|
|
375
|
+
if min_interval <= 0:
|
|
376
|
+
return 1e6
|
|
377
|
+
|
|
378
|
+
# Convert to seconds and set sample rate for ~10 samples per interval
|
|
379
|
+
interval_seconds = min_interval * timescale
|
|
380
|
+
sample_rate = 10.0 / interval_seconds
|
|
381
|
+
|
|
382
|
+
# Clamp to reasonable range
|
|
383
|
+
sample_rate = max(1e3, min(1e12, sample_rate))
|
|
384
|
+
|
|
385
|
+
return sample_rate
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _changes_to_samples(
|
|
389
|
+
changes: list[tuple[int, str]],
|
|
390
|
+
timescale: float,
|
|
391
|
+
sample_rate: float,
|
|
392
|
+
) -> tuple[NDArray[np.bool_], list[tuple[float, bool]]]:
|
|
393
|
+
"""Convert value changes to sampled data.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
changes: List of (timestamp, value) tuples.
|
|
397
|
+
timescale: VCD timescale in seconds.
|
|
398
|
+
sample_rate: Target sample rate in Hz.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
Tuple of (data array, edges list).
|
|
402
|
+
"""
|
|
403
|
+
if not changes:
|
|
404
|
+
return np.array([], dtype=np.bool_), []
|
|
405
|
+
|
|
406
|
+
# Sort changes by timestamp
|
|
407
|
+
changes = sorted(changes, key=lambda x: x[0])
|
|
408
|
+
|
|
409
|
+
# Get time range
|
|
410
|
+
start_time = changes[0][0]
|
|
411
|
+
end_time = changes[-1][0]
|
|
412
|
+
|
|
413
|
+
# Calculate number of samples
|
|
414
|
+
duration_seconds = (end_time - start_time) * timescale
|
|
415
|
+
n_samples = max(1, int(duration_seconds * sample_rate) + 1)
|
|
416
|
+
|
|
417
|
+
# Initialize data array
|
|
418
|
+
data = np.zeros(n_samples, dtype=np.bool_)
|
|
419
|
+
edges: list[tuple[float, bool]] = []
|
|
420
|
+
|
|
421
|
+
# Convert values to boolean (for single-bit) or LSB (for multi-bit)
|
|
422
|
+
def value_to_bool(val: str) -> bool:
|
|
423
|
+
"""Convert VCD value to boolean."""
|
|
424
|
+
val = val.lower()
|
|
425
|
+
if val in ("1", "h"):
|
|
426
|
+
return True
|
|
427
|
+
if val in ("0", "l"):
|
|
428
|
+
return False
|
|
429
|
+
# For multi-bit, check LSB
|
|
430
|
+
return bool(val and val[-1] in ("1", "h"))
|
|
431
|
+
|
|
432
|
+
# Fill samples based on value changes
|
|
433
|
+
prev_value = False
|
|
434
|
+
for i, (timestamp, value) in enumerate(changes):
|
|
435
|
+
current_value = value_to_bool(value)
|
|
436
|
+
|
|
437
|
+
# Calculate sample index
|
|
438
|
+
time_seconds = (timestamp - start_time) * timescale
|
|
439
|
+
sample_idx = int(time_seconds * sample_rate)
|
|
440
|
+
|
|
441
|
+
# Calculate next change sample index
|
|
442
|
+
if i + 1 < len(changes):
|
|
443
|
+
next_time_seconds = (changes[i + 1][0] - start_time) * timescale
|
|
444
|
+
next_sample_idx = int(next_time_seconds * sample_rate)
|
|
445
|
+
else:
|
|
446
|
+
next_sample_idx = n_samples
|
|
447
|
+
|
|
448
|
+
# Fill samples
|
|
449
|
+
sample_idx = max(0, min(sample_idx, n_samples - 1))
|
|
450
|
+
next_sample_idx = max(0, min(next_sample_idx, n_samples))
|
|
451
|
+
data[sample_idx:next_sample_idx] = current_value
|
|
452
|
+
|
|
453
|
+
# Record edge
|
|
454
|
+
if current_value != prev_value:
|
|
455
|
+
edge_time = time_seconds
|
|
456
|
+
is_rising = current_value
|
|
457
|
+
edges.append((edge_time, is_rising))
|
|
458
|
+
|
|
459
|
+
prev_value = current_value
|
|
460
|
+
|
|
461
|
+
return data, edges
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
__all__ = ["load_vcd"]
|
oscura/loaders/wav.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""WAV audio file loader.
|
|
2
|
+
|
|
3
|
+
This module provides loading of WAV audio files using scipy.io.wavfile.
|
|
4
|
+
WAV files are useful for audio signal analysis and can contain
|
|
5
|
+
oscilloscope data recorded as audio.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> from oscura.loaders.wav import load_wav
|
|
10
|
+
>>> trace = load_wav("recording.wav")
|
|
11
|
+
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
from scipy.io import wavfile
|
|
21
|
+
|
|
22
|
+
from oscura.core.exceptions import FormatError, LoaderError
|
|
23
|
+
from oscura.core.types import TraceMetadata, WaveformTrace
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from os import PathLike
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def load_wav(
|
|
30
|
+
path: str | PathLike[str],
|
|
31
|
+
*,
|
|
32
|
+
channel: int | str | None = None,
|
|
33
|
+
normalize: bool = True,
|
|
34
|
+
) -> WaveformTrace:
|
|
35
|
+
"""Load a WAV audio file.
|
|
36
|
+
|
|
37
|
+
Extracts audio samples and sample rate from WAV files. Supports
|
|
38
|
+
mono and stereo files, with automatic normalization to [-1, 1] range.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
path: Path to the WAV file.
|
|
42
|
+
channel: Channel to load for stereo files. Can be:
|
|
43
|
+
- 0 or "left": Left channel
|
|
44
|
+
- 1 or "right": Right channel
|
|
45
|
+
- "mono" or "mix": Average of both channels
|
|
46
|
+
- None: First channel (left for stereo)
|
|
47
|
+
normalize: If True, normalize samples to [-1, 1] range.
|
|
48
|
+
Default is True.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
WaveformTrace containing the audio data and metadata.
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
LoaderError: If the file cannot be loaded.
|
|
55
|
+
FormatError: If the file is not a valid WAV file.
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
>>> trace = load_wav("recording.wav")
|
|
59
|
+
>>> print(f"Sample rate: {trace.metadata.sample_rate} Hz")
|
|
60
|
+
>>> print(f"Duration: {trace.duration:.2f} seconds")
|
|
61
|
+
|
|
62
|
+
>>> # Load right channel of stereo file
|
|
63
|
+
>>> trace = load_wav("stereo.wav", channel="right")
|
|
64
|
+
|
|
65
|
+
References:
|
|
66
|
+
WAV file format: https://en.wikipedia.org/wiki/WAV
|
|
67
|
+
"""
|
|
68
|
+
path = Path(path)
|
|
69
|
+
|
|
70
|
+
if not path.exists():
|
|
71
|
+
raise LoaderError(
|
|
72
|
+
"File not found",
|
|
73
|
+
file_path=str(path),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
sample_rate, data = wavfile.read(str(path))
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
raise FormatError(
|
|
80
|
+
"Invalid WAV file format",
|
|
81
|
+
file_path=str(path),
|
|
82
|
+
expected="Valid WAV audio file",
|
|
83
|
+
) from e
|
|
84
|
+
except Exception as e:
|
|
85
|
+
raise LoaderError(
|
|
86
|
+
"Failed to read WAV file",
|
|
87
|
+
file_path=str(path),
|
|
88
|
+
details=str(e),
|
|
89
|
+
) from e
|
|
90
|
+
|
|
91
|
+
# Handle stereo/multichannel files
|
|
92
|
+
if data.ndim == 2:
|
|
93
|
+
n_channels = data.shape[1]
|
|
94
|
+
channel_names = (
|
|
95
|
+
["left", "right"] if n_channels == 2 else [f"ch{i}" for i in range(n_channels)]
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if channel is None:
|
|
99
|
+
# Default to first channel
|
|
100
|
+
audio_data = data[:, 0]
|
|
101
|
+
channel_name = channel_names[0]
|
|
102
|
+
elif isinstance(channel, int):
|
|
103
|
+
if channel < 0 or channel >= n_channels:
|
|
104
|
+
raise LoaderError(
|
|
105
|
+
f"Channel index {channel} out of range",
|
|
106
|
+
file_path=str(path),
|
|
107
|
+
details=f"Available channels: 0-{n_channels - 1}",
|
|
108
|
+
)
|
|
109
|
+
audio_data = data[:, channel]
|
|
110
|
+
channel_name = (
|
|
111
|
+
channel_names[channel] if channel < len(channel_names) else f"ch{channel}"
|
|
112
|
+
)
|
|
113
|
+
elif isinstance(channel, str):
|
|
114
|
+
channel_lower = channel.lower()
|
|
115
|
+
if channel_lower in ("left", "l", "0"):
|
|
116
|
+
audio_data = data[:, 0]
|
|
117
|
+
channel_name = "left"
|
|
118
|
+
elif channel_lower in ("right", "r", "1") and n_channels >= 2:
|
|
119
|
+
audio_data = data[:, 1]
|
|
120
|
+
channel_name = "right"
|
|
121
|
+
elif channel_lower in ("mono", "mix", "avg"):
|
|
122
|
+
# Average all channels
|
|
123
|
+
audio_data = np.mean(data, axis=1)
|
|
124
|
+
channel_name = "mono"
|
|
125
|
+
else:
|
|
126
|
+
raise LoaderError(
|
|
127
|
+
f"Invalid channel specifier: '{channel}'",
|
|
128
|
+
file_path=str(path),
|
|
129
|
+
details="Use 'left', 'right', 'mono', or channel index",
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
audio_data = data[:, 0] # type: ignore[unreachable]
|
|
133
|
+
channel_name = channel_names[0]
|
|
134
|
+
else:
|
|
135
|
+
# Mono file
|
|
136
|
+
if channel is not None and isinstance(channel, int) and channel != 0:
|
|
137
|
+
raise LoaderError(
|
|
138
|
+
f"Channel index {channel} out of range",
|
|
139
|
+
file_path=str(path),
|
|
140
|
+
details="File is mono (only channel 0 available)",
|
|
141
|
+
)
|
|
142
|
+
audio_data = data
|
|
143
|
+
channel_name = "mono"
|
|
144
|
+
|
|
145
|
+
# Convert to float64
|
|
146
|
+
audio_data = audio_data.astype(np.float64)
|
|
147
|
+
|
|
148
|
+
# Normalize based on original dtype
|
|
149
|
+
if normalize:
|
|
150
|
+
if data.dtype == np.int16:
|
|
151
|
+
audio_data = audio_data / 32768.0
|
|
152
|
+
elif data.dtype == np.int32:
|
|
153
|
+
audio_data = audio_data / 2147483648.0
|
|
154
|
+
elif data.dtype == np.uint8:
|
|
155
|
+
audio_data = (audio_data - 128.0) / 128.0
|
|
156
|
+
elif data.dtype in (np.float32, np.float64):
|
|
157
|
+
# Already in float format, typically [-1, 1]
|
|
158
|
+
# Clip to ensure range
|
|
159
|
+
max_val = np.max(np.abs(audio_data))
|
|
160
|
+
if max_val > 1.0:
|
|
161
|
+
audio_data = audio_data / max_val
|
|
162
|
+
|
|
163
|
+
# Build metadata
|
|
164
|
+
metadata = TraceMetadata(
|
|
165
|
+
sample_rate=float(sample_rate),
|
|
166
|
+
source_file=str(path),
|
|
167
|
+
channel_name=channel_name,
|
|
168
|
+
trigger_info={
|
|
169
|
+
"original_dtype": str(data.dtype),
|
|
170
|
+
"n_channels": data.shape[1] if data.ndim == 2 else 1,
|
|
171
|
+
"normalized": normalize,
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return WaveformTrace(data=audio_data, metadata=metadata)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def get_wav_info(
|
|
179
|
+
path: str | PathLike[str],
|
|
180
|
+
) -> dict: # type: ignore[type-arg]
|
|
181
|
+
"""Get WAV file information without loading all data.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
path: Path to the WAV file.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Dictionary with file information:
|
|
188
|
+
- sample_rate: Sample rate in Hz
|
|
189
|
+
- n_channels: Number of channels
|
|
190
|
+
- n_samples: Number of samples per channel
|
|
191
|
+
- duration: Duration in seconds
|
|
192
|
+
- dtype: Sample data type
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
LoaderError: If the file cannot be read.
|
|
196
|
+
|
|
197
|
+
Example:
|
|
198
|
+
>>> info = get_wav_info("recording.wav")
|
|
199
|
+
>>> print(f"Duration: {info['duration']:.2f}s")
|
|
200
|
+
>>> print(f"Channels: {info['n_channels']}")
|
|
201
|
+
"""
|
|
202
|
+
path = Path(path)
|
|
203
|
+
|
|
204
|
+
if not path.exists():
|
|
205
|
+
raise LoaderError(
|
|
206
|
+
"File not found",
|
|
207
|
+
file_path=str(path),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
sample_rate, data = wavfile.read(str(path))
|
|
212
|
+
|
|
213
|
+
n_samples = data.shape[0]
|
|
214
|
+
n_channels = data.shape[1] if data.ndim == 2 else 1
|
|
215
|
+
duration = n_samples / sample_rate
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"sample_rate": sample_rate,
|
|
219
|
+
"n_channels": n_channels,
|
|
220
|
+
"n_samples": n_samples,
|
|
221
|
+
"duration": duration,
|
|
222
|
+
"dtype": str(data.dtype),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
raise LoaderError(
|
|
227
|
+
"Failed to read WAV file info",
|
|
228
|
+
file_path=str(path),
|
|
229
|
+
details=str(e),
|
|
230
|
+
) from e
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
__all__ = ["get_wav_info", "load_wav"]
|