oscura 0.0.1__py3-none-any.whl → 0.1.0__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.
Files changed (465) hide show
  1. oscura/__init__.py +813 -8
  2. oscura/__main__.py +392 -0
  3. oscura/analyzers/__init__.py +37 -0
  4. oscura/analyzers/digital/__init__.py +177 -0
  5. oscura/analyzers/digital/bus.py +691 -0
  6. oscura/analyzers/digital/clock.py +805 -0
  7. oscura/analyzers/digital/correlation.py +720 -0
  8. oscura/analyzers/digital/edges.py +632 -0
  9. oscura/analyzers/digital/extraction.py +413 -0
  10. oscura/analyzers/digital/quality.py +878 -0
  11. oscura/analyzers/digital/signal_quality.py +877 -0
  12. oscura/analyzers/digital/thresholds.py +708 -0
  13. oscura/analyzers/digital/timing.py +1104 -0
  14. oscura/analyzers/eye/__init__.py +46 -0
  15. oscura/analyzers/eye/diagram.py +434 -0
  16. oscura/analyzers/eye/metrics.py +555 -0
  17. oscura/analyzers/jitter/__init__.py +83 -0
  18. oscura/analyzers/jitter/ber.py +333 -0
  19. oscura/analyzers/jitter/decomposition.py +759 -0
  20. oscura/analyzers/jitter/measurements.py +413 -0
  21. oscura/analyzers/jitter/spectrum.py +220 -0
  22. oscura/analyzers/measurements.py +40 -0
  23. oscura/analyzers/packet/__init__.py +171 -0
  24. oscura/analyzers/packet/daq.py +1077 -0
  25. oscura/analyzers/packet/metrics.py +437 -0
  26. oscura/analyzers/packet/parser.py +327 -0
  27. oscura/analyzers/packet/payload.py +2156 -0
  28. oscura/analyzers/packet/payload_analysis.py +1312 -0
  29. oscura/analyzers/packet/payload_extraction.py +236 -0
  30. oscura/analyzers/packet/payload_patterns.py +670 -0
  31. oscura/analyzers/packet/stream.py +359 -0
  32. oscura/analyzers/patterns/__init__.py +266 -0
  33. oscura/analyzers/patterns/clustering.py +1036 -0
  34. oscura/analyzers/patterns/discovery.py +539 -0
  35. oscura/analyzers/patterns/learning.py +797 -0
  36. oscura/analyzers/patterns/matching.py +1091 -0
  37. oscura/analyzers/patterns/periodic.py +650 -0
  38. oscura/analyzers/patterns/sequences.py +767 -0
  39. oscura/analyzers/power/__init__.py +116 -0
  40. oscura/analyzers/power/ac_power.py +391 -0
  41. oscura/analyzers/power/basic.py +383 -0
  42. oscura/analyzers/power/conduction.py +314 -0
  43. oscura/analyzers/power/efficiency.py +297 -0
  44. oscura/analyzers/power/ripple.py +356 -0
  45. oscura/analyzers/power/soa.py +372 -0
  46. oscura/analyzers/power/switching.py +479 -0
  47. oscura/analyzers/protocol/__init__.py +150 -0
  48. oscura/analyzers/protocols/__init__.py +150 -0
  49. oscura/analyzers/protocols/base.py +500 -0
  50. oscura/analyzers/protocols/can.py +620 -0
  51. oscura/analyzers/protocols/can_fd.py +448 -0
  52. oscura/analyzers/protocols/flexray.py +405 -0
  53. oscura/analyzers/protocols/hdlc.py +399 -0
  54. oscura/analyzers/protocols/i2c.py +368 -0
  55. oscura/analyzers/protocols/i2s.py +296 -0
  56. oscura/analyzers/protocols/jtag.py +393 -0
  57. oscura/analyzers/protocols/lin.py +445 -0
  58. oscura/analyzers/protocols/manchester.py +333 -0
  59. oscura/analyzers/protocols/onewire.py +501 -0
  60. oscura/analyzers/protocols/spi.py +334 -0
  61. oscura/analyzers/protocols/swd.py +325 -0
  62. oscura/analyzers/protocols/uart.py +393 -0
  63. oscura/analyzers/protocols/usb.py +495 -0
  64. oscura/analyzers/signal_integrity/__init__.py +63 -0
  65. oscura/analyzers/signal_integrity/embedding.py +294 -0
  66. oscura/analyzers/signal_integrity/equalization.py +370 -0
  67. oscura/analyzers/signal_integrity/sparams.py +484 -0
  68. oscura/analyzers/spectral/__init__.py +53 -0
  69. oscura/analyzers/spectral/chunked.py +273 -0
  70. oscura/analyzers/spectral/chunked_fft.py +571 -0
  71. oscura/analyzers/spectral/chunked_wavelet.py +391 -0
  72. oscura/analyzers/spectral/fft.py +92 -0
  73. oscura/analyzers/statistical/__init__.py +250 -0
  74. oscura/analyzers/statistical/checksum.py +923 -0
  75. oscura/analyzers/statistical/chunked_corr.py +228 -0
  76. oscura/analyzers/statistical/classification.py +778 -0
  77. oscura/analyzers/statistical/entropy.py +1113 -0
  78. oscura/analyzers/statistical/ngrams.py +614 -0
  79. oscura/analyzers/statistics/__init__.py +119 -0
  80. oscura/analyzers/statistics/advanced.py +885 -0
  81. oscura/analyzers/statistics/basic.py +263 -0
  82. oscura/analyzers/statistics/correlation.py +630 -0
  83. oscura/analyzers/statistics/distribution.py +298 -0
  84. oscura/analyzers/statistics/outliers.py +463 -0
  85. oscura/analyzers/statistics/streaming.py +93 -0
  86. oscura/analyzers/statistics/trend.py +520 -0
  87. oscura/analyzers/validation.py +598 -0
  88. oscura/analyzers/waveform/__init__.py +36 -0
  89. oscura/analyzers/waveform/measurements.py +943 -0
  90. oscura/analyzers/waveform/measurements_with_uncertainty.py +371 -0
  91. oscura/analyzers/waveform/spectral.py +1689 -0
  92. oscura/analyzers/waveform/wavelets.py +298 -0
  93. oscura/api/__init__.py +62 -0
  94. oscura/api/dsl.py +538 -0
  95. oscura/api/fluent.py +571 -0
  96. oscura/api/operators.py +498 -0
  97. oscura/api/optimization.py +392 -0
  98. oscura/api/profiling.py +396 -0
  99. oscura/automotive/__init__.py +73 -0
  100. oscura/automotive/can/__init__.py +52 -0
  101. oscura/automotive/can/analysis.py +356 -0
  102. oscura/automotive/can/checksum.py +250 -0
  103. oscura/automotive/can/correlation.py +212 -0
  104. oscura/automotive/can/discovery.py +355 -0
  105. oscura/automotive/can/message_wrapper.py +375 -0
  106. oscura/automotive/can/models.py +385 -0
  107. oscura/automotive/can/patterns.py +381 -0
  108. oscura/automotive/can/session.py +452 -0
  109. oscura/automotive/can/state_machine.py +300 -0
  110. oscura/automotive/can/stimulus_response.py +461 -0
  111. oscura/automotive/dbc/__init__.py +15 -0
  112. oscura/automotive/dbc/generator.py +156 -0
  113. oscura/automotive/dbc/parser.py +146 -0
  114. oscura/automotive/dtc/__init__.py +30 -0
  115. oscura/automotive/dtc/database.py +3036 -0
  116. oscura/automotive/j1939/__init__.py +14 -0
  117. oscura/automotive/j1939/decoder.py +745 -0
  118. oscura/automotive/loaders/__init__.py +35 -0
  119. oscura/automotive/loaders/asc.py +98 -0
  120. oscura/automotive/loaders/blf.py +77 -0
  121. oscura/automotive/loaders/csv_can.py +136 -0
  122. oscura/automotive/loaders/dispatcher.py +136 -0
  123. oscura/automotive/loaders/mdf.py +331 -0
  124. oscura/automotive/loaders/pcap.py +132 -0
  125. oscura/automotive/obd/__init__.py +14 -0
  126. oscura/automotive/obd/decoder.py +707 -0
  127. oscura/automotive/uds/__init__.py +48 -0
  128. oscura/automotive/uds/decoder.py +265 -0
  129. oscura/automotive/uds/models.py +64 -0
  130. oscura/automotive/visualization.py +369 -0
  131. oscura/batch/__init__.py +55 -0
  132. oscura/batch/advanced.py +627 -0
  133. oscura/batch/aggregate.py +300 -0
  134. oscura/batch/analyze.py +139 -0
  135. oscura/batch/logging.py +487 -0
  136. oscura/batch/metrics.py +556 -0
  137. oscura/builders/__init__.py +41 -0
  138. oscura/builders/signal_builder.py +1131 -0
  139. oscura/cli/__init__.py +14 -0
  140. oscura/cli/batch.py +339 -0
  141. oscura/cli/characterize.py +273 -0
  142. oscura/cli/compare.py +775 -0
  143. oscura/cli/decode.py +551 -0
  144. oscura/cli/main.py +247 -0
  145. oscura/cli/shell.py +350 -0
  146. oscura/comparison/__init__.py +66 -0
  147. oscura/comparison/compare.py +397 -0
  148. oscura/comparison/golden.py +487 -0
  149. oscura/comparison/limits.py +391 -0
  150. oscura/comparison/mask.py +434 -0
  151. oscura/comparison/trace_diff.py +30 -0
  152. oscura/comparison/visualization.py +481 -0
  153. oscura/compliance/__init__.py +70 -0
  154. oscura/compliance/advanced.py +756 -0
  155. oscura/compliance/masks.py +363 -0
  156. oscura/compliance/reporting.py +483 -0
  157. oscura/compliance/testing.py +298 -0
  158. oscura/component/__init__.py +38 -0
  159. oscura/component/impedance.py +365 -0
  160. oscura/component/reactive.py +598 -0
  161. oscura/component/transmission_line.py +312 -0
  162. oscura/config/__init__.py +191 -0
  163. oscura/config/defaults.py +254 -0
  164. oscura/config/loader.py +348 -0
  165. oscura/config/memory.py +271 -0
  166. oscura/config/migration.py +458 -0
  167. oscura/config/pipeline.py +1077 -0
  168. oscura/config/preferences.py +530 -0
  169. oscura/config/protocol.py +875 -0
  170. oscura/config/schema.py +713 -0
  171. oscura/config/settings.py +420 -0
  172. oscura/config/thresholds.py +599 -0
  173. oscura/convenience.py +457 -0
  174. oscura/core/__init__.py +299 -0
  175. oscura/core/audit.py +457 -0
  176. oscura/core/backend_selector.py +405 -0
  177. oscura/core/cache.py +590 -0
  178. oscura/core/cancellation.py +439 -0
  179. oscura/core/confidence.py +225 -0
  180. oscura/core/config.py +506 -0
  181. oscura/core/correlation.py +216 -0
  182. oscura/core/cross_domain.py +422 -0
  183. oscura/core/debug.py +301 -0
  184. oscura/core/edge_cases.py +541 -0
  185. oscura/core/exceptions.py +535 -0
  186. oscura/core/gpu_backend.py +523 -0
  187. oscura/core/lazy.py +832 -0
  188. oscura/core/log_query.py +540 -0
  189. oscura/core/logging.py +931 -0
  190. oscura/core/logging_advanced.py +952 -0
  191. oscura/core/memoize.py +171 -0
  192. oscura/core/memory_check.py +274 -0
  193. oscura/core/memory_guard.py +290 -0
  194. oscura/core/memory_limits.py +336 -0
  195. oscura/core/memory_monitor.py +453 -0
  196. oscura/core/memory_progress.py +465 -0
  197. oscura/core/memory_warnings.py +315 -0
  198. oscura/core/numba_backend.py +362 -0
  199. oscura/core/performance.py +352 -0
  200. oscura/core/progress.py +524 -0
  201. oscura/core/provenance.py +358 -0
  202. oscura/core/results.py +331 -0
  203. oscura/core/types.py +504 -0
  204. oscura/core/uncertainty.py +383 -0
  205. oscura/discovery/__init__.py +52 -0
  206. oscura/discovery/anomaly_detector.py +672 -0
  207. oscura/discovery/auto_decoder.py +415 -0
  208. oscura/discovery/comparison.py +497 -0
  209. oscura/discovery/quality_validator.py +528 -0
  210. oscura/discovery/signal_detector.py +769 -0
  211. oscura/dsl/__init__.py +73 -0
  212. oscura/dsl/commands.py +246 -0
  213. oscura/dsl/interpreter.py +455 -0
  214. oscura/dsl/parser.py +689 -0
  215. oscura/dsl/repl.py +172 -0
  216. oscura/exceptions.py +59 -0
  217. oscura/exploratory/__init__.py +111 -0
  218. oscura/exploratory/error_recovery.py +642 -0
  219. oscura/exploratory/fuzzy.py +513 -0
  220. oscura/exploratory/fuzzy_advanced.py +786 -0
  221. oscura/exploratory/legacy.py +831 -0
  222. oscura/exploratory/parse.py +358 -0
  223. oscura/exploratory/recovery.py +275 -0
  224. oscura/exploratory/sync.py +382 -0
  225. oscura/exploratory/unknown.py +707 -0
  226. oscura/export/__init__.py +25 -0
  227. oscura/export/wireshark/README.md +265 -0
  228. oscura/export/wireshark/__init__.py +47 -0
  229. oscura/export/wireshark/generator.py +312 -0
  230. oscura/export/wireshark/lua_builder.py +159 -0
  231. oscura/export/wireshark/templates/dissector.lua.j2 +92 -0
  232. oscura/export/wireshark/type_mapping.py +165 -0
  233. oscura/export/wireshark/validator.py +105 -0
  234. oscura/exporters/__init__.py +94 -0
  235. oscura/exporters/csv.py +303 -0
  236. oscura/exporters/exporters.py +44 -0
  237. oscura/exporters/hdf5.py +219 -0
  238. oscura/exporters/html_export.py +701 -0
  239. oscura/exporters/json_export.py +291 -0
  240. oscura/exporters/markdown_export.py +367 -0
  241. oscura/exporters/matlab_export.py +354 -0
  242. oscura/exporters/npz_export.py +219 -0
  243. oscura/exporters/spice_export.py +210 -0
  244. oscura/extensibility/__init__.py +131 -0
  245. oscura/extensibility/docs.py +752 -0
  246. oscura/extensibility/extensions.py +1125 -0
  247. oscura/extensibility/logging.py +259 -0
  248. oscura/extensibility/measurements.py +485 -0
  249. oscura/extensibility/plugins.py +414 -0
  250. oscura/extensibility/registry.py +346 -0
  251. oscura/extensibility/templates.py +913 -0
  252. oscura/extensibility/validation.py +651 -0
  253. oscura/filtering/__init__.py +89 -0
  254. oscura/filtering/base.py +563 -0
  255. oscura/filtering/convenience.py +564 -0
  256. oscura/filtering/design.py +725 -0
  257. oscura/filtering/filters.py +32 -0
  258. oscura/filtering/introspection.py +605 -0
  259. oscura/guidance/__init__.py +24 -0
  260. oscura/guidance/recommender.py +429 -0
  261. oscura/guidance/wizard.py +518 -0
  262. oscura/inference/__init__.py +251 -0
  263. oscura/inference/active_learning/README.md +153 -0
  264. oscura/inference/active_learning/__init__.py +38 -0
  265. oscura/inference/active_learning/lstar.py +257 -0
  266. oscura/inference/active_learning/observation_table.py +230 -0
  267. oscura/inference/active_learning/oracle.py +78 -0
  268. oscura/inference/active_learning/teachers/__init__.py +15 -0
  269. oscura/inference/active_learning/teachers/simulator.py +192 -0
  270. oscura/inference/adaptive_tuning.py +453 -0
  271. oscura/inference/alignment.py +653 -0
  272. oscura/inference/bayesian.py +943 -0
  273. oscura/inference/binary.py +1016 -0
  274. oscura/inference/crc_reverse.py +711 -0
  275. oscura/inference/logic.py +288 -0
  276. oscura/inference/message_format.py +1305 -0
  277. oscura/inference/protocol.py +417 -0
  278. oscura/inference/protocol_dsl.py +1084 -0
  279. oscura/inference/protocol_library.py +1230 -0
  280. oscura/inference/sequences.py +809 -0
  281. oscura/inference/signal_intelligence.py +1509 -0
  282. oscura/inference/spectral.py +215 -0
  283. oscura/inference/state_machine.py +634 -0
  284. oscura/inference/stream.py +918 -0
  285. oscura/integrations/__init__.py +59 -0
  286. oscura/integrations/llm.py +1827 -0
  287. oscura/jupyter/__init__.py +32 -0
  288. oscura/jupyter/display.py +268 -0
  289. oscura/jupyter/magic.py +334 -0
  290. oscura/loaders/__init__.py +526 -0
  291. oscura/loaders/binary.py +69 -0
  292. oscura/loaders/configurable.py +1255 -0
  293. oscura/loaders/csv.py +26 -0
  294. oscura/loaders/csv_loader.py +473 -0
  295. oscura/loaders/hdf5.py +9 -0
  296. oscura/loaders/hdf5_loader.py +510 -0
  297. oscura/loaders/lazy.py +370 -0
  298. oscura/loaders/mmap_loader.py +583 -0
  299. oscura/loaders/numpy_loader.py +436 -0
  300. oscura/loaders/pcap.py +432 -0
  301. oscura/loaders/preprocessing.py +368 -0
  302. oscura/loaders/rigol.py +287 -0
  303. oscura/loaders/sigrok.py +321 -0
  304. oscura/loaders/tdms.py +367 -0
  305. oscura/loaders/tektronix.py +711 -0
  306. oscura/loaders/validation.py +584 -0
  307. oscura/loaders/vcd.py +464 -0
  308. oscura/loaders/wav.py +233 -0
  309. oscura/math/__init__.py +45 -0
  310. oscura/math/arithmetic.py +824 -0
  311. oscura/math/interpolation.py +413 -0
  312. oscura/onboarding/__init__.py +39 -0
  313. oscura/onboarding/help.py +498 -0
  314. oscura/onboarding/tutorials.py +405 -0
  315. oscura/onboarding/wizard.py +466 -0
  316. oscura/optimization/__init__.py +19 -0
  317. oscura/optimization/parallel.py +440 -0
  318. oscura/optimization/search.py +532 -0
  319. oscura/pipeline/__init__.py +43 -0
  320. oscura/pipeline/base.py +338 -0
  321. oscura/pipeline/composition.py +242 -0
  322. oscura/pipeline/parallel.py +448 -0
  323. oscura/pipeline/pipeline.py +375 -0
  324. oscura/pipeline/reverse_engineering.py +1119 -0
  325. oscura/plugins/__init__.py +122 -0
  326. oscura/plugins/base.py +272 -0
  327. oscura/plugins/cli.py +497 -0
  328. oscura/plugins/discovery.py +411 -0
  329. oscura/plugins/isolation.py +418 -0
  330. oscura/plugins/lifecycle.py +959 -0
  331. oscura/plugins/manager.py +493 -0
  332. oscura/plugins/registry.py +421 -0
  333. oscura/plugins/versioning.py +372 -0
  334. oscura/py.typed +0 -0
  335. oscura/quality/__init__.py +65 -0
  336. oscura/quality/ensemble.py +740 -0
  337. oscura/quality/explainer.py +338 -0
  338. oscura/quality/scoring.py +616 -0
  339. oscura/quality/warnings.py +456 -0
  340. oscura/reporting/__init__.py +248 -0
  341. oscura/reporting/advanced.py +1234 -0
  342. oscura/reporting/analyze.py +448 -0
  343. oscura/reporting/argument_preparer.py +596 -0
  344. oscura/reporting/auto_report.py +507 -0
  345. oscura/reporting/batch.py +615 -0
  346. oscura/reporting/chart_selection.py +223 -0
  347. oscura/reporting/comparison.py +330 -0
  348. oscura/reporting/config.py +615 -0
  349. oscura/reporting/content/__init__.py +39 -0
  350. oscura/reporting/content/executive.py +127 -0
  351. oscura/reporting/content/filtering.py +191 -0
  352. oscura/reporting/content/minimal.py +257 -0
  353. oscura/reporting/content/verbosity.py +162 -0
  354. oscura/reporting/core.py +508 -0
  355. oscura/reporting/core_formats/__init__.py +17 -0
  356. oscura/reporting/core_formats/multi_format.py +210 -0
  357. oscura/reporting/engine.py +836 -0
  358. oscura/reporting/export.py +366 -0
  359. oscura/reporting/formatting/__init__.py +129 -0
  360. oscura/reporting/formatting/emphasis.py +81 -0
  361. oscura/reporting/formatting/numbers.py +403 -0
  362. oscura/reporting/formatting/standards.py +55 -0
  363. oscura/reporting/formatting.py +466 -0
  364. oscura/reporting/html.py +578 -0
  365. oscura/reporting/index.py +590 -0
  366. oscura/reporting/multichannel.py +296 -0
  367. oscura/reporting/output.py +379 -0
  368. oscura/reporting/pdf.py +373 -0
  369. oscura/reporting/plots.py +731 -0
  370. oscura/reporting/pptx_export.py +360 -0
  371. oscura/reporting/renderers/__init__.py +11 -0
  372. oscura/reporting/renderers/pdf.py +94 -0
  373. oscura/reporting/sections.py +471 -0
  374. oscura/reporting/standards.py +680 -0
  375. oscura/reporting/summary_generator.py +368 -0
  376. oscura/reporting/tables.py +397 -0
  377. oscura/reporting/template_system.py +724 -0
  378. oscura/reporting/templates/__init__.py +15 -0
  379. oscura/reporting/templates/definition.py +205 -0
  380. oscura/reporting/templates/index.html +649 -0
  381. oscura/reporting/templates/index.md +173 -0
  382. oscura/schemas/__init__.py +158 -0
  383. oscura/schemas/bus_configuration.json +322 -0
  384. oscura/schemas/device_mapping.json +182 -0
  385. oscura/schemas/packet_format.json +418 -0
  386. oscura/schemas/protocol_definition.json +363 -0
  387. oscura/search/__init__.py +16 -0
  388. oscura/search/anomaly.py +292 -0
  389. oscura/search/context.py +149 -0
  390. oscura/search/pattern.py +160 -0
  391. oscura/session/__init__.py +34 -0
  392. oscura/session/annotations.py +289 -0
  393. oscura/session/history.py +313 -0
  394. oscura/session/session.py +445 -0
  395. oscura/streaming/__init__.py +43 -0
  396. oscura/streaming/chunked.py +611 -0
  397. oscura/streaming/progressive.py +393 -0
  398. oscura/streaming/realtime.py +622 -0
  399. oscura/testing/__init__.py +54 -0
  400. oscura/testing/synthetic.py +808 -0
  401. oscura/triggering/__init__.py +68 -0
  402. oscura/triggering/base.py +229 -0
  403. oscura/triggering/edge.py +353 -0
  404. oscura/triggering/pattern.py +344 -0
  405. oscura/triggering/pulse.py +581 -0
  406. oscura/triggering/window.py +453 -0
  407. oscura/ui/__init__.py +48 -0
  408. oscura/ui/formatters.py +526 -0
  409. oscura/ui/progressive_display.py +340 -0
  410. oscura/utils/__init__.py +99 -0
  411. oscura/utils/autodetect.py +338 -0
  412. oscura/utils/buffer.py +389 -0
  413. oscura/utils/lazy.py +407 -0
  414. oscura/utils/lazy_imports.py +147 -0
  415. oscura/utils/memory.py +836 -0
  416. oscura/utils/memory_advanced.py +1326 -0
  417. oscura/utils/memory_extensions.py +465 -0
  418. oscura/utils/progressive.py +352 -0
  419. oscura/utils/windowing.py +362 -0
  420. oscura/visualization/__init__.py +321 -0
  421. oscura/visualization/accessibility.py +526 -0
  422. oscura/visualization/annotations.py +374 -0
  423. oscura/visualization/axis_scaling.py +305 -0
  424. oscura/visualization/colors.py +453 -0
  425. oscura/visualization/digital.py +337 -0
  426. oscura/visualization/eye.py +420 -0
  427. oscura/visualization/histogram.py +281 -0
  428. oscura/visualization/interactive.py +858 -0
  429. oscura/visualization/jitter.py +702 -0
  430. oscura/visualization/keyboard.py +394 -0
  431. oscura/visualization/layout.py +365 -0
  432. oscura/visualization/optimization.py +1028 -0
  433. oscura/visualization/palettes.py +446 -0
  434. oscura/visualization/plot.py +92 -0
  435. oscura/visualization/power.py +290 -0
  436. oscura/visualization/power_extended.py +626 -0
  437. oscura/visualization/presets.py +467 -0
  438. oscura/visualization/protocols.py +932 -0
  439. oscura/visualization/render.py +207 -0
  440. oscura/visualization/rendering.py +444 -0
  441. oscura/visualization/reverse_engineering.py +791 -0
  442. oscura/visualization/signal_integrity.py +808 -0
  443. oscura/visualization/specialized.py +553 -0
  444. oscura/visualization/spectral.py +811 -0
  445. oscura/visualization/styles.py +381 -0
  446. oscura/visualization/thumbnails.py +311 -0
  447. oscura/visualization/time_axis.py +351 -0
  448. oscura/visualization/waveform.py +367 -0
  449. oscura/workflow/__init__.py +13 -0
  450. oscura/workflow/dag.py +377 -0
  451. oscura/workflows/__init__.py +58 -0
  452. oscura/workflows/compliance.py +280 -0
  453. oscura/workflows/digital.py +272 -0
  454. oscura/workflows/multi_trace.py +502 -0
  455. oscura/workflows/power.py +178 -0
  456. oscura/workflows/protocol.py +492 -0
  457. oscura/workflows/reverse_engineering.py +639 -0
  458. oscura/workflows/signal_integrity.py +227 -0
  459. oscura-0.1.0.dist-info/METADATA +300 -0
  460. oscura-0.1.0.dist-info/RECORD +463 -0
  461. oscura-0.1.0.dist-info/entry_points.txt +2 -0
  462. {oscura-0.0.1.dist-info → oscura-0.1.0.dist-info}/licenses/LICENSE +1 -1
  463. oscura-0.0.1.dist-info/METADATA +0 -63
  464. oscura-0.0.1.dist-info/RECORD +0 -5
  465. {oscura-0.0.1.dist-info → oscura-0.1.0.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"]